Миграция кажется рутиной, пока она не заблокирует таблицу заказов на 10 минут в час пик. Middle должен уверенно работать с миграциями, senior должен уметь менять схему большой базы без простоя.
Миграции Django
python manage.py makemigrations # сравнить модели с состоянием миграций и создать файл
python manage.py migrate # применить к базе
python manage.py showmigrations # что применено
python manage.py sqlmigrate app 0007 # какой SQL выполнится
- Файл миграции содержит
dependencies(от каких миграций зависит) иoperations(AddField,AlterField,RunPython...). - Применённые миграции записываются в таблицу
django_migrations. - Миграции коммитят в репозиторий вместе с кодом и не редактируют задним числом, если они уже применены где-то кроме вашей машины.
Alembic (SQLAlchemy, FastAPI)
alembic revision --autogenerate -m "add status"
alembic upgrade head
alembic downgrade -1
Ревизии связаны через down_revision, текущая версия лежит в таблице alembic_version. Autogenerate сравнивает модели и схему, но не всё видит (переименования он видит как удаление плюс добавление), поэтому сгенерированный файл всегда читают глазами.
Миграции данных
Изменение данных (заполнить новое поле, перенести значения) делают отдельной миграцией:
def fill_slug(apps, schema_editor):
Article = apps.get_model('blog', 'Article') # историческая модель
for a in Article.objects.filter(slug='').iterator():
a.slug = slugify(a.title)
a.save(update_fields=['slug'])
class Migration(migrations.Migration):
dependencies = [('blog', '0012_article_slug')]
operations = [migrations.RunPython(fill_slug, migrations.RunPython.noop)]
Важно брать модель через apps.get_model, а не импортировать: импортированная модель описывает текущий код, а миграция должна работать со схемой на момент миграции.
Конфликты
Два разработчика в разных ветках создали 0008_... в одном приложении. После слияния Django увидит два «листа» и попросит makemigrations --merge, который создаст миграцию-слияние. В Alembic аналогично: две ревизии с одним down_revision объединяют alembic merge.
Изменения без простоя (expand и contract)
При деплое старый и новый код работают одновременно (rolling update), поэтому схема должна подходить обоим.
- Expand: добавить новое (колонку nullable или с дефолтом, новую таблицу, индекс конкурентно).
- Выкатить код, который пишет в оба места или понимает оба варианта.
- Migrate: перенести данные пачками.
- Выкатить код, который использует только новое.
- Contract: удалить старое отдельным релизом.
Переименование колонки «в лоб» ломает старый код, который ещё работает. Удаление колонки, которую читает старый код, тоже.
Опасные операции в PostgreSQL
ADD COLUMN ... NOT NULLбез дефолта на непустой таблице не применится, а с изменчивым дефолтом перепишет таблицу. С PostgreSQL 11 добавление колонки с константным дефолтом быстрое.CREATE INDEXблокирует запись в таблицу: на больших таблицахCREATE INDEX CONCURRENTLY(в DjangoAddIndexConcurrently, миграция сatomic = False).- Смена типа колонки часто переписывает таблицу.
- Любая DDL-операция берёт блокировку и может встать в очередь за долгой транзакцией, а за ней встанут все запросы. Ставят
lock_timeout.
Частые ошибки
- Импорт модели в миграцию данных вместо
apps.get_model. - Миграция данных в той же транзакции, что и тяжёлая схема, на миллионах строк.
- Редактирование уже применённой миграции.
- Удаление колонки одновременно с выкладкой кода, который перестал её использовать.
- Отсутствие обратной операции там, где откат реально нужен.
Что спросят на собесе
- Как работают миграции в Django или Alembic, где хранится состояние.
- Как написать миграцию данных и почему
apps.get_model. - Что делать при конфликте миграций.
- Как переименовать колонку или добавить NOT NULL поле без простоя.
- Какие операции опасны на большой таблице PostgreSQL.