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

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

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

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

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

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

  1. Создайте папку для вашего кода.
  2. Настройте базу данных.
  3. Импортируйте драйвер базы данных.
  4. Получите дескриптор базы данных и подключитесь.
  5. Выполните запрос для получения нескольких строк.
  6. Выполните запрос для получения одной строки.
  7. Добавьте данные.

Примечание: Для других обучающих материалов см. Обучающие материалы.

Предварительные требования

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

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

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

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

    В Linux или Mac:

    <code>$ cd
    </code>

    В Windows:

    <code>C:\> cd %HOMEPATH%
    </code>

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

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

    <code>$ mkdir data-access
    $ cd data-access
    </code>
  3. Создайте модуль, в котором вы сможете управлять зависимостями, которые добавите во время этого обучающего материала.

    Запустите команду 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, но большинство СУБД имеют собственную командную строку с аналогичными возможностями.

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

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

    <code>$ mysql -u root -p
    Enter password:
    mysql>
    </code>
  3. В командной строке mysql создайте базу данных.

    <code>mysql> create database recordings;
    </code>
  4. Переключитесь на только что созданную базу данных, чтобы можно было добавлять таблицы.

    <code>mysql> use recordings;
    Database changed
    </code>
  5. В вашем текстовом редакторе, в папке data-access, создайте файл с именем create-tables.sql для хранения SQL-скрипта добавления таблиц.

  6. Вставьте следующий 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 каждой строки создаётся автоматически СУБД.

    • Добавляете четыре строки с данными.

  7. Из командной строки mysql запустите созданный вами скрипт.

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

    <code>mysql> source /path/to/create-tables.sql
    </code>
  8. В командной строке вашей СУБД используйте инструкцию 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, в запросы, понятные базе данных.

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

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

  2. Заметьте название пакета для драйвера – в данном случае github.com/go-sql-driver/mysql.

  3. С помощью вашего текстового редактора создайте файл для записи кода на Go и сохраните его как main.go в каталоге data-access, который вы создали ранее.

  4. В файл main.go вставьте следующий код для импорта пакета драйвера.

    <code>package main
    import "github.com/go-sql-driver/mysql"
    </code>

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

    • Добавляете свой код в пакет main, чтобы его можно было запускать независимо.

    • Импортируете драйвер MySQL github.com/go-sql-driver/mysql.

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

Получение дескриптора базы данных и установление соединения

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

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

Написание кода

  1. В файл 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 успешно установил соединение.

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

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

    <code>package main
    import (
      "database/sql"
      "fmt"
      "log"
      "os"
      "github.com/go-sql-driver/mysql"
    )
    </code>
  3. Сохраните main.go.

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

  1. Начните отслеживать модуль драйвера 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 на предыдущем шаге. Для получения дополнительной информации об отслеживании зависимостей, см. Добавление зависимости.

  2. В командной строке установите переменные среды 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>
  3. В командной строке в каталоге, содержащем main.go, запустите код, введя go run с аргументом точки, чтобы обозначить «запустить пакет в текущем каталоге».

    <code>$ go run .
    Connected!
    </code>

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

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

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

Для SQL-инструкций, которые могут возвращать несколько строк, вы используете метод Query из пакета database/sql, а затем проходите по строкам, которые он возвращает. (Вы узнаете, как выполнять запросы для одной строки позже, в разделе Запрос одной строки.)

Напишите код

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

    <code>type Album struct {
      ID     int64
      Title  string
      Artist string
      Price  float32
    }
    </code>
  2. Под 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. Обратите внимание, что если сам запрос завершится ошибкой, проверка на ошибку здесь — единственная возможность узнать, что результаты неполны.

  3. Обновите функцию 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.

Напишите код

  1. Под функцией 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 указывает, что запрос не вернул ни одной строки. Обычно такая ошибка подлежит замене более конкретным текстом, например, здесь — «нет такого альбома».

  2. Обновите 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.

Напишите код

  1. Под функцией 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.

  2. Обновите 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>
GoRu.dev Golang на русском

На сайте представлена адаптированная под русский язык документация языка программирования Golang