суббота, 28 августа 2021 г.

Доступ к реляционной базе данных c Golang

В этом руководстве представлены основы доступа к реляционной базе данных с помощью Go и пакета database/sql в его стандартной библиотеке.

Вы получите максимальную отдачу от этого руководства, если у вас будут базовые знания Go и его инструментов. Если это ваше первое знакомство с Go, смотрите: Начало работы с Go для быстрого ознакомления.

Пакет database/sql, который вы будете использовать, включает типы и функции для подключения к базам данных, выполнения транзакций, отмены выполняемой операции и т. д.

В этом руководстве вы создадите базу данных, а затем напишете код для доступа к базе данных. Ваш примерный проект будет репозиторием данных о винтажных джазовых записях.

В этом руководстве вы пройдете через следующие разделы:

  • Создадите папку для вашего кода.
  • Создадите базу данных.
  • Импортируете драйвер базы данных.
  • Получите дескриптор базы данных и подключитесь к ней.
  • Выполните запрос для нескольких строк.
  • Выполните запрос для одной строки.
  • Добавите данные.

Предпосылки

  • Установка системы управления реляционными базами данных (СУБД) MySQL.
  • Установка Go. Инструкции по установке смотрите в посте Установка Go.
  • Инструмент для редактирования вашего кода. Любой текстовый редактор, который у вас есть, будет работать нормально.
  • Командный терминал. Go хорошо работает с любым терминалом в Linux и Mac, а также с PowerShell или cmd в Windows.

Создайте папку для вашего кода

Для начала создайте папку для кода, который вы будете писать.

1. Откройте командную строку и перейдите в свой домашний каталог.

В Linux или Mac:

$ cd

В Windows:

C:\> cd %HOMEPATH%

В оставшейся части руководства мы будем показывать $ в качестве подсказки. Команды, которые мы используем, будут работать и в Windows.

2. В командной строке создайте каталог для вашего кода с именем data-access.

$ mkdir data-access
$ cd data-access

3. Создайте модуль, в котором вы сможете управлять зависимостями, которые вы добавите в ходе этого руководства.

Запустите команду go mod init, указав ей путь к модулю вашего нового кода.

$ go mod init example.com/data-access
go: creating new go.mod: module example.com/data-access

Эта команда создает файл go.mod, в котором будут перечислены добавленные вами зависимости для отслеживания.

Далее вы создадите базу данных.

Создание базы данных

На этом этапе вы создадите базу данных, с которой будете работать. Вы будете использовать интерфейс командной строки для самой СУБД для создания базы данных и таблицы, а также для добавления данных.

Вы создадите базу данных с данными о винтажных джазовых записях на виниле.

Код здесь использует MySQL CLI, но у большинства СУБД есть собственный CLI с аналогичными функциями.

1. Откройте новую командную строку.

2. В командной строке войдите в свою СУБД, как в следующем примере для MySQL.

$ mysql -u root -p
Enter password:

mysql> 

3. В командной строке mysql создайте базу данных.

mysql> create database recordings;

4. Измените только что созданную базу данных, чтобы можно было добавлять таблицы.

mysql> use recordings;
Database changed

5. В текстовом редакторе в папке data-access (которую мы ранее создали) создайте файл с именем create-tables.sql для хранения сценария SQL для добавления таблиц.

6. В файл вставьте следующий код SQL, затем сохраните файл.

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);

В этом коде SQL вы:

  • Удаляете (drop) таблицу под названием album. Выполнение этой команды вначале упрощает повторный запуск сценария позже, если вы хотите начать работу с таблицей заново.
  • Создаете таблицу album с четырьмя столбцами: id, title, artist, и price. Значение идентификатора каждой строки создается СУБД автоматически.
  • Добавляете три строки со значениями.

7. В командной строке mysql запустите только что созданный сценарий.

Вы будете использовать исходную команду в следующей форме:

mysql> source /путь/к/create-tables.sql

8. В командной строке СУБД используйте оператор SELECT, чтобы убедиться, что вы успешно создали таблицу с данными.

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)

Далее вы напишете код Go для подключения, чтобы можно было делать запросы.

Найдите и импортируйте драйвер базы данных

Теперь, когда у вас есть база данных с некоторыми данными, приступайте к работе над кодом Go.

Найдите и импортируйте драйвер базы данных, который будет переводить запросы, которые вы делаете с помощью функций в пакете database/sql, в запросы, которые понимает база данных.

1. В своем браузере посетите вики-страницу SQLDrivers, чтобы определить драйвер, который вы можете использовать.

Используйте список на странице, чтобы определить драйвер, который вы будете использовать. Для доступа к MySQL в этом руководстве вы будете использовать Go-MySQL-Driver.

2. Обратите внимание на имя пакета для драйвера - здесь github.com/go-sql-driver/mysql.

3. Используя текстовый редактор, создайте файл, в который будет записан код Go, и сохраните файл как main.go в каталоге data-access, который вы создали ранее.

