Миграции
Планирование, применение и доказательство изменений схемы.
Модель#
В режиме managed декларации — это желаемое состояние. makemigrations сравнивает их с состоянием, которое описывают существующие артефакты миграций, — не с живой базой — и пишет разницу артефактом.
Разница существенна: планирование по живой базе дало бы миграцию, зависящую от той базы, на которой её планировали.
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 создаст её за вас, чтобы вы правили файл, а не
выдумывали его с нуля:
$ 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:
{
"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 говорит, можно ли выполнять это внутри транзакции.
Это же делает возможным изменение колонки в три шага, недостижимое ни для одного инструмента, который умеет только схему:
- добавить колонку как nullable;
raw_sql, чтобы её заполнить;- поставить
NOT NULL.
Движок не разбирает SQL, поэтому raw_sql ничего не меняет в состоянии миграций
и объявляет себя разрушительной операцией — осторожное предположение о том, что
прочитать нельзя. Если ваш SQL всё-таки меняет схему, добавьте рядом
state_only, чтобы состояние осталось правдой; иначе следующий план попытается
внести это изменение снова.
Начальные данные — случай попроще и тот же механизм. Где им место, в миграции или
в отдельном шаге, — это настоящий выбор: миграция выполняется по разу на базу и
просматривается вместе с тем изменением схемы, к которому относится, а файл
начальных данных, выполняемый при каждом развёртывании, требует
ON CONFLICT DO NOTHING и уникального ограничения, по которому это сработает.
Справочные данные, без которых схема бессмысленна, относятся к миграции.
Удобные фикстуры разработчика — нет.
Материализованные представления#
Материализованное представление хранит строки, вычисленные по телу запроса, поэтому смена этого тела — не изменение колонки: после неё строки просто неверны. Планировщик отказывается менять определение молча и просит написать явную миграцию.
Индексы на матпредставлении планируются отдельно от самого отношения — именно это делает пригодность к конкурентному обновлению фактом, который генератор может записать. См. Представления.
Проверка в CI#
orm makemigrations --check # за каждой декларацией стоит миграция
orm check --generated # закоммиченный сгенерированный код актуаленПервая падает, когда кто-то поменял структуру и забыл спланировать. Вторая — когда спланировал и забыл перегенерировать.
Разобранные примеры#
Безопасное добавление колонки#
Добавить nullable-колонку — мгновенно. Добавить NOT NULL без умолчания —
переписать таблицу и заблокировать запись на это время, поэтому это три миграции,
а не одна:
// 1. Добавить nullable.
Currency *string
// 2. Заполнить, вне миграции, пачками.
// 3. Затем сделать NOT NULL.
Currency string `orm:"default:'EUR'"`orm makemigrations --dry-run --sql показывает, что из этого PostgreSQL сделает
дёшево, — до того как вы узнаете это на проде.
Переименование без простоя#
Планировщик видит удалённую колонку и добавленную, а не переименование, а удаление колонки удаляет её данные. Добавить, писать в обе, заполнить, удалить — четыре выката:
orm makemigrations --dry-run --sql # прочитайте, прежде чем поверитьПроверка, что выкат завершён#
orm showmigrations # что применено, что ожидает
orm migrate --plan # что именно сделает следующий запуск
orm check --generated # закоммиченный код соответствует схемеШлюз в CI#
- run: orm makemigrations --check # декларация, которую никто не спланировал
- run: orm check --generated # план, под который никто не перегенерировалПервая падает, когда структуру изменили и забыли. Вторая — когда спланировали и забыли перегенерировать. Вдвоём они не дают трём представлениям разойтись.