Любой более или менее серьезный продакшен, работающий с базой данных, подразумевает процесс миграции - обновление структуры базы данных от одной версии до другой (обычно более новой) [источник].

Миграции в БД можно делать вручную или использовать для этого специальные утилиты (фреймворки). В данной статье речь идет об утилите goose. Это инструмент миграции схемы, который обеспечивает управление миграциями схемы в проекте. Начиная с версии v3.16.0 goose поддерживает YDB - распределенную open-source СУБД. В данной статье мы будем разбирать кейс применения миграций конкретно в YDB.

Что такое "Миграция схемы"?

Под миграциями понимают несколько разных понятий:

  1. Миграция - как процесс перемещения схемы данных и самих данных между различными базами данных. Например, из PostgreSQL в Oracle.

  2. Миграция - как процесс бесшовного перемещения схемы данных и самих данных между однородными базами данных. Например, миграция PostgreSQL с мастер-сервера А на сервер-фолловер Б. Этот процесс еще называют репликацией.

  3. Миграция - как процесс изменения версий схемы данных и самих данных в рамках одной базы данных. Например, миграция версии схемы данных в PostgreSQL с таблицей без вторичных индексов к версии с вторичными индексами.

В данной статье мы будем рассматривать миграции как процесс управления версиями схемы данных в рамках одной базы данных.

Современные приложения обычно не являются чем-то однократно написанным. В процессе эксплуатации, а также по мере развития бизнеса так или иначе приложение претерпевает изменения. И изменения касаются не только логики работы самого приложения, но и в том числе изменения схемы данных. Изменение схемы специально для онлайн-базы данных называется "миграция схемы".

Пока проект еще не вырос из прототипа, можно экспериментировать, каждый раз инициализируя схему данных при старте приложения. Однако как только в базе данных находятся реальные данные уже нельзя удалить старую схему данных и накатить новую. Необходимо задумываться о миграции старой схемы в новую. Особенно, если приложение реализует непрерывную работу. Нужно понять и разделить этапы миграции схемы данных в БД и релизы приложения, закладывающиеся на старую или новую схему данных. Сам процесс разделения релизов для обеспечения непрерывной работоспособности приложения остается за скобками в рамках данной статьи.

Практическая задача

Представьте, что мы разрабатываем сервис - админку пользователей. Обычное CRUD-приложение. По задумке этот сервис будет использоваться для проверки и предоставления прав доступа к другим сервисам в рамках планируемой системы.

Пусть развитие нашего сервиса протекает следующим образом:

  1. На этапе проектирования приложения мы решили сразу завести таблицу пользователей с полями: id (идентификатор записи), username (имя пользователя) и created_at (дату создания учетной записи). Естественно, часть приложения, отвечающего за добавление пользователей, в самом начале отсутствует. Чтобы развязать работу различных команд в части разработки функционала добавления пользователей, их листинга, редактирования и удаления, мы решили сразу же добавить четырех тестовых пользователей (Bob Smith, Dow Jones, John Dow, Elon Mask).

  2. На следующем спринте мы подошли к понятию ролей. Мы добавляем таблицу ролей с полями: id (идентификатор записи) и name (название роли). Сразу же добавили роли adminviewerguest. Нам потребовалось добавить в таблицу пользователей колонку с идентификатором роли role_id. Тестовых пользователей мы готовы сразу разметить ролями. Остальным установим роль guest и сделаем интерфейс админки, чтобы сменить роль.

  3. На этом этапе мы осознали большой просчет - учётки пользователей никак не защищены. Мы захотели добавить пароль (хэш), чтобы аутентифицировать пользователя. Поэтому добавляем колонку с password_hash в таблицу пользователей. А в нашем сервисе делаем везде middleware, чтобы автоматически блокировать запросы неавторизованных пользователей.

  4. У нас набралось уже немало пользователей и мы увидели, что в топе запросов оказались запросы с инструкцией WHERE username=… AND password_hash=… Проанализировав запросы, мы поняли, что не хватает индекса на поля password_hash. В этой версии мы решили добавить индекс на это поле.

  5. и так далее (все как в жизни)