4. В main.go вставьте следующий код, чтобы импортировать пакет драйвера.

package main

import "github.com/go-sql-driver/mysql"

В этом коде вы:

  • Добавляете свой код в main пакет, чтобы вы могли выполнять его независимо.
  • Импортируете драйвер MySQL с github.com/go-sql-driver/mysql.

Импортировав драйвер, вы начнете писать код для доступа к базе данных.

Получите дескриптор базы данных и подключитесь к ней

Теперь напишите код Go, который дает вам доступ к базе данных с помощью дескриптора базы данных.

Вы будете использовать указатель на структуру sql.DB, которая предоставляет доступ к конкретной базе данных.

Напишите код

1. В main.go под только что добавленным кодом импорта вставьте следующий код Go, чтобы создать дескриптор базы данных.

var db *sql.DB

func main() {
    // Получаем свойства соединения.
    cfg := mysql.Config{
        User:   os.Getenv("DBUSER"),
        Passwd: os.Getenv("DBPASS"),
        Net:    "tcp",
        Addr:   "127.0.0.1:3306",
        DBName: "recordings",
    }
    // Получаем дескриптор базы данных.
    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!")
}

В этом коде вы:

  • Объявляете переменную 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 успешно установит соединение.

2. В верхней части файла main.go, сразу под объявлением пакета, импортируйте пакеты, которые вам понадобятся для поддержки только что написанного кода.

Теперь верх файла должен выглядеть так:

package main

import (
    "database/sql"
    "fmt"
    "log"
    "os"

    "github.com/go-sql-driver/mysql"
)

3. Сохраните main.go.

Запустите код

1. Начните отслеживать модуль драйвера MySQL как зависимость.

Используйте go get, чтобы добавить модуль github.com/go-sql-driver/mysql в качестве зависимости для вашего собственного модуля. Используйте аргумент с точкой для обозначения "получить зависимости для кода в текущем каталоге".

$ go get .
go get: added github.com/go-sql-driver/mysql v1.6.0

Go загрузил эту зависимость, потому что вы добавили ее в объявление импорта на предыдущем шаге.

2. В командной строке установите переменные среды DBUSER и DBPASS для использования программой Go.

В Linux или Mac:

$ export DBUSER=username
$ export DBPASS=password

В Windows:

C:\Users\you\data-access> set DBUSER=username
C:\Users\you\data-access> set DBPASS=password

3. В командной строке в каталоге, содержащем main.go, запустите код, набрав go run с аргументом точка, что означает "запустить пакет в текущем каталоге".

$ go run .
Connected!

Вы можете подключиться! Далее вы запросите некоторые данные.

Запрос для нескольких строк

В этом разделе вы будете использовать Go для выполнения SQL-запроса, предназначенного для возврата нескольких строк.

Для операторов SQL, которые могут возвращать несколько строк, вы используете метод Query из пакета database/sql, а затем перебираете строки, которые он возвращает.

Напишите код

1. В main.go сразу над func main вставьте следующее определение структуры Album. Вы будете использовать ее для хранения данных строк, возвращенных из запроса.

type Album struct {
    ID     int64
    Title  string
    Artist string
    Price  float32
}

2. Под func main вставьте следующую функцию albumByArtist для запроса базы данных.

// albumsByArtist запрашивает альбомы с указанным именем исполнителя.
func albumsByArtist(name string) ([]Album, error) {
    // Срез альбомов для хранения данных из возвращенных строк.
    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()
    // Цикл по строкам, используя Scan для назначения данных столбца полям структуры.
    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
}

В этом коде вы:

  • Объявляете срез albums определенного вами типа Album. Он будет содержать данные из возвращенных строк. Имена и типы полей структуры соответствуют именам и типам столбцов базы данных.
  • Используете DB.Query для выполнения оператора SELECT для запроса альбомов с указанным именем исполнителя.
    Первый параметр запроса - это оператор SQL. После параметра вы можете передать ноль или более параметров любого типа. Они предоставляют вам место для указания значений параметров в вашем операторе SQL. Отделяя инструкцию SQL от значений параметров (а не объединяя их, скажем, с fmt.Sprintf), вы разрешаете пакету database/sql отправлять значения отдельно от текста SQL, устраняя любой риск внедрения SQL-кода.
  • Откладываете закрытие rows, чтобы все ресурсы, которые она хранит, были освобождены при выходе из функции.
  • Проходите в цикле по возвращенным строкам, используя Rows.Scan, чтобы назначить значения столбцов каждой строки полям структуры Album.
    Scan принимает список указателей на значения Go, куда будут записаны значения столбцов. Здесь вы передаете указатели на поля в переменной alb, созданные с помощью оператора &. Scan записывает через указатели для обновления полей структуры.
  • Внутри цикла проверяете, нет ли ошибки при сканировании значений столбцов в поля структуры.
  • Внутри цикла добавляете новый альбом к срезу альбомов.
  • После цикла проверяете, нет ли ошибки в общем запросе, используя rows.Err. Обратите внимание, что если сам запрос завершается ошибкой, проверка ошибки здесь - единственный способ узнать, что результаты являются неполными.

