Міграції через goose¶
AutoMigrate годиться для тестової бази даних. Продакшн-схема потребує
змін, які можна рецензувати, які впорядковані, повторювані й
зворотні. goose дає вам це у звичайних файлах SQL.
Модуль:
github.com/pressly/goose/v3.
-- +goose Up
CREATE TABLE users (
id bigserial PRIMARY KEY,
email text NOT NULL UNIQUE
);
-- +goose Down
DROP TABLE users;
Формат файлу¶
Один файл на зміну, названий NNNNN_description.sql, застосовується в
числовому порядку:
Мітки -- +goose Up і -- +goose Down розділяють файл. Up застосовує
зміну; Down її скасовує.
Надавайте перевагу послідовності з нулями попереду, а не міткам часу. Порядкові номери роблять порядок очевидним з першого погляду, а коли двоє розробників додають міграцію того самого дня, виникає конфлікт злиття — і саме цього ви хочете, бо він змушує ухвалити рішення про порядок замість того, щоб мовчки перемішати їх.
Стейтменти, що містять крапки з комою¶
goose розбиває на ;, що ламає будь-що з внутрішньою крапкою з
комою — тіло функції, блок DO, тригер. Огорніть їх:
-- +goose Up
-- +goose StatementBegin
CREATE FUNCTION touch() RETURNS trigger AS $$
BEGIN
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
Забудьте мітки, і отримаєте синтаксичну помилку, що вказує на фрагмент вашої функції — заплутано, поки не знаєш причину.
Вбудовування їх у бінарник¶
Міграції мають постачатися разом із кодом, що на них розраховує, через
go:embed:
//go:embed all:migrations
var embedMigrations embed.FS
func main() {
db, err := sql.Open("pgx", dsn)
defer db.Close()
goose.SetBaseFS(embedMigrations)
if err := goose.SetDialect("postgres"); err != nil {
return err
}
if err := goose.Up(db, "migrations"); err != nil {
return err
}
}
Зауважте all:migrations, а не migrations. Проста форма пропускає
файли, що починаються з _ або ., а мовчки пропущена міграція
означає схему, що розходиться між середовищами — найгірше місце, де
таке правило може вкусити.
goose бере *sql.DB, тож працює з будь-яким драйвером.
Запуск¶
goose.Up(db, "migrations") // apply everything pending
goose.Down(db, "migrations") // roll back exactly one
goose.UpTo(db, "migrations", 2) // apply up to a version
goose.Status(db, "migrations") // what is applied
version, err := goose.GetDBVersion(db)
Up ідемпотентний: повторний запуск нічого не застосовує. Саме це
дозволяє запускати його безумовно при старті або як крок деплою.
Зауважте, Down відкочує одну міграцію, не всі.
Таблиця версій¶
goose записує, що вже застосував, у goose_db_version:
Версія 0 — початковий рядок, який він створює. Ця таблиця — стан вашої схеми — ніколи не редагуйте її вручну і включайте, коли клонуєте базу даних для тестування.
Міграції на Go¶
Для зміни, що потребує логіки — заповнення колонки обчисленими значеннями — зареєструйте функцію Go натомість:
func init() {
goose.AddMigrationContext(upBackfill, downBackfill)
}
func upBackfill(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `UPDATE users SET slug = lower(email)`)
return err
}
Файл живе поряд із SQL-файлами й впорядкований за тим самим числовим
префіксом. Реєстрація відбувається в init(), тож пакет має бути
імпортований — зазвичай порожнім імпортом з бінарника міграцій.
Використовуйте це ощадливо. SQL-міграції може рецензувати будь-хто; Go-міграція — це код, що запускається один раз і потім ніколи більше, що ускладнює її тестування і полегшує помилку.
Де їх запускати¶
| Підхід | Компроміс |
|---|---|
окремий бінарник cmd/migrate |
явно, впорядковано до деплою; потребує кроку job |
| при старті сервісу | нічого оркеструвати; ризиковано з кількома репліками |
Запуск при старті з кількома репліками означає, що кілька процесів мігрують одночасно. goose бере блокування, тож це безпечніше, ніж звучить, але провалена міграція тепер провалює ваш роловт, а не job, який можна повторити. Для всього, що виходить за межі одного екземпляра, окремий бінарник, запущений як крок деплою — спокійніший вибір.
Пишіть міграції, що не ламають роловт¶
Під час деплою старий і новий код певний час працюють з однією схемою. Це обмежує, що може робити одна міграція:
- Додавання колонки, що допускає NULL, або зі значенням за замовчуванням — безпечно.
- Видалення колонки ламає старий код, що все ще її вибирає. Спершу деплойте код, що перестає її використовувати, видаляйте в наступному релізі.
- Перейменування — це видалення плюс додавання. Робіть це в три кроки: додайте нову колонку й пишіть в обидві, заповніть даними, потім приберіть стару.
- Додавання індексу блокує таблицю. У Postgres використовуйте
CREATE INDEX CONCURRENTLY— яка не може виконуватись усередині транзакції, тож потребує-- +goose NO TRANSACTIONна початку файлу. - Заповнення великої таблиці одним стейтментом тримає довге блокування. Розбивайте на пачки.
Кожна міграція також потребує Down, що справді працює. Час виявити
протилежне — не під час інциденту — застосуйте й відкотіть локально
перед відкриттям pull request.
Тримаємо моделі й схему в узгодженому стані¶
goose володіє схемою; ваші структури описують, чого очікує код. Ніщо не з'єднує їх, тож міграція, що додає колонку, і структура, яка ніколи не отримала це поле, не поскаржаться. Перевірка при старті, що порівнює обидві сторони — або тест, що запускає міграції й перевіряє, що колонки збігаються з вашими моделями — дешево закриває цю прогалину.
З досвіду Python: goose — це Alembic без автогенерації. Ви пишете SQL самі, що є більшим обсягом набору тексту, але дає міграції, які справді можна прочитати в рецензії — жодного згенерованого diff, що видаляє колонку, яку ви хотіли залишити.
Швидка довідка¶
| Задача | Форма |
|---|---|
| міграція | NNNNN_name.sql з -- +goose Up / Down |
| крапки з комою всередині стейтменту | -- +goose StatementBegin / StatementEnd |
| постачати в бінарнику | //go:embed all:migrations + goose.SetBaseFS |
| діалект | goose.SetDialect("postgres") |
| застосувати | goose.Up(db, "migrations") — ідемпотентно |
| відкотити одну | goose.Down(db, "migrations") |
| поточна версія | goose.GetDBVersion(db) |
| логіка, а не лише SQL | goose.AddMigrationContext у init() |
| без транзакції | -- +goose NO TRANSACTION |
| безпечні зміни | спершу адитивні; видаляти в наступному релізі |