Надеюсь, этот пример красноречиво описывает как меняется схема данных в процессе развития приложения. Попробуем эти изменения провести через соответствующий инструмент миграции.

Что такое "goose"?

Goose - это один из инструментов миграции схемы, небольшой, но очень простой. Он написан на Go, и вы можете установить его с помощью "go get" или с помощью готового двоичного файла.

$ go install github.com/pressly/goose/v3/cmd/goose@latest

Существует два варианта goose:

В статье мы будем рассматривать форк github.com/pressly/goose, т.к. его развитие шагнуло далеко вперед по сравнению с апстримом. Также следует отметить, что поддержка YDB в goose реализована именно в этом репозитории и стала общедоступной, начиная с версии v3.16.0.

Предупреждение об опасности гонки при выполнении миграций

Следует опасаться гонки между миграциями, запускаемыми из разных процессов (на той же самой машине или на различных машиных). Когда говорят о распределенной базе данных - имеют в виду то, что с этой базой данных работает множество приложений (например горизонтально-масштабированных). Представьте, что вы стартуете свой веб-сервер на 500 машинах. Каждый из этих процессов на старте применяет миграции. Что будет, если 500 клиентов базы данных одновременно захотят выполнить CREATE TABLE или ALTER TABLE? Поведение базы данных может быть непредсказуемо или 499 процессов вашего веб-сервера завершатся ошибкой, т.к. только одно из них “выиграет” в этой конкурентной гонке. На практике гонку можно исключить двумя способами:

  • явным разделением этапа миграции данных и работы приложения (например, с помощью {Dev,DB}OPS - однократной процедуры приведения окружения приложения в желаемое состояние).

  • использованием механизмов leader election с помощью распределенного семафора. В YDB есть специальный сервис координации (coordination service), позволяющий делать leader election в задаче привилегированной блокировки некоторого ресурса для применения миграций данных. В бинарной утилите goose при работе с YDB этот механизм не используется.

Задача исключения гонки между миграциями данных в данной статье не рассматривается.

Подготовка

Прежде всего, запустим YDB в докер-контейнере командой (см. документацию YDB):

$ docker run -d --rm --name ydb-local -h localhost 
-p 2135:2135 -p 2136:2136 -p 8765:8765 
-v pwd)/ydb_certs:/ydb_certs -v $(pwd)/ydb_data:/ydb_data \
  -e GRPC_TLS_PORT=2135 -e GRPC_PORT=2136 -e MON_PORT=8765 \
  ghcr.io/ydb-platform/local-ydb:23.3

Следует отметить, что goose корректно работает с версиями YDB не ниже 23-3.

Параметры строки подключения

Для подключения к YDB из goose следует использовать строку подключения вида grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric.

Разберем элементы строки подключения:

  • grpc - протокол подключения. В данном случае это insecure подключение по gRPC. Для защищенного подключения (с TLS) следует использовать протокол grpcs. При этом следует явно подключить сертификаты из директории $(pwd)/ydb_certs,  например так: export YDB_SSL_ROOT_CERTIFICATES_FILE=$(pwd)/ydb_certs.

  • localhost - адрес подключения к YDB.

  • 2136 - стандартный порт YDB для обработки запросов по insecure протоколу. Для защищенного подключения (если не указано иное в конфигурации YDB) следует указывать порт 2135. При этом следует явно подключить сертификаты из директории $(pwd)/ydb_certs,  например так: export YDB_SSL_ROOT_CERTIFICATES_FILE=$(pwd)/ydb_certs.

  • local - имя базы данных внутри кластера YDB.

  • go_query_mode=scripting - специальный режим scripting выполнения запросов по умолчанию в драйвере YDB. В этом режиме все запросы от goose направляются в YDB сервис scripting, который позволяет обрабатывать как DDL, так и DML инструкции SQL.

  • go_fake_tx=scripting - поддержка эмуляции транзакций в режиме выполнения запросов через сервис YDB scripting. Дело в том, что в YDB выполнение DDL инструкций SQL в транзакции невозможно (или несет значительные накладные расходы). В частности сервис scripting не позволяет делать интерактивные транзакции (с явными Begin+Commit/Rollback). Соответственно, режим эмуляции транзакций на деле не делает ничего (nop) на вызовах Begin+Commit/Rollback из goose. Этот трюк в редких случаях может привести к тому, что отдельный шаг миграции может оказаться в промежуточном состоянии. Команда YDB работает на новым сервисом query, который должен помочь убрать этот риск.

  • go_query_bind=declare,numeric - поддержка биндингов авто-выведения типов YQL из параметров запросов (declare) и поддержка биндингов нумерованных параметров (numeric). Дело в том, что YQL - язык со строгой типизацией, требующий явным образом указывать типы параметров запросов в теле самого SQL-запроса с помощью специальной инструкции DECLARE. Также YQL поддерживает только именованные параметры запроса (например, $my_arg), в то время как ядро goose генерирует SQL-запросы с нумерованными параметрами ($1, $2 и т.д.). Биндинги declare и numeric модифицируют исходные запросы из goose на уровне драйвера YDB, что позволило в конечном счете встроиться в goose. Подробнее о биндингах написано в статье database/sql биндинги для YDB в Go.

Авторизация

По умолчанию goose подключается к YDB, используя anonymous credentials. Через строку подключения можно изменить поведение по умолчанию:

  • для подключения к YDB с помощью static credentials нужно указать в строке подключения учетные данные (login и password): grpc://login:password@localhost:2136/local?... .

  • для подключения к YDB с помощью токена нужно использовать дополнительный параметр: grpc://localhost:2136/local?token=<ACCESS_TOKEN>&... .

Иные способы авторизации в YDB из goose на данный момент не поддерживаются.

Директория для файлов миграций

Создадим директорию migrations и далее все команды мы будем выполнять в этой директории:

$ mkdir migrations && cd migrations

Демонстрация работы goose с SQL-файлами миграцийДемонстрация работы goose с SQL-файлами миграций

Первый шаг миграции

Создать первый файл миграции. Его можно сгенерировать с помощью команды goose create:

$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" create 00001_create_first_table sql
2023/12/15 05:22:48 Created new file: 20231215052248_00001_create_table_users.sql

Это означает, что инструмент создал новый файл миграции <timestamp>_00001_create_table_users.sql, где мы можем записать шаги по изменению схемы для базы данных YDB, которая доступна через соответствующую строку подключения.

Итак, после выполнения команды goose create будет создан файл миграции <timestamp>_00001_create_table_users.sql со следующим содержимым:

-- +goose Up
-- +goose StatementBegin
SELECT 'up SQL query';
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd

Такая структура файла миграции помогает держать в контексте внимания инструкции, приводящие к следующей версии базы данных. Также легко, не отвлекаясь на лишнее, можно написать инструкции, откатывающие изменение базы данных.

Файл миграции состоит из двух разделов. Первый - +goose Up, область, в которой мы можем записать шаги миграции. Вторая - +goose Down, область, в которой мы можем написать шаг для инвертирования изменений для шагов +goose Up. Goose заботливо вставил запросы-плейсхолдеры:

SELECT 'up SQL query';

и

SELECT 'down SQL query';

чтобы мы могли вместо них вписать по сути сами запросы миграции. Отредактируем файл миграции <timestamp>_00001_create_table_users.sql так, чтобы при применении миграции мы создавали таблицу нужной нам структуры, а при откате миграции - мы удаляли созданную таблицу:

-- +goose Up
-- +goose StatementBegin
CREATE TABLE users (
id Uint64,
username Text,
created_at TzDatetime,
PRIMARY KEY (id)
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE users;
-- +goose StatementEnd

Синтаксис CREATE TABLE в YQL описан в соответствующем разделе документации.

Добавим также шаги миграции по начальному наполнению таблицы users данными:

$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" create 00002_upsert_test_users sql
2023/12/15 05:24:32 Created new file: 20231215052432_00002_upsert_test_users.sql

Содержание файла <timestamp>_00002_upsert_test_users.sql отредактируем до вида:

-- +goose Up
-- +goose StatementBegin
UPSERT INTO users (id, username, created_at) VALUES
(1, 'Bob Smith', CAST('2023-12-01T15:14:13Z' AS Datetime)),
(2, 'Dow Jones', CAST('2023-12-02T12:11:10Z' AS Datetime)),
(3, 'Elon Mask', CAST('2023-12-03T09:08:07Z' AS Datetime)),
(4, 'John Dow', CAST('2023-12-04T06:05:04Z' AS Datetime));
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DELETE FROM users WHERE id in (1, 2, 3, 4);
-- +goose StatementEnd

Мы намеренно разделили этап инициализации базы данных на 2 миграции: DDL и DML. Дело в том, что на данный момент YDB не поддерживает смешанные типы SQL-запросов в рамках одной транзакции. Поэтому мы разделили миграции по типам - сначала создание таблицы users (DDL), затем вставка тестовых пользователей (DML).

Остальные шаги миграций

Но мы прошли только первый этап описанной выше практической задачи. По аналогии добавим файлы миграций для:

добавления таблицы ролей

Cоздаем файл миграции:

$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" create 00003_create_table_roles sql
2023/12/15 05:38:47 Created new file: 20231215053847_00003_create_table_roles.sql

Отредактируем содержимое до следующего вида:

-- +goose Up
-- +goose StatementBegin
CREATE TABLE roles (
id Uint64,
name Text,
PRIMARY KEY (id)
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE roles;
-- +goose StatementEnd

начального наполнения таблицы ролей

Cоздаем файл миграции:

$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" create 00004_upsert_initial_roles sql
2023/12/15 05:38:54 Created new file: 20231215053854_00004_upsert_initial_roles.sql

Отредактируем содержимое до следующего вида:

-- +goose Up
-- +goose StatementBegin
UPSERT INTO roles (id, name) VALUES
(1, 'admin'),
(2, 'viewer'),
(3, 'guest');
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
DELETE FROM roles WHERE id in (1, 2, 3);
-- +goose StatementEnd

добавления колонки role_id в таблице пользователей

Cоздаем файл миграции:

$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" create 00005_add_column_role_id_into_table_users sql
2023/12/15 05:39:28 Created new file: 20231215053928_00005_add_column_role_id_into_table_users.sql

Отредактируем содержимое до следующего вида:

-- +goose Up
-- +goose StatementBegin
ALTER TABLE users ADD COLUMN role_id Uint64;
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
ALTER TABLE users DROP COLUMN role_id;
-- +goose StatementEnd

обновления ролей известных записей пользователей

Cоздаем файл миграции:

$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" create 00006_update_role_id_of_test_users sql
2023/12/15 05:40:03 Created new file: 20231215054003_00006_update_role_id_of_test_users.sql

Отредактируем содержимое до следующего вида:

-- +goose Up
-- +goose StatementBegin
UPDATE users SET role_id=1 WHERE id=1;
UPDATE users SET role_id=2 WHERE id=2;
UPDATE users SET role_id=3 WHERE id=3;
UPDATE users SET role_id=1 WHERE id=1;
UPDATE users SET role_id=3 WHERE id NOT IN (1, 2, 3, 4);
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
UPDATE users SET role_id=NULL;
-- +goose StatementEnd

добавления поля password_hash в таблицу пользователей

Cоздаем файл миграции:

$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" create 00007_add_column_password_hash_into_table_users sql
2023/12/15 05:38:54 Created new file: 20231215054033_00007_add_column_password_hash_into_table_users.sql

Отредактируем содержимое до следующего вида:

-- +goose Up
-- +goose StatementBegin
ALTER TABLE users ADD COLUMN password_hash Text;
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
ALTER TABLE users DROP COLUMN password_hash;
-- +goose StatementEnd

добавления вторичного индекса по колонке password_hash таблицы пользователей

Cоздаем файл миграции:

$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" create 00008_add_index_password_hash_on_table_users sql
2023/12/15 05:41:02 Created new file: 20231215054102_00008_add_index_password_hash_on_table_users.sql

Отредактируем содержимое до следующего вида:

-- +goose Up
-- +goose StatementBegin
ALTER TABLE users ADD INDEX idx_users_password_hash GLOBAL ON (password_hash);
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
ALTER TABLE users DROP INDEX idx_users_password_hash;
-- +goose StatementEnd

Обратите внимание, что мы создаем файлы миграции каждый раз, когда меняем схему, и рекомендуется размещать их в одном каталоге. Вы думаете, что тогда накапливается много файлов миграции? Это верно, и это очень ожидаемо, потому что многие файлы миграции просто обозначают историю миграции. Таким образом, вы можете настроить базу данных либо с последней схемой, либо со схемой на указанный момент.
Таким образом, в директории migrations появились SQL-файлы миграций:

$ ls .
20231215052248_00001_create_table_users.sql
20231215052432_00002_upsert_test_users.sql
20231215053847_00003_create_table_roles.sql
20231215053854_00004_upsert_initial_roles.sql
20231215053928_00005_add_column_role_id_into_table_users.sql
20231215054003_00006_update_role_id_of_test_users.sql
20231215054033_00007_add_column_password_hash_into_table_users.sql
20231215054102_00008_add_index_password_hash_on_table_users.sql

Теперь мы можем применять и откатывать миграции, пользуясь goose:

Обратите внимание, что мы создаем файлы миграции каждый раз, когда меняем схему, и рекомендуется размещать их в одном каталоге. Вы думаете, что тогда накапливается много файлов миграции? Это верно, и это очень ожидаемо, потому что многие файлы миграции просто обозначают историю миграции. Таким образом, вы можете настроить базу данных либо с последней схемой, либо со схемой на указанный момент.
Таким образом, в директории migrations появились SQL-файлы миграций:

$ ls .
20231215052248_00001_create_table_users.sql
20231215052432_00002_upsert_test_users.sql
20231215053847_00003_create_table_roles.sql
20231215053854_00004_upsert_initial_roles.sql
20231215053928_00005_add_column_role_id_into_table_users.sql
20231215054003_00006_update_role_id_of_test_users.sql
20231215054033_00007_add_column_password_hash_into_table_users.sql
20231215054102_00008_add_index_password_hash_on_table_users.sql

Теперь мы можем применять и откатывать миграции, пользуясь goose:

Применить все миграции (goose up)
$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" up
2023/12/15 05:59:55 OK   20231215052248_00001_create_table_users.sql (68.32ms)
2023/12/15 05:59:55 OK   20231215052432_00002_upsert_test_users.sql (47.47ms)
2023/12/15 05:59:55 OK   20231215053847_00003_create_table_roles.sql (72.54ms)
2023/12/15 05:59:55 OK   20231215053854_00004_upsert_initial_roles.sql (42.19ms)
2023/12/15 05:59:55 OK   20231215053928_00005_add_column_role_id_into_table_users.sql (56.82ms)
2023/12/15 05:59:55 OK   20231215054003_00006_update_role_id_of_test_users.sql (90.8ms)
2023/12/15 05:59:55 OK   20231215054033_00007_add_column_password_hash_into_table_users.sql (58ms)
2023/12/15 05:59:55 OK   20231215054102_00008_add_index_password_hash_on_table_users.sql (209.6ms)
2023/12/15 05:59:55 goose: successfully migrated database to version: 20231215054102

Откатить все миграции (goose reset)
$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" reset
2023/12/15 06:00:16 OK   20231215054102_00008_add_index_password_hash_on_table_users.sql (58.82ms)
2023/12/15 06:00:16 OK   20231215054033_00007_add_column_password_hash_into_table_users.sql (46.08ms)
2023/12/15 06:00:16 OK   20231215054003_00006_update_role_id_of_test_users.sql (36.02ms)
2023/12/15 06:00:16 OK   20231215053928_00005_add_column_role_id_into_table_users.sql (48.9ms)
2023/12/15 06:00:16 OK   20231215053854_00004_upsert_initial_roles.sql (46.55ms)
2023/12/15 06:00:16 OK   20231215053847_00003_create_table_roles.sql (60.72ms)
2023/12/15 06:00:16 OK   20231215052432_00002_upsert_test_users.sql (25.2ms)
2023/12/15 06:00:16 OK   20231215052248_00001_create_table_users.sql (84.94ms)

Применить одну миграцию (goose up-by-one)
$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" up-by-one
2023/12/15 06:00:38 OK   20231215052248_00001_create_table_users.sql (63.62ms)

Применить еще одну миграцию (goose up-by-one)
$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" up-by-one
2023/12/15 06:00:51 OK   20231215052432_00002_upsert_test_users.sql (46.38ms)

И еще одну (goose up-by-one)
$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" up-by-one
2023/12/15 06:01:03 OK   20231215053847_00003_create_table_roles.sql (60.55ms)

Переприменить последнюю миграцию (goose redo)
$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" redo
2023/12/15 06:01:16 OK   20231215053847_00003_create_table_roles.sql (50.72ms)
2023/12/15 06:01:16 OK   20231215053847_00003_create_table_roles.sql (66.41ms)

Узнать статус миграций (goose status)
$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" status
2023/12/15 06:01:31     Applied At                  Migration
2023/12/15 06:01:31     =======================================
2023/12/15 06:01:31     Fri Dec 15 06:00:38 2023 -- 20231215052248_00001_create_table_users.sql
2023/12/15 06:01:31     Fri Dec 15 06:00:51 2023 -- 20231215052432_00002_upsert_test_users.sql
2023/12/15 06:01:31     Fri Dec 15 06:01:16 2023 -- 20231215053847_00003_create_table_roles.sql
2023/12/15 06:01:31     Pending                  -- 20231215053854_00004_upsert_initial_roles.sql
2023/12/15 06:01:31     Pending                  -- 20231215053928_00005_add_column_role_id_into_table_users.sql
2023/12/15 06:01:31     Pending                  -- 20231215054003_00006_update_role_id_of_test_users.sql
2023/12/15 06:01:31     Pending                  -- 20231215054033_00007_add_column_password_hash_into_table_users.sql
2023/12/15 06:01:31     Pending                  -- 20231215054102_00008_add_index_password_hash_on_table_users.sql

Откатить последнюю миграцию (goose down)
$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" down
2023/12/15 06:01:49 OK   20231215053847_00003_create_table_roles.sql (48.58ms)

Посмотреть статус миграций (убедиться, что откатилась последняя миграция) (goose status)
$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" status
2023/12/15 06:02:02     Applied At                  Migration
2023/12/15 06:02:02     =======================================
2023/12/15 06:02:02     Fri Dec 15 06:00:38 2023 -- 20231215052248_00001_create_table_users.sql
2023/12/15 06:02:02     Fri Dec 15 06:00:51 2023 -- 20231215052432_00002_upsert_test_users.sql
2023/12/15 06:02:02     Pending                  -- 20231215053847_00003_create_table_roles.sql
2023/12/15 06:02:02     Pending                  -- 20231215053854_00004_upsert_initial_roles.sql
2023/12/15 06:02:02     Pending                  -- 20231215053928_00005_add_column_role_id_into_table_users.sql
2023/12/15 06:02:02     Pending                  -- 20231215054003_00006_update_role_id_of_test_users.sql
2023/12/15 06:02:02     Pending                  -- 20231215054033_00007_add_column_password_hash_into_table_users.sql
2023/12/15 06:02:02     Pending                  -- 20231215054102_00008_add_index_password_hash_on_table_users.sql

Применить все оставшиеся миграции (goose up)
$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" up
2023/12/15 06:02:37 OK   20231215053847_00003_create_table_roles.sql (62.95ms)
2023/12/15 06:02:37 OK   20231215053854_00004_upsert_initial_roles.sql (44.33ms)
2023/12/15 06:02:37 OK   20231215053928_00005_add_column_role_id_into_table_users.sql (66.65ms)
2023/12/15 06:02:37 OK   20231215054003_00006_update_role_id_of_test_users.sql (109.95ms)
2023/12/15 06:02:37 OK   20231215054033_00007_add_column_password_hash_into_table_users.sql (56.46ms)
2023/12/15 06:02:37 OK   20231215054102_00008_add_index_password_hash_on_table_users.sql (185.34ms)
2023/12/15 06:02:37 goose: successfully migrated database to version: 20231215054102

Проверить статус миграций (убедиться, что все миграции успешно применились) (goose status)
$ goose ydb "grpc://localhost:2136/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" status
2023/12/15 06:02:52     Applied At                  Migration
2023/12/15 06:02:52     =======================================
2023/12/15 06:02:52     Fri Dec 15 06:00:38 2023 -- 20231215052248_00001_create_table_users.sql
2023/12/15 06:02:52     Fri Dec 15 06:00:51 2023 -- 20231215052432_00002_upsert_test_users.sql
2023/12/15 06:02:52     Fri Dec 15 06:02:37 2023 -- 20231215053847_00003_create_table_roles.sql
2023/12/15 06:02:52     Fri Dec 15 06:02:37 2023 -- 20231215053854_00004_upsert_initial_roles.sql
2023/12/15 06:02:52     Fri Dec 15 06:02:37 2023 -- 20231215053928_00005_add_column_role_id_into_table_users.sql
2023/12/15 06:02:52     Fri Dec 15 06:02:37 2023 -- 20231215054003_00006_update_role_id_of_test_users.sql
2023/12/15 06:02:52     Fri Dec 15 06:02:37 2023 -- 20231215054033_00007_add_column_password_hash_into_table_users.sql
2023/12/15 06:02:52     Fri Dec 15 06:02:37 2023 -- 20231215054102_00008_add_index_password_hash_on_table_users.sql

Более полную справку по командам goose читайте в документации goose.

Демонстрация работы goose из Go-кода

Мы можем писать шаги миграции не только через SQL, но и из Go. Это может быть удобно в двух случаях:

  • если вам требуется иметь утилиту миграции конкретно вашей собственной схемы данных

  • если процесс применения миграций является частью этапа старта вашего приложения.

В обоих случаях предполагается, что на старте приложения происходит соединение с БД и применяются миграции.

Goose и пакет embed стандартной библиотеки Go позволяют встроить SQL-файлы миграций в бинарный исполняемый файл.

Пример встраивания SQL-файлов миграций в Go-код
package main

import (
  "context"
  "database/sql"
  "embed"

  "github.com/pressly/goose/v3"
  "github.com/ydb-platform/ydb-go-sdk/v3"
)

//go:embed migrations/*.sql
var embedMigrations embed.FS

func main() {
  ctx, cancel := context.WithCancel(context.Background())
  defer cancel()
  nativeDriver, err := ydb.Open(ctx, "grpc://localhost:2136/local")
  if err != nil {
    panic(err)
  }
  defer nativeDriver.Close(ctx)
  connector, err := ydb.Connector(nativeDriver,
    ydb.WithDefaultQueryMode(ydb.ScriptingQueryMode),
    ydb.WithFakeTx(ydb.ScriptingQueryMode),
    ydb.WithAutoDeclare(),
    ydb.WithNumericArgs(),
  )
  if err != nil {
    panic(err)
  }
  db := sql.OpenDB(connector)
  defer db.Close()
  goose.SetBaseFS(embedMigrations)

  if err := goose.SetDialect("ydb"); err != nil {
    panic(err)
  }

  if err := goose.Up(db, "migrations"); err != nil {
    panic(err)
  }

  // Основная логика работы приложения
}

Также goose позволяет описать миграции непосредственно в Go-коде.

Пример описания миграций непосредственно в виде функций в Go-коде.
package main

import (
  "context"
  "database/sql"
  
  "github.com/pressly/goose/v3"
  "github.com/ydb-platform/ydb-go-sdk/v3"
)

func init() {
  goose.AddMigrationContext(Up00001, Down00001)
  goose.AddMigrationContext(Up00002, Down00002)
}
func Up00001(ctx context.Context, tx *sql.Tx) error {
  _, err := tx.ExecContext(ctx, `
    CREATE TABLE users (
      id Uint64,
      username Text,
      created_at Datetime,
      PRIMARY KEY (id)
  );`)
  return err
}
func Down00001(ctx context.Context, tx *sql.Tx) error {
  _, err := tx.ExecContext(ctx, "DROP TABLE users;")
  return err
}
func Up00002(ctx context.Context, tx *sql.Tx) error {
  _, err := tx.ExecContext(ctx, `
    UPSERT INTO users (id, username, created_at) VALUES
    (1, 'Bob Smith', CAST('2023-12-01T15:14:13Z' AS Datetime)),
    (2, 'Dow Jones', CAST('2023-12-02T12:11:10Z' AS Datetime)),
    (3, 'Elon Mask', CAST('2023-12-03T09:08:07Z' AS Datetime)),
    (4, 'John Dow', CAST('2023-12-04T06:05:04Z' AS Datetime));

  `)
  return err
}
func Down00002(ctx context.Context, tx *sql.Tx) error {
  _, err := tx.ExecContext(ctx, "DELETE FROM users WHERE id in (1, 2, 3, 4);")
  return err
}
func main() {
  ctx, cancel := context.WithCancel(context.Background())
  defer cancel()
  nativeDriver, err := ydb.Open(ctx, "grpc://localhost:2136/local")
  if err != nil {
    panic(err)
  }
  defer nativeDriver.Close(ctx)
  connector, err := ydb.Connector(nativeDriver,
    ydb.WithDefaultQueryMode(ydb.ScriptingQueryMode),
    ydb.WithFakeTx(ydb.ScriptingQueryMode),
    ydb.WithAutoDeclare(),
    ydb.WithNumericArgs(),
  )
  if err != nil {
    panic(err)
  }
  db := sql.OpenDB(connector)
  defer db.Close()

  if err := goose.SetDialect("ydb"); err != nil {
    panic(err)
  }

  if err := goose.Up(db, "migrations"); err != nil {
    panic(err)
  }

  // Основная логика работы приложения
}

Заключение

Версионирование и миграция базы данных в продакшен-приложениях упрощаются при использовании утилиты миграции goose. Goose позволяет версионировать не только схему таблиц SQL базы данных, но и сами данные. Утилита goose поддерживает команды применения миграций по одной или всех сразу, отката, повторного применения миграций и статуса базы данных. Goose написан на Go, но не ограничивает проекты только этим языком. Если выполнять миграции отдельно от самих жизненного цикла самих приложений, то не возникает ограничений на используемый стек. Goose позволяет описывать шаги миграции на SQL или в Go-коде.
Начиная с версии v3.16.0 goose поддерживает YDB - распределенную open-source СУБД. В статье показаны примеры версионирования базы данных YDB через SQL инструкции, а также в Go-коде.

Если у вас возникнут какие-либо трудности или вопросы, пожалуйста, не стесняйтесь обращаться к нам через:

Комментарии (0)