Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Любой более или менее серьезный продакшен, работающий с базой данных, подразумевает процесс миграции - обновление структуры базы данных от одной версии до другой (обычно более новой) [источник].
Миграции в БД можно делать вручную или использовать для этого специальные утилиты (фреймворки). В данной статье речь идет об утилите goose. Это инструмент миграции схемы, который обеспечивает управление миграциями схемы в проекте. Начиная с версии v3.16.0 goose поддерживает YDB - распределенную open-source СУБД. В данной статье мы будем разбирать кейс применения миграций конкретно в YDB.
Что такое "Миграция схемы"?
Под миграциями понимают несколько разных понятий:
Миграция - как процесс перемещения схемы данных и самих данных между различными базами данных. Например, из PostgreSQL в Oracle.
Миграция - как процесс бесшовного перемещения схемы данных и самих данных между однородными базами данных. Например, миграция PostgreSQL с мастер-сервера А на сервер-фолловер Б. Этот процесс еще называют репликацией.
Миграция - как процесс изменения версий схемы данных и самих данных в рамках одной базы данных. Например, миграция версии схемы данных в PostgreSQL с таблицей без вторичных индексов к версии с вторичными индексами.
В данной статье мы будем рассматривать миграции как процесс управления версиями схемы данных в рамках одной базы данных.
Современные приложения обычно не являются чем-то однократно написанным. В процессе эксплуатации, а также по мере развития бизнеса так или иначе приложение претерпевает изменения. И изменения касаются не только логики работы самого приложения, но и в том числе изменения схемы данных. Изменение схемы специально для онлайн-базы данных называется "миграция схемы".
Пока проект еще не вырос из прототипа, можно экспериментировать, каждый раз инициализируя схему данных при старте приложения. Однако как только в базе данных находятся реальные данные уже нельзя удалить старую схему данных и накатить новую. Необходимо задумываться о миграции старой схемы в новую. Особенно, если приложение реализует непрерывную работу. Нужно понять и разделить этапы миграции схемы данных в БД и релизы приложения, закладывающиеся на старую или новую схему данных. Сам процесс разделения релизов для обеспечения непрерывной работоспособности приложения остается за скобками в рамках данной статьи.
Практическая задача
Представьте, что мы разрабатываем сервис - админку пользователей. Обычное CRUD-приложение. По задумке этот сервис будет использоваться для проверки и предоставления прав доступа к другим сервисам в рамках планируемой системы.
Пусть развитие нашего сервиса протекает следующим образом:
На этапе проектирования приложения мы решили сразу завести таблицу пользователей с полями:
id
(идентификатор записи),username
(имя пользователя) иcreated_at
(дату создания учетной записи). Естественно, часть приложения, отвечающего за добавление пользователей, в самом начале отсутствует. Чтобы развязать работу различных команд в части разработки функционала добавления пользователей, их листинга, редактирования и удаления, мы решили сразу же добавить четырех тестовых пользователей (Bob Smith, Dow Jones, John Dow, Elon Mask).На следующем спринте мы подошли к понятию ролей. Мы добавляем таблицу ролей с полями:
id
(идентификатор записи) иname
(название роли). Сразу же добавили ролиadmin
,viewer
,guest
. Нам потребовалось добавить в таблицу пользователей колонку с идентификатором ролиrole_id
. Тестовых пользователей мы готовы сразу разметить ролями. Остальным установим рольquest
и сделаем интерфейс админки, чтобы сменить роль.На этом этапе мы осознали большой просчет - учётки пользователей никак не защищены. Мы захотели добавить пароль (хэш), чтобы аутентифицировать пользователя. Поэтому добавляем колонку с
password_hash
в таблицу пользователей. А в нашем сервисе делаем везде middleware, чтобы автоматически блокировать запросы неавторизованных пользователей.У нас набралось уже немало пользователей и мы увидели, что в топе запросов оказались запросы с инструкцией
WHERE username=… AND password_hash=…
Проанализировав запросы, мы поняли, что не хватает индекса на поляpassword_hash
. В этой версии мы решили добавить индекс на это поле.и так далее (все как в жизни)
Надеюсь, этот пример красноречиво описывает как меняется схема данных в процессе развития приложения. Попробуем эти изменения провести через соответствующий инструмент миграции.
Что такое "goose"?Что такое "goose"?
Goose - это один из инструментов миграции схемы, небольшой, но очень простой. Он написан на Go, и вы можете установить его с помощью "go get" или с помощью готового двоичного файла.
$ go install github.com/pressly/goose/v3/cmd/goose@latest
Существует два варианта goose:
github.com/pressly/goose (форк апстрима)
bitbucket.org/liamstask/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
Параметры строки подключения
Для подключения к 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
- поддержка эмуляции транзакций в режиме выполнения запросов через сервис YDBscripting
. Дело в том, что в 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-коде.
Если у вас возникнут какие-либо трудности или вопросы, пожалуйста, не стесняйтесь обращаться к нам через:
GitHub
Telegram