К содержимому

Миграции

Планирование, применение и доказательство изменений схемы.

Модель#

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

Разница существенна: планирование по живой базе дало бы миграцию, зависящую от той базы, на которой её планировали.

bash
orm makemigrations                   # спланировать и записать
orm makemigrations --dry-run --sql   # показать SQL, ничего не писать
orm makemigrations --check           # упасть, если что-то не спланировано
orm migrate                          # применить
orm migrate --plan                   # показать, что будет применено
orm showmigrations                   # что применено, что ожидает

Артефакты переносимы#

Миграция — это JSON с описанием операций, а не SQL-скрипт. Два следствия:

  • Она одинаково воспроизводится на любом поддерживаемом мажоре PostgreSQL.
  • В ней нет ничего серверно-локального: ни OID, ни имени базы, ни версии сервера, ни разобранного сервером определения, ни абсолютных путей. Артефакт с любым из этого сходится на машине, где его написали, и больше нигде.

Транзакции#

Каждая миграция применяется в одной транзакции вместе с записью в свою историю. Упавшая миграция откатывается и не записывается — то есть неудачный запуск оставляет базу ровно такой, какой она была, а повторный запуск падает так же, а не применяется наполовину.

Applying 0002_add_orders ... FAILED

orm migrate: migration 0002_add_orders failed at operation 1
(alter column public.orders.total: type text -> numeric); the transaction was
rolled back and the migration is not recorded: ERROR: column "total" cannot be
cast automatically to type numeric (SQLSTATE 42804)

Обратите внимание, что здесь произошло: планировщик спланировал, а отказал PostgreSQL. Библиотека не придумывает выражение USING, чтобы такая миграция прошла, — это было бы решением инструмента за вас о том, что делать со строками, которые не конвертируются.

Разрушающие изменения проходят через шлюз#

Удаление колонки или таблицы не планируется молча. Шлюз существует потому, что цена ошибочного DROP неограниченна, а цена лишнего подтверждения — одна команда.

Чего миграции не делают#

Две замороженные границы:

  • Миграции не создают схемы. CREATE SCHEMA — ваш.
  • Миграции не создают домены и расширения. По той же причине.

Это предпосылки, а не изменение схемы, и притворяться иначе значило бы делать orm migrate командой для суперпользователя.

Данные и аварийный выход в сырой SQL#

Порождённая миграция описывает операции над схемой, и в create table или add column строкам места нет. Но это не вся правда, а документация до сих пор позволяла думать, что вся.

orm makemigrations --empty создаст её за вас, чтобы вы правили файл, а не выдумывали его с нуля:

console
$ orm makemigrations --empty --name seed_tags
wrote migrations/0002_seed_tags.json

Fill in Up with the SQL to run, and Down with the SQL that undoes it.

Она пишется независимо от того, менялись ли модели: данные — ровно тот случай, которого не видит сравнение схем. Оставленная заготовка не «ничего не делает», а бросает исключение, поэтому созданная и забытая миграция упадёт, а не будет записана как применённая.

Артефакт — это JSON, а операция называется raw_sql:

json
{
  "op": "raw_sql",
  "args": {
    "Up": "INSERT INTO user_tags (text) VALUES ('music'), ('sports') ON CONFLICT (text) DO NOTHING",
    "Down": "DELETE FROM user_tags WHERE text IN ('music', 'sports')",
    "Atomic": true,
    "Description": "seed the starting tags"
  }
}

Down необязателен, и именно его отсутствие делает операцию необратимой: это сказано прямо, а не подделано пустышкой, которая якобы что-то откатила. Atomic говорит, можно ли выполнять это внутри транзакции.

Это же делает возможным изменение колонки в три шага, недостижимое ни для одного инструмента, который умеет только схему:

  1. добавить колонку как nullable;
  2. raw_sql, чтобы её заполнить;
  3. поставить NOT NULL.

Движок не разбирает SQL, поэтому raw_sql ничего не меняет в состоянии миграций и объявляет себя разрушительной операцией — осторожное предположение о том, что прочитать нельзя. Если ваш SQL всё-таки меняет схему, добавьте рядом state_only, чтобы состояние осталось правдой; иначе следующий план попытается внести это изменение снова.

Начальные данные — случай попроще и тот же механизм. Где им место, в миграции или в отдельном шаге, — это настоящий выбор: миграция выполняется по разу на базу и просматривается вместе с тем изменением схемы, к которому относится, а файл начальных данных, выполняемый при каждом развёртывании, требует ON CONFLICT DO NOTHING и уникального ограничения, по которому это сработает. Справочные данные, без которых схема бессмысленна, относятся к миграции. Удобные фикстуры разработчика — нет.

Материализованные представления#

Материализованное представление хранит строки, вычисленные по телу запроса, поэтому смена этого тела — не изменение колонки: после неё строки просто неверны. Планировщик отказывается менять определение молча и просит написать явную миграцию.

Индексы на матпредставлении планируются отдельно от самого отношения — именно это делает пригодность к конкурентному обновлению фактом, который генератор может записать. См. Представления.

Проверка в CI#

bash
orm makemigrations --check   # за каждой декларацией стоит миграция
orm check --generated        # закоммиченный сгенерированный код актуален

Первая падает, когда кто-то поменял структуру и забыл спланировать. Вторая — когда спланировал и забыл перегенерировать.

Разобранные примеры#

Безопасное добавление колонки#

Добавить nullable-колонку — мгновенно. Добавить NOT NULL без умолчания — переписать таблицу и заблокировать запись на это время, поэтому это три миграции, а не одна:

go
// 1. Добавить nullable.
Currency *string

// 2. Заполнить, вне миграции, пачками.
// 3. Затем сделать NOT NULL.
Currency string `orm:"default:'EUR'"`

orm makemigrations --dry-run --sql показывает, что из этого PostgreSQL сделает дёшево, — до того как вы узнаете это на проде.

Переименование без простоя#

Планировщик видит удалённую колонку и добавленную, а не переименование, а удаление колонки удаляет её данные. Добавить, писать в обе, заполнить, удалить — четыре выката:

bash
orm makemigrations --dry-run --sql   # прочитайте, прежде чем поверить

Проверка, что выкат завершён#

bash
orm showmigrations         # что применено, что ожидает
orm migrate --plan         # что именно сделает следующий запуск
orm check --generated      # закоммиченный код соответствует схеме

Шлюз в CI#

yaml
- run: orm makemigrations --check   # декларация, которую никто не спланировал
- run: orm check --generated        # план, под который никто не перегенерировал

Первая падает, когда структуру изменили и забыли. Вторая — когда спланировали и забыли перегенерировать. Вдвоём они не дают трём представлениям разойтись.