3. Обновите свою main функцию, чтобы она вызывала albumByArtist.

В конец func main добавьте следующий код.

albums, err := albumsByArtist("John Coltrane")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Albums found: %v\n", albums)

В новом коде вы:

  • Вызываете добавленную функцию albumByArtist, присвоив ее возвращаемое значение новой переменной albums.
  • Распечатываете результат.

Запустите код

Из командной строки в каталоге, содержащем main.go, запустите код.

$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]

Далее вы запросите одну строку.

Запрос для одной строки

В этом разделе вы будете использовать Go для запроса одной строки из базы данных.

Для операторов SQL, которые, как вы знаете, вернут не более одной строки, вы можете использовать QueryRow, что проще, чем использование цикла Query.

Напишите код

1. Под albumsByArtist вставьте следующую функцию albumByID.

// albumByID запрашивает альбом с указанным идентификатором.
func albumByID(id int64) (Album, error) {
    // Альбом для хранения данных из возвращенной строки.
    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
}

В этом коде вы:

  • Используете DB.QueryRow для выполнения инструкции SELECT для запроса альбома с указанным идентификатором.
    Он возвращает sql.Row. Чтобы упростить вызывающий код, QueryRow не возвращает ошибку. Вместо этого он организует возврат любой ошибки запроса (например, sql.ErrNoRows) из Rows.Scan позже.
  • Используете Row.Scan, чтобы скопировать значения столбцов в поля структуры.
  • Проверяете наличие ошибок при сканировании.
    Специальная ошибка sql.ErrNoRows указывает, что запрос не вернул строк. Обычно эту ошибку следует заменить более конкретным текстом, например "нет такого альбома".

2. Обновите main для вызова albumByID.

В конец func main добавьте следующий код.

// ID 2 зафиксирован здесь для проверки запроса.
alb, err := albumByID(2)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Album found: %v\n", alb)

В этом коде вы:

  • Вызываете добавленную функцию albumByID.
  • Распечатываете возвращенный идентификатор альбома.

Запустите код

Из командной строки в каталоге, содержащем main.go, запустите код.

$ 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}

Далее вы добавите альбом в базу данных.

Добавить данные

В этом разделе вы воспользуетесь Go для выполнения инструкции SQL INSERT для добавления новой строки в базу данных.

Вы видели, как использовать Query и QueryRow с операторами SQL, которые возвращают данные. Чтобы выполнять операторы SQL, которые не возвращают данные, используйте Exec.

Напишите код

1. Под albumByID вставьте следующую функцию addAlbum, чтобы вставить новый альбом в базу данных, затем сохраните файл main.go.

// addAlbum добавляет указанный альбом в базу данных,
// возвращает ID альбома новой записи
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
}

В этом коде вы:

  • Используете DB.Exec для выполнения инструкции INSERT.
    Как и Query, Exec принимает оператор SQL, за которым следуют значения параметров для оператора SQL.
  • Проверяете, нет ли ошибки при попытке INSERT.
  • Получаете идентификатор вставленной строки базы данных с помощью result.LastInsertId.
  • Проверяете, нет ли ошибки при попытке получить идентификатор.

2. Обновите main для вызова новой функции addAlbum.

В конец func main добавьте следующий код.

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)

В новом коде вы теперь:

Вызываете addAlbum с новым альбомом, назначив идентификатор альбома, который вы добавляете, переменной albID.

Запустите код

Из командной строки в каталоге, содержащем main.go, запустите код.

$ 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

Готовый код

В этом разделе содержится код приложения, которое вы создаете с помощью этого руководства.

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() {
    // Получаем свойства соединения.
    cfg := mysql.Config{
        User:   os.Getenv("DBUSER"),
        Passwd: os.Getenv("DBPASS"),
        Net:    "tcp",
        Addr:   "127.0.0.1:3306",
        DBName: "recordings",
    }
    // Получаем дескриптор базы данных.
    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)

    // ID 2 зафиксирован здесь для проверки запроса.
    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 запрашивает альбомы с указанным именем исполнителя.
func albumsByArtist(name string) ([]Album, error) {
    // Срез альбомов для хранения данных из возвращенных строк.
    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()
    // Цикл по строкам, используя Scan для назначения данных столбца полям структуры.
    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 запрашивает альбом с указанным идентификатором.
func albumByID(id int64) (Album, error) {
    // Альбом для хранения данных из возвращенной строки.
    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 добавляет указанный альбом в базу данных,
// возвращает ID альбома новой записи
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
}


Читайте также:


Купить gopher

Комментариев нет:

Отправить комментарий