
Недавно мы столкнулись с необходимостью найти библиотеку для удобной работы с базами данных. В нашем проекте команда решила не использовать 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 здесь.
