Обучение: Доступ к реляционной базе данных
В этом обучающем материале рассматриваются основы доступа к реляционной базе данных с использованием
Go и пакета database/sql из его стандартной библиотеки.
Чтобы получить максимальную отдачу от этого обучающего материала, вы должны иметь базовое знакомство с Go и его инструментами. Если вы впервые знакомитесь с Go, пожалуйста, ознакомьтесь с Обучение: Начало работы с Go для быстрого введения.
Пакет database/sql, который вы будете
использовать, включает типы и функции для подключения к базам данных, выполнения
транзакций, отмены выполняющейся операции и т. д. Для получения дополнительных сведений
о применении пакета, см.
Доступ к базам данных.
В этом обучающем материале вы создадите базу данных, а затем напишете код для доступа к базе данных. Ваш пример проекта будет репозиторием данных о винтажных джазовых пластинках.
В этом обучающем материале вы пройдете следующие разделы:
- Создайте папку для вашего кода.
- Настройте базу данных.
- Импортируйте драйвер базы данных.
- Получите дескриптор базы данных и подключитесь.
- Выполните запрос для получения нескольких строк.
- Выполните запрос для получения одной строки.
- Добавьте данные.
Примечание: Для других обучающих материалов см. Обучающие материалы.
Предварительные требования
- Установленная система управления реляционными базами данных MySQL.
- Установленный Go. Инструкции по установке см. Установка Go.
- Инструмент для редактирования кода. Подойдёт любой текстовый редактор.
- Командная строка. Go хорошо работает в любой терминальной среде в Linux и Mac, а также в PowerShell или cmd в Windows.
Создайте папку для вашего кода
Для начала создайте папку для кода, который вы будете писать.
-
Откройте командную строку и перейдите в домашний каталог.
В Linux или Mac:
<code>$ cd </code>
В Windows:
<code>C:\> cd %HOMEPATH% </code>
Для остальной части обучающего материала мы будем использовать $ в качестве приглашения. Команды, которые мы используем, также будут работать в Windows.
-
Из командной строки создайте каталог для вашего кода с названием data-access.
<code>$ mkdir data-access $ cd data-access </code>
-
Создайте модуль, в котором вы сможете управлять зависимостями, которые добавите во время этого обучающего материала.
Запустите команду
go mod init, указав путь к вашему новому коду.<code>$ go mod init example/data-access go: creating new go.mod: module example/data-access </code>
Эта команда создаёт файл go.mod, в котором будут перечислены зависимости, добавленные вами для отслеживания. Для получения дополнительной информации обязательно ознакомьтесь с Управление зависимостями.
Примечание: В реальной разработке вы указали бы путь к модулю, который будет более конкретным для ваших собственных нужд. Для получения дополнительной информации см. Управление зависимостями.
Далее вы создадите базу данных.
Настройка базы данных
На этом шаге вы создадите базу данных, с которой будете работать. Вы будете использовать CLI (командную строку) самой СУБД для создания базы данных и таблицы, а также для добавления данных.
Вы создадите базу данных с данными о виниловых записих джаза.
В коде ниже используется MySQL CLI, но большинство СУБД имеют собственную командную строку с аналогичными возможностями.
-
Откройте новую командную строку.
-
В командной строке войдите в вашу СУБД, как в следующем примере для MySQL.
<code>$ mysql -u root -p Enter password: mysql> </code>
-
В командной строке
mysqlсоздайте базу данных.<code>mysql> create database recordings; </code>
-
Переключитесь на только что созданную базу данных, чтобы можно было добавлять таблицы.
<code>mysql> use recordings; Database changed </code>
-
В вашем текстовом редакторе, в папке data-access, создайте файл с именем
create-tables.sqlдля хранения SQL-скрипта добавления таблиц. -
Вставьте следующий SQL-код в файл и сохраните его.
<code>DROP TABLE IF EXISTS album; CREATE TABLE album ( id INT AUTO_INCREMENT NOT NULL, title VARCHAR(128) NOT NULL, artist VARCHAR(255) NOT NULL, price DECIMAL(5,2) NOT NULL, PRIMARY KEY (`id`) ); INSERT INTO album (title, artist, price) VALUES ('Blue Train', 'John Coltrane', 56.99), ('Giant Steps', 'John Coltrane', 63.99), ('Jeru', 'Gerry Mulligan', 17.99), ('Sarah Vaughan', 'Sarah Vaughan', 34.98); </code>В этом SQL-коде вы:
-
Удаляете (drop) таблицу с именем
album. Выполнение этой команды сначала облегчает повторный запуск скрипта позже, если вы захотите начать сначала с таблицы. -
Создаёте таблицу
albumс четырьмя колонками:title,artistиprice. Значениеidкаждой строки создаётся автоматически СУБД. -
Добавляете четыре строки с данными.
-
-
Из командной строки
mysqlзапустите созданный вами скрипт.Вы будете использовать команду
sourceв следующем виде:<code>mysql> source /path/to/create-tables.sql </code>
-
В командной строке вашей СУБД используйте инструкцию
SELECT, чтобы проверить, успешно ли вы создали таблицу с данными.<code>mysql> select * from album; +----+---------------+----------------+-------+ | id | title | artist | price | +----+---------------+----------------+-------+ | 1 | Blue Train | John Coltrane | 56.99 | | 2 | Giant Steps | John Coltrane | 63.99 | | 3 | Jeru | Gerry Mulligan | 17.99 | | 4 | Sarah Vaughan | Sarah Vaughan | 34.98 | +----+---------------+----------------+-------+ 4 rows in set (0.00 sec) </code>
Далее вы напишете некоторый код на Go, чтобы установить соединение и выполнить запросы.
Поиск и импорт драйвера базы данных
Теперь, когда у вас есть база данных с некоторыми данными, начните писать код на Go.
Найдите и импортируйте драйвер базы данных, который будет переводить запросы, выполняемые вами через функции в пакете database/sql, в запросы, понятные базе данных.
-
В вашем браузере откройте страницу SQLDrivers в вики, чтобы определить драйвер, который можно использовать.
Используйте список на странице, чтобы определить драйвер, который вы будете использовать. Для доступа к MySQL в этом руководстве вы будете использовать Go-MySQL-Driver.
-
Заметьте название пакета для драйвера – в данном случае
github.com/go-sql-driver/mysql. -
С помощью вашего текстового редактора создайте файл для записи кода на Go и сохраните его как main.go в каталоге data-access, который вы создали ранее.
-
В файл main.go вставьте следующий код для импорта пакета драйвера.
<code>package main import "github.com/go-sql-driver/mysql" </code>
В этом коде вы:
-
Добавляете свой код в пакет
main, чтобы его можно было запускать независимо. -
Импортируете драйвер MySQL
github.com/go-sql-driver/mysql.
-
После импорта драйвера вы начнете писать код для доступа к базе данных.
Получение дескриптора базы данных и установление соединения
Теперь напишите некоторый код на Go, который предоставит вам доступ к базе данных с помощью дескриптора базы данных.
Вы будете использовать указатель на структуру sql.DB, которая представляет доступ к определённой базе данных.
Написание кода
-
В файл main.go, под кодом
import, который вы только что добавили, вставьте следующий код на Go для создания дескриптора базы данных.<code>var db *sql.DB func main() { // Capture connection properties. cfg := mysql.NewConfig() cfg.User = os.Getenv("DBUSER") cfg.Passwd = os.Getenv("DBPASS") cfg.Net = "tcp" cfg.Addr = "127.0.0.1:3306" cfg.DBName = "recordings" // Get a database handle. var err error db, err = sql.Open("mysql", cfg.FormatDSN()) if err != nil { log.Fatal(err) } pingErr := db.Ping() if pingErr != nil { log.Fatal(pingErr) } fmt.Println("Connected!") } </code>В этом коде вы:
-
Объявляете переменную
dbтипа*sql.DB. Это ваш дескриптор базы данных.Сделав
dbглобальной переменной, вы упрощаете этот пример. В рабочем коде вы избегали бы глобальной переменной, передавая переменную функциям, которым она нужна, или обернув её в структуру. -
Используете
Configдрайвера MySQL и методFormatDSNдля сбора свойств соединения и форматирования их в DSN для строки подключения.Структура
Configделает код более читаемым по сравнению со строкой подключения. -
Вызываете
sql.Openдля инициализации переменнойdb, передавая возвращаемое значениеFormatDSN. -
Проверяете наличие ошибки от
sql.Open. Она может возникнуть, например, если свойства соединения с базой данных были некорректно заданы.Для упрощения кода вы вызываете
log.Fatal, чтобы завершить выполнение и вывести ошибку в консоль. В рабочем коде вы захотите обрабатывать ошибки более грациозным способом. -
Вызываете
DB.Pingдля подтверждения того, что соединение с базой данных установлено. Во время выполненияsql.Openможет не сразу устанавливать соединение, в зависимости от драйвера. Вы используетеPingздесь, чтобы убедиться, что пакетdatabase/sqlсможет подключиться, когда это будет необходимо. -
Проверяете наличие ошибки от
Ping, если соединение не удалось. -
Выводите сообщение, если
Pingуспешно установил соединение.
-
-
В верхней части файла main.go, сразу под объявлением пакета, импортируйте необходимые пакеты для поддержки написанного вами кода.
Верхняя часть файла теперь должна выглядеть так:
<code>package main import ( "database/sql" "fmt" "log" "os" "github.com/go-sql-driver/mysql" ) </code>
-
Сохраните main.go.
Запустите код
-
Начните отслеживать модуль драйвера MySQL как зависимость.
Используйте
go getдля добавления модуля github.com/go-sql-driver/mysql в качестве зависимости для вашего собственного модуля. Используйте аргумент с точкой, чтобы обозначить «получить зависимости для кода в текущем каталоге».<code>$ go get . go: added filippo.io/edwards25519 v1.1.0 go: added github.com/go-sql-driver/mysql v1.8.1 </code>
Go загрузил эту зависимость, потому что вы добавили её в инструкцию
importна предыдущем шаге. Для получения дополнительной информации об отслеживании зависимостей, см. Добавление зависимости. -
В командной строке установите переменные среды
DBUSERиDBPASSдля использования программой Go.В Linux или Mac:
<code>$ export DBUSER=username $ export DBPASS=password </code>
В Windows:
<code>C:\Users\you\data-access> set DBUSER=username C:\Users\you\data-access> set DBPASS=password </code>
-
В командной строке в каталоге, содержащем main.go, запустите код, введя
go runс аргументом точки, чтобы обозначить «запустить пакет в текущем каталоге».<code>$ go run . Connected! </code>
Вы можете подключиться! Далее вы выполните запрос для получения данных.
Запрос нескольких строк
В этом разделе вы будете использовать Go для выполнения SQL-запроса, предназначенного для возврата нескольких строк.
Для SQL-инструкций, которые могут возвращать несколько строк, вы используете метод Query
из пакета database/sql, а затем проходите по строкам, которые он возвращает. (Вы узнаете, как выполнять запросы для одной строки позже, в разделе
Запрос одной строки.)
Напишите код
-
В файл main.go, непосредственно над
func main, вставьте следующее определение структурыAlbum. Вы будете использовать её для хранения данных строк, возвращённых из запроса.<code>type Album struct { ID int64 Title string Artist string Price float32 } </code> -
Под
func mainвставьте следующую функциюalbumsByArtistдля запроса к базе данных.<code>// albumsByArtist queries for albums that have the specified artist name. func albumsByArtist(name string) ([]Album, error) { // An albums slice to hold data from returned rows. var albums []Album rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name) if err != nil { return nil, fmt.Errorf("albumsByArtist %q: %v", name, err) } defer rows.Close() // Loop through rows, using Scan to assign column data to struct fields. for rows.Next() { var alb Album if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil { return nil, fmt.Errorf("albumsByArtist %q: %v", name, err) } albums = append(albums, alb) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("albumsByArtist %q: %v", name, err) } return albums, nil } </code>В этом коде вы:
-
Объявляете срез
albumsтипаAlbum, который вы определили. Он будет содержать данные из возвращённых строк. Имена и типы полей структуры соответствуют именам и типам столбцов базы данных. -
Используете
DB.Queryдля выполнения оператораSELECTдля запроса альбомов с указанным именем исполнителя.Первым параметром
Queryявляется SQL-выражение. После параметра можно передать ноль или более параметров любого типа. Они обеспечивают место для указания значений параметров в SQL-выражении. Разделяя SQL-выражение от значений параметров (вместо того, чтобы объединять их, например, с помощьюfmt.Sprintf), вы позволяете пакетуdatabase/sqlотправлять значения отдельно от текста SQL, что устраняет риск SQL-инъекции. -
Откладываете закрытие
rows, чтобы все ресурсы, которые он удерживает, были освобождены при выходе из функции. -
Проходите по возвращённым строкам, используя
Rows.Scanдля присвоения значений столбцов полям структурыAlbum.Scanпринимает список указателей на значения Go, в которые будут записаны значения столбцов. Здесь вы передаёте указатели на поля переменнойalb, созданной с помощью оператора&.Scanзаписывает через указатели, чтобы обновить поля структуры. -
Внутри цикла проверяете наличие ошибки при сканировании значений столбцов в поля структуры.
-
Внутри цикла добавляете новый
albв срезalbums. -
После цикла проверяете наличие ошибки в результате всего запроса, используя
rows.Err. Обратите внимание, что если сам запрос завершится ошибкой, проверка на ошибку здесь — единственная возможность узнать, что результаты неполны.
-
-
Обновите функцию
main, чтобы вызватьalbumsByArtist.В конец функции
func mainдобавьте следующий код.<code>albums, err := albumsByArtist("John Coltrane") if err != nil { log.Fatal(err) } fmt.Printf("Albums found: %v\n", albums) </code>В новом коде вы теперь:
-
Вызываете добавленную функцию
albumsByArtist, присваивая её возвращаемое значение новой переменнойalbums. -
Выводите результат.
-
Запустите код
Из командной строки в директории, содержащей main.go, выполните код.
<code>$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
</code>
Далее вы выполните запрос для получения одной строки.
Запрос для одной строки
В этом разделе вы будете использовать Go для запроса одной строки из базы данных.
Для SQL-выражений, которые, как известно, возвращают не более одной строки, можно использовать
QueryRow, что проще, чем использование цикла Query.
Напишите код
-
Под функцией
albumsByArtistвставьте следующую функциюalbumByID.<code>// albumByID queries for the album with the specified ID. func albumByID(id int64) (Album, error) { // An album to hold data from the returned row. var alb Album row := db.QueryRow("SELECT * FROM album WHERE id = ?", id) if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil { if err == sql.ErrNoRows { return alb, fmt.Errorf("albumsById %d: no such album", id) } return alb, fmt.Errorf("albumsById %d: %v", id, err) } return alb, nil } </code>В этом коде вы:
-
Используете
DB.QueryRowдля выполненияSELECT-выражения, чтобы запросить альбом с указанным ID.Он возвращает
sql.Row. Чтобы упростить код вызова (ваш код!),QueryRowне возвращает ошибку. Вместо этого, он организует возврат любой ошибки запроса (например,sql.ErrNoRows) изRows.Scanпозже. -
Используете
Row.Scanдля копирования значений столбцов в поля структуры. -
Проверяете наличие ошибки от
Scan.Специальная ошибка
sql.ErrNoRowsуказывает, что запрос не вернул ни одной строки. Обычно такая ошибка подлежит замене более конкретным текстом, например, здесь — «нет такого альбома».
-
-
Обновите
main, чтобы вызватьalbumByID.В конец функции
func mainдобавьте следующий код.<code>// Hard-code ID 2 here to test the query. alb, err := albumByID(2) if err != nil { log.Fatal(err) } fmt.Printf("Album found: %v\n", alb) </code>В новом коде вы теперь:
-
Вызываете добавленную функцию
albumByID. -
Выводите возвращенный ID альбома.
-
Запустите код
Из командной строки в директории, содержащей main.go, выполните код.
<code>$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
Album found: {2 Giant Steps John Coltrane 63.99}
</code>
Далее вы добавите альбом в базу данных.
Добавление данных
В этом разделе вы будете использовать Go для выполнения SQL-инструкции INSERT, чтобы добавить новую строку в базу данных.
Вы уже видели, как использовать Query и QueryRow с SQL-запросами, которые возвращают данные. Чтобы выполнить SQL-запросы, которые не возвращают данные, используется Exec.
Напишите код
-
Под функцией
albumByIDвставьте следующую функциюaddAlbumдля вставки нового альбома в базу данных, затем сохраните файл main.go.<code>// addAlbum adds the specified album to the database, // returning the album ID of the new entry func addAlbum(alb Album) (int64, error) { result, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)", alb.Title, alb.Artist, alb.Price) if err != nil { return 0, fmt.Errorf("addAlbum: %v", err) } id, err := result.LastInsertId() if err != nil { return 0, fmt.Errorf("addAlbum: %v", err) } return id, nil } </code>В этом коде вы:
-
Используете
DB.Execдля выполнения инструкцииINSERT.Как и
Query,Execпринимает SQL-запрос, за которым следуют значения параметров для SQL-запроса. -
Проверяете наличие ошибки при попытке выполнить
INSERT. -
Получаете ID вставленной строки базы данных с помощью
Result.LastInsertId. -
Проверяете наличие ошибки при попытке получить ID.
-
-
Обновите
mainдля вызова новой функцииaddAlbum.В конец функции
func mainдобавьте следующий код.<code>albID, err := addAlbum(Album{ Title: "The Modern Sound of Betty Carter", Artist: "Betty Carter", Price: 49.99, }) if err != nil { log.Fatal(err) } fmt.Printf("ID of added album: %v\n", albID) </code>В новом коде вы:
- Вызываете
addAlbumс новым альбомом, присваивая ID добавляемого альбома переменнойalbID.
- Вызываете
Запустите код
Из командной строки в директории, содержащей main.go, выполните код.
<code>$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
Album found: {2 Giant Steps John Coltrane 63.99}
ID of added album: 5
</code>
Заключение
Поздравляем! Вы только что использовали Go для выполнения простых действий с реляционной базой данных.
Предлагаемые следующие темы:
-
Ознакомьтесь с руководством по доступу к данным, которое содержит дополнительную информацию о темах, только кратко рассмотренных здесь.
-
Если вы новичок в Go, вы найдете полезные рекомендации в Effective Go и How to write Go code.
-
Go Tour — отличное пошаговое введение в основы языка Go.
Завершённый код
В этом разделе содержится код приложения, которое вы создаёте с помощью этого руководства.
<code>package main
import (
"database/sql"
"fmt"
"log"
"os"
"github.com/go-sql-driver/mysql"
)
var db *sql.DB
type Album struct {
ID int64
Title string
Artist string
Price float32
}
func main() {
// Capture connection properties.
cfg := mysql.NewConfig()
cfg.User = os.Getenv("DBUSER")
cfg.Passwd = os.Getenv("DBPASS")
cfg.Net = "tcp"
cfg.Addr = "127.0.0.1:3306"
cfg.DBName = "recordings"
// Get a database handle.
var err error
db, err = sql.Open("mysql", cfg.FormatDSN())
if err != nil {
log.Fatal(err)
}
pingErr := db.Ping()
if pingErr != nil {
log.Fatal(pingErr)
}
fmt.Println("Connected!")
albums, err := albumsByArtist("John Coltrane")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Albums found: %v\n", albums)
// Hard-code ID 2 here to test the query.
alb, err := albumByID(2)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Album found: %v\n", alb)
albID, err := addAlbum(Album{
Title: "The Modern Sound of Betty Carter",
Artist: "Betty Carter",
Price: 49.99,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("ID of added album: %v\n", albID)
}
// albumsByArtist queries for albums that have the specified artist name.
func albumsByArtist(name string) ([]Album, error) {
// An albums slice to hold data from returned rows.
var albums []Album
rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name)
if err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
}
defer rows.Close()
// Loop through rows, using Scan to assign column data to struct fields.
for rows.Next() {
var alb Album
if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
}
albums = append(albums, alb)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
}
return albums, nil
}
// albumByID queries for the album with the specified ID.
func albumByID(id int64) (Album, error) {
// An album to hold data from the returned row.
var alb Album
row := db.QueryRow("SELECT * FROM album WHERE id = ?", id)
if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
if err == sql.ErrNoRows {
return alb, fmt.Errorf("albumsById %d: no such album", id)
}
return alb, fmt.Errorf("albumsById %d: %v", id, err)
}
return alb, nil
}
// addAlbum adds the specified album to the database,
// returning the album ID of the new entry
func addAlbum(alb Album) (int64, error) {
result, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)", alb.Title, alb.Artist, alb.Price)
if err != nil {
return 0, fmt.Errorf("addAlbum: %v", err)
}
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("addAlbum: %v", err)
}
return id, nil
}
</code>