Использование миграций баз данных в Go
Недавно мы столкнулись с необходимостью найти библиотеку для удобной работы с базами данных. В нашем проекте команда решила не использовать ORM (Object-Relational Mapping), а вместо этого применить миграции. Так как я работал только с ORM, мне, как и автору статьи, было мало знакомо понятие миграций баз данных. В поисках информации о миграциях и популярных решениях, я наткнулся на эту статью. Перевод статьи я оставил ниже. Возможно, она будет вам полезна. Буду признателен, если вы сможете поделиться библиотеками, которые используете.
Использование миграций баз данных в Go
Недавно я приступил к новой работе, и я был поражен инфраструктурой тестирования, которую создала команда. Для меня такой «тестовый» подход был в новинку.
Одна из тем, которую мы затронули во время обсуждения тестирования слоя базы данных, была посвящена миграциям баз данных. Я использовал базы данных на протяжении всей моей карьеры разработчика, и все же мне пришлось задать вопрос: "Что такое миграции баз данных?".
В этой статье я расскажу о том, как можно использовать миграции базы данных в ваших сервисах, написанных на Golang.
Что такое Миграция БД?
Согласно определению:
Миграции баз данных, также известные как схемные миграции или миграции схемы базы данных, представляют собой контролируемые наборы изменений, которые позволяют модифицировать структуру объектов в реляционной базе данных. Они помогают перевести схемы баз данных из текущего состояния в новое желаемое состояние, включая добавление таблиц и столбцов, удаление элементов, изменение типов и ограничений, а также разделение полей.
В этой статье я иногда называю миграции баз данных SQL-миграциями, потому что я сосредоточусь на SQL-базах данных, таких как PostgreSQL или MySQL, но, как упомянуто в определении, это применимо к многим различным базам данных.
Использование миграций баз данных имеет преимущество в том, что оно упрощает развитие БД по мере изменения требований к приложению и сервису. Кроме того, за счет различных миграций на каждое изменение легче отслеживать, регистрировать и связывать их с требуемым изменением сервиса.
Однако, этот процесс не лишен недостатков. При добавлении новой миграции, необходимо быть внимательным, чтобы не создать несовместимости между новой версией БД и самим сервисом. Например, случайно удалить столбец, изменить его имя, удалить используемую таблицу и так далее. Кроме того, существует риск потери данных при добавлении миграций. Например, если удалить столбец из таблицы, содержащей важную информацию, то необходимо убедиться, что эта информация не понадобится в будущем.
Как пишутся SQL-миграции?
Нет ничего сложного в написании SQL миграций. Они представляют собой просто SQL-запросы, которые выполняются в определенном порядке. Например, SQL-миграция может выглядеть так:
CREATE TABLE books (
id UUID,
name character varying (255),
description text
);
ALTER TABLE books ADD PRIMARY KEY (id);
Предположим, вы применили эту миграцию, развернули свой сервис и обнаружили, что забыли добавить индекс, который хотели добавить. В таком случае, вы можете просто написать еще один SQL-запрос в рамках другой миграции, например:
CREATE INDEX idx_book_name
ON books(name);
Теперь, когда мы разобрались в том, как работают миграции, важно понять, насколько критична очередность их выполнения. Вы не сможете запустить вторую миграцию, так как ссылочная таблица еще не создана. Это мы рассмотрим в следующем разделе.
Как использовать SQL-миграции в GO?
К счастью Go никогда не разочаровывает. Существует библиотека под названием golang-migrate, которая может использоваться для выполнения SQL-миграций. Это очень удобная библиотека, которая поддерживает большинство баз данных.
Библиотека (ее также можно использовать с помощью инструмента командной строки) позволяет нам выполнять миграции из различных источников данных: Список файлов SQL, хранящиеся в Google Cloud Storage или AWS Cloud, файлы в GitHub или GitLab. В нашем случае мы будем загружать миграции из определенной папки в нашем проекте, которая будет содержать файлы SQL-миграций. Теперь важная часть. Я уже упоминал, что порядок важен для того, чтобы гарантировать, что миграции будут выполнены "правильно". Ну, это делается с помощью шаблона именования. Он достаточно подробно описан, поэтому я просто дам вам краткий обзор.
Файлы имеют следующий шаблон именования:
{version}_{title}.up.{extension}
“version” указывает порядок, в котором будет применяться миграция. Например, если у нас есть:
1_innit_database.up.sql
2_alter_database.up.sql
тогда сначала будет применен `1_innit_database.up.sql`. "title” предназначен только для удобства чтения и описания и не служит никакой дополнительной цели.
Теперь относительно up / down. Метод up используется для добавления новых таблиц, столбцов или индексов в БД, в то время как метод down должен отменять операции, выполняемые методом up.
Теперь, когда мы знаем, как записывать файлы миграции, давайте посмотрим, как мы можем их применить. Я написал небольшую структуру Migrator:
package migrator
import (
"database/sql"
"embed"
"errors"
"fmt"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source"
"github.com/golang-migrate/migrate/v4/source/iofs"
)
// Migrator структура для применения миграций.
type Migrator struct {
srcDriver source.Driver // Драйвер источника миграций.
}
// MustGetNewMigrator создает новый экземпляр Migrator с встроенными SQL-файлами миграций.
// В случае ошибки вызывает panic.
func MustGetNewMigrator(sqlFiles embed.FS, dirName string) *Migrator {
// Создаем новый драйвер источника миграций с встроенными SQL-файлами.
d, err := iofs.New(sqlFiles, dirName)
if err != nil {
panic(err)
}
return &Migrator{
srcDriver: d,
}
}
// ApplyMigrations применяет миграции к базе данных.
func (m *Migrator) ApplyMigrations(db *sql.DB) error {
// Создаем экземпляр драйвера базы данных для PostgreSQL.
driver, err := postgres.WithInstance(db, &postgres.Config{})
if err != nil {
return fmt.Errorf("unable to create db instance: %v", err)
}
// Создаем новый экземпляр мигратора с использованием драйвера источника и драйвера базы данных PostgreSQL.
migrator, err := migrate.NewWithInstance("migration_embeded_sql_files", m.srcDriver, "psql_db", driver)
if err != nil {
return fmt.Errorf("unable to create migration: %v", err)
}
// Закрываем мигратор в конце работы функции.
defer func() {
migrator.Close()
}()
// Применяем миграции.
if err = migrator.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return fmt.Errorf("unable to apply migrations %v", err)
}
return nil
}
Когда мы создаем Migrator, мы передаем путь, по которому находятся все файлы миграции. Мы также предоставляем встроенную файловую систему (дополнительную информацию о внедрении Go смотрите здесь). С помощью этого мы создаем исходный драйвер, который содержит загруженные файлы миграции.
Метод ApplyMigrations выполняет процесс миграции для предоставленного экземпляра базы данных. Мы используем исходный драйвер файлов, указанный в структуре Migrator, и создаем экземпляр миграции, используя библиотеку и указывая экземпляр базы данных. После этого мы просто вызываем функцию Up (или Down), и миграции применяются.
Я также написал небольшой файл main.go
, в котором создаю экземпляр Migrator и применяю его к локальному экземпляру базы данных в Docker.
package main
import (
"database/sql"
"embed"
"fmt"
"psql_migrations/internal/migrator"
)
const migrationsDir = "migrations"
//go:embed migrations/*.sql
var MigrationsFS embed.FS
func main() {
// --- (1) ----
// Recover Migrator
migrator := migrator.MustGetNewMigrator(MigrationsFS, migrationsDir)
// --- (2) ----
// Get the DB instance
connectionStr := "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
conn, err := sql.Open("postgres", connectionStr)
if err != nil {
panic(err)
}
defer conn.Close()
// --- (2) ----
// Apply migrations
err = migrator.ApplyMigrations(conn)
if err != nil {
panic(err)
}
fmt.Printf("Migrations applied!!")
}
Это позволит прочитать все файлы миграции внутри папки migrations и создать migrator с ее содержимым. Затем мы создаем экземпляр БД в нашей локальной базе данных и применяем к нему миграции.
Выводы
Мне очень нравится управлять базами данных. Я не знал о миграции. Это была интересная тема для написания.
Я думаю, что миграция баз данных - очень полезный инструмент не только для тестирования, но и для лучшего контроля и управления версиями ваших баз данных. Конечно, это не идеальный вариант, поскольку небольшая ошибка в определении миграции может вызвать проблемы для вашего сервиса (и других сервисов, если ваша база данных и таблицы являются общими).
Кроме того, я был очень впечатлен библиотекой go-migrate. На ее странице на Github есть очень подробные объяснения по использованию, типичные ошибки, часто задаваемые вопросы и т.д.. Эта библиотека очень мощная и проста в использовании, что делает ее отличным выбором для работы с миграциями. Я настоятельно рекомендую ее попробовать
Как всегда, вы можете найти полный проект, описанный в этой статье, в моем аккаунте на GitHub здесь.