Источник: Nuances of Programming
Недавно я присоединился к новой команде и был поражен созданной у них инфраструктурой тестирования для успешной работы приложений. Для меня это было большой переменой: я не привык к такой манере «тестирования».
С тестированием уровня данных связаны миграции БД. С базами данных я работаю на протяжении всей своей карьеры инженера-разработчика и все же задался вопросом: «Что это за миграции БД?»
Рассмотрим применение миграций БД в службах, написанных на Golang.
Что такое «миграции БД»?
Вот определение из prisma.io:
Миграции БД, известные как миграции схем, миграции схем баз данных или просто миграции, — это контролируемые наборы изменений, разработанные для модификации структуры объектов в реляционной базе данных. Миграции способствуют переводу схем БД из текущего их состояния в новое, желаемое — с добавлением таблиц и столбцов, удалением элементов, разделением полей или изменением типов и ограничений.
Будем называть миграции БД SQL-миграциями, делая акцент на базах данных вроде PostgreSQL или MySQL, хотя такие миграции применяются ко многим другим БД.
Преимущество миграций БД заключается в упрощении эволюции баз данных по мере изменения требований к приложениям и службам. Кроме того, имея различные миграции для каждого изменения, легче отслеживать и регистрировать выполненные в БД изменения, связывать их с требуемым изменением службы.
Но имеются и недостатки. Новая миграция добавляется очень осторожно, чтобы случайным удалением столбца, изменением его названия, удалением используемой таблицы и т. д. не создать несовместимость между новой версией БД и самой службой. Кроме того, при добавлении миграций существует возможность потери данных. Например, когда из таблицы удаляется столбец с нужной информацией.
Как пишутся 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 SQL-миграции выполняются с библиотекой golang-migrate для самых разных БД.
Этой библиотекой — с собственным инструментом CLI — миграции запускаются из различных источников данных:
- Список .sql-файлов.
- Файлы из Google Storage или AWS Cloud.
- Файлы из Github или Gitlab.
Мы загрузим миграции из конкретной папки проекта с .sql-файлами миграции. А теперь важная часть: порядок для корректного выполнения миграций, обеспечиваемый в шаблоне именования файла. Подробное объяснение этого шаблона содержится у владельцев пакета здесь, поэтому приведем лишь краткую версию.
У файлов имеется такой шаблон именования:
{version}_{title}.up.{extension}
Версией version указывается порядок, в котором применится миграция. Например, если у нас:
1_innit_database.up.sql
2_alter_database.up.sql
то сначала применится миграция из первой строки. Заголовок title нужен для удобства и описания, без дополнительных целей.
Методом up в базу данных добавляются новые таблицы, столбцы или индексы, методом down операции, выполняемые методом up, отменяются.
Посмотрим теперь, как файлы миграции применяются. Вот небольшая структура Migrator на Go с таким определением:
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"
)
type Migrator struct {
srcDriver source.Driver
}
func MustGetNewMigrator(sqlFiles embed.FS, dirName string) *Migrator {
d, err := iofs.New(sqlFiles, dirName)
if err != nil {
panic(err)
}
return &Migrator{
srcDriver: d,
}
}
func (m *Migrator) ApplyMigrations(db *sql.DB) error {
driver, err := postgres.WithInstance(db, &postgres.Config{})
if err != nil {
return fmt.Errorf("unable to create db instance: %v", err)
}
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, и создаем экземпляр migrate с помощью библиотеки экземпляра БД. Затем просто запускаем функцию Up или Down, и миграции применяются.
Написали также небольшой main.go, где создаем экземпляр Migrator и применяем его к локальному экземпляру БД в Docker:
package main
import (
"database/sql"
"embed"
"fmt"
"psql_migrations/internal/migrator"
)
const migrationsDir = "migrations"
//go:встроенные миграции/*.sql
var MigrationsFS embed.FS
func main() {
// --- (1) ----
// Восстанавливаем «Migrator»
migrator := migrator.MustGetNewMigrator(MigrationsFS, migrationsDir)
// --- (2) ----
// Получаем экземпляр БД
connectionStr := "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
conn, err := sql.Open("postgres", connectionStr)
if err != nil {
panic(err)
}
defer conn.Close()
// --- (2) ----
// Применяем миграции
err = migrator.ApplyMigrations(conn)
if err != nil {
panic(err)
}
fmt.Printf("Migrations applied!!")
}
Так считаются все файлы миграции в папке migrations и создастся migrator с ее содержимым. Затем для локальной базы данных создаем экземпляр БД и применяем к нему миграции.
Заключение
Это была действительно интересная тема для статьи. Мне нравится управление базами данных, но об их миграциях я и не подозревал.
Думаю, миграции БД — очень полезный инструмент не только для тестирования, но и для контроля и управления версиями баз данных. Конечно, он не лишен недостатков, ведь небольшая ошибка в определении миграции чревата проблемами для служб, в которых применяются БД и таблицы.
Также впечатлила библиотека go-migrate. На ее странице Github приведены подробнейшие объяснения по применению, типичные ошибки, часто задаваемые вопросы и т. д. Поэтому ее очень просто использовать, рекомендуем заглянуть.
Весь проект выложен здесь.
Читайте также:
Перевод статьи Simeon Grancharov: Using Database Migrations with Golang