Недавно мы столкнулись с необходимостью найти библиотеку, упрощающую работу с базами данных. В нашем проекте команда решила не использовать ORM (объектно-реляционное сопоставление), а использовать миграции. Поскольку я, как и автор статьи, работал только с ORM, я мало что знал о концепции миграции баз данных. В поисках информации о популярных миграциях и решениях я наткнулся на эту статью. Перевод статьи я оставил ниже. Возможно, это будет вам полезно. Буду признателен, если вы поделитесь библиотеками, которые используете.
Contents
Использование миграции базы данных в 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 никогда не разочаровывает. Есть библиотека под названием голанг-мигрировать, который можно использовать для миграции SQL. Это очень удобная библиотека, поддерживающая большинство база данных.
Библиотека (которую также можно использовать с помощью инструмента командной строки) позволяет нам выполнять миграцию из различных источников данных: Список файлов SQL. Файлы, хранящиеся в Google Cloud Storage или AWS Cloud. Файлы в GitHub или GitLab. В нашем случае мы загрузим миграции из папки, специфичной для нашего проекта, которая будет содержать файлы миграции SQL. Теперь важная часть. Я уже упоминал, что порядок важен для обеспечения «правильного» выполнения миграции. Ну, с этим покончено шаблон именования. Это описано довольно подробно, поэтому я просто дам вам краткий обзор.
Файлы имеют следующий шаблон именования:
{version}_{title}.up.{extension}
«версия» указывает порядок, в котором будет применяться миграция. Например, если у нас есть:
1_innit_database.up.sql
2_alter_database.up.sql
тогда сначала будет применен `1_innit_database.up.sql`. «Название» предназначено только для удобства чтения и описания и не служит никакой дополнительной цели.
Теперь относительно Вверх вниз. Метод вверх используется для добавления в базу данных новых таблиц, столбцов или индексов, а метод вниз должен отменить операции, выполненные методом вверх.
Теперь мы знаем, как писать файлы миграции. Давайте посмотрим, как мы можем их применить. Я написал небольшую структуру 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 см. Здесь). При этом мы создаем исходный драйвер, содержащий загруженные файлы миграции.
Метод Применить миграцию Выполняет процесс миграции на предоставленном экземпляре базы данных. Мы используем драйвер исходного файла, указанный в платформе Migrator, и создаем экземпляр миграции, используя библиотеку и указывая экземпляр базы данных. После этого мы просто вызываем функцию Вверх (Или Вниз), и применяются миграции.
Я также написал небольшой файл 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!!")
}
Это прочитает все файлы миграции в папке миграции и создаст программу миграции с ее содержимым. Затем мы создаем экземпляр базы данных в нашей локальной базе данных и применяем к нему миграцию.
выводы
Мне очень нравится управлять базами данных. Я не знал о миграции. Это была интересная тема для написания.
Я считаю, что миграция баз данных — очень полезный инструмент не только для тестирования, но и для лучшего контроля и управления версиями ваших баз данных. Конечно, это не идеально, поскольку небольшая ошибка в определении миграции может вызвать проблемы для вашего сервиса (и других сервисов, если ваша база данных и таблицы являются общими).
Кроме того, меня очень впечатлила библиотека. иди мигрируй. На странице Github есть очень подробные объяснения использования, распространенные ошибки, часто задаваемые вопросы и многое другое. Он очень полный и делает его практически простым в использовании. Очень рекомендую посмотреть!!
Как всегда, вы можете найти полный проект, описанный в этой статье, в моей учетной записи GitHub. Здесь.