[Перевод] Использование миграций баз данных в Go

c4af6234c0753b53c1edee12109b1544.png

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

Habrahabr.ru прочитано 6720 раз