.NET Tutorial
Middle7 мин чтенияВопрос 4 из 14

Миграции: создание, применение, откат, squash?

Как устроены миграции

Изменили модель
(добавили свойство)

dotnet ef migrations add

Сравнение модели
со снапшотом

20250930_AddPhone.cs
Up / Down

ModelSnapshot.cs
обновлён

database update /
script / bundle

БД + запись в
__EFMigrationsHistory

Изменили модель
(добавили свойство)

dotnet ef migrations add

Сравнение модели
со снапшотом

20250930_AddPhone.cs
Up / Down

ModelSnapshot.cs
обновлён

database update /
script / bundle

БД + запись в
__EFMigrationsHistory

Жизненный цикл миграции
Сгенерированная миграция
public partial class AddCustomerPhone : Migration
{
    protected override void Up(MigrationBuilder mb)
    {
        mb.AddColumn<string>(name: "phone", table: "customers", type: "varchar(20)", maxLength: 20, nullable: true);
        mb.CreateIndex(name: "ix_customers_phone", table: "customers", column: "phone");
    }

    protected override void Down(MigrationBuilder mb)
    {
        mb.DropIndex(name: "ix_customers_phone", table: "customers");
        mb.DropColumn(name: "phone", table: "customers");
    }
}

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

Основные команды

Shell
dotnet ef migrations add AddCustomerPhone            # создать
dotnet ef migrations list                            # список и статус (применена ли)
dotnet ef migrations remove                          # удалить ПОСЛЕДНЮЮ неприменённую
dotnet ef migrations has-pending-model-changes       # есть ли изменения модели без миграции (EF 8) — для CI

dotnet ef database update                            # применить все
dotnet ef database update AddOrders                  # откатить/накатить до конкретной миграции (Down для последующих)
dotnet ef database update 0                          # откатить всё

dotnet ef migrations script --idempotent -o migrate.sql        # SQL для всех миграций с проверками
dotnet ef migrations script AddOrders AddCustomerPhone          # SQL между двумя миграциями
dotnet ef migrations bundle --self-contained -r linux-x64      # исполняемый файл efbundle

Способы применения в продакшене

1. Идемпотентный SQL-скрипт

CI: migrations script --idempotent

Ревью DBA

Применение: psql / sqlcmd /
шаг пайплайна

2. Migration bundle

CI: dotnet ef migrations bundle

Шаг деплоя / K8s Job:
./efbundle --connection ...

3. Migrate() при старте

Каждый экземпляр
при старте вызывает Migrate()

гонки при нескольких репликах,
приложению нужны права DDL

1. Идемпотентный SQL-скрипт

CI: migrations script --idempotent

Ревью DBA

Применение: psql / sqlcmd /
шаг пайплайна

2. Migration bundle

CI: dotnet ef migrations bundle

Шаг деплоя / K8s Job:
./efbundle --connection ...

3. Migrate() при старте

Каждый экземпляр
при старте вызывает Migrate()

гонки при нескольких репликах,
приложению нужны права DDL

Варианты доставки миграций
Способ Плюсы Минусы
Идемпотентный SQL-скрипт Прозрачно, ревью DBA, любые инструменты Нужно отдельно выполнять
Migration bundle Один артефакт, не нужен SDK, выполняется в пайплайне или K8s Job Ещё один бинарник в поставке
Database.Migrate() при старте Просто для прототипов и одиночного инстанса Гонки реплик, долгий старт, права DDL у приложения, откат приложения не откатит схему

Ручная доработка миграций

Сгенерированный код — это черновик. Его нужно читать и часто править:

C#
protected override void Up(MigrationBuilder mb)
{
    // EF сгенерировал DropColumn + AddColumn при переименовании свойства → ПОТЕРЯ ДАННЫХ
    // Исправляем на переименование:
    mb.RenameColumn(name: "fullname", table: "customers", newName: "display_name");

    // Перенос данных
    mb.AddColumn<string>("status_code", "orders", nullable: true);
    mb.Sql("UPDATE orders SET status_code = CASE status WHEN 0 THEN 'new' WHEN 1 THEN 'paid' END");
    mb.AlterColumn<string>("status_code", "orders", nullable: false);

    // Индекс без блокировки таблицы (PostgreSQL) — вне транзакции
    mb.Sql("CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_orders_created ON orders (created_at)", suppressTransaction: true);
}

Откат

  • В разработке: dotnet ef database update PreviousMigration → выполнит Down().
  • В продакшене Down применяют редко: откат удаления столбца не вернёт данные. Чаще — roll forward: новая миграция, исправляющая проблему.
  • Поэтому важно, чтобы новая версия схемы была совместима с предыдущей версией кода — тогда откат приложения не требует отката схемы.

Zero-downtime: expand / contract

Во время rolling deployment старая и новая версии приложения работают одновременно с одной схемой.

Приложение v2Приложение v1Схема БДШаг 1 — expand: добавить новый столбец display_name,скопировать данные, триггер или двойная записьШаг 2 — всеэкземпляры на v2Шаг 3 — contract: v3 перестаёт использовать fullname,следующая миграция удаляет столбецпишет fullnameпишет fullname и display_name, читает display_name
Приложение v2Приложение v1Схема БДШаг 1 — expand: добавить новый столбец display_name,скопировать данные, триггер или двойная записьШаг 2 — всеэкземпляры на v2Шаг 3 — contract: v3 перестаёт использовать fullname,следующая миграция удаляет столбецпишет fullnameпишет fullname и display_name, читает display_name
Переименование столбца без простоя

Правила:

  • Добавлять столбцы — nullable или с default.
  • Не удалять и не переименовывать столбцы, которые использует работающая версия.
  • NOT NULL на существующий столбец — только после заполнения данных.
  • Большие таблицы: индексы CONCURRENTLY/ONLINE, изменения пачками.

Squash миграций

Через пару лет в проекте сотни миграций: медленная сборка, долгое создание тестовой БД. Squash:

  1. Убедиться, что все окружения применили все миграции.
  2. Удалить папку Migrations.
  3. dotnet ef migrations add Baseline — одна миграция с полной схемой.
  4. На существующих БД: очистить __EFMigrationsHistory и вставить запись Baseline (не применяя её). На новых — применится целиком.
  5. Перенести в Baseline ручные SQL-объекты (представления, функции, seed-данные) из старых миграций — scaffolding их не восстановит!

Типичные ошибки

  • Не читать сгенерированную миграцию → потеря данных при переименовании.
  • Редактировать уже применённую в других окружениях миграцию — окружения разойдутся.
  • Конфликт в ModelSnapshot.cs при слиянии двух веток с миграциями — разрешают, удаляя свою миграцию, подтягивая чужую и пересоздавая свою.
  • Длинные миграции данных внутри транзакции деплоя — блокировки таблиц и таймауты.
  • Seed-данные через HasData для больших или часто меняющихся справочников — каждое изменение генерирует миграцию.

Вопросы на засыпку

Что делает флаг --idempotent?

Оборачивает каждую миграцию в проверку «есть ли её Id в __EFMigrationsHistory». Такой скрипт можно безопасно выполнять на БД в любом состоянии — применятся только недостающие миграции.

Как проверить в CI, что разработчик не забыл создать миграцию?

dotnet ef migrations has-pending-model-changes (EF Core 8+) вернёт ошибку, если модель отличается от снапшота. Альтернатива — тест, вызывающий context.Database.HasPendingModelChanges().

Как применять миграции в Kubernetes?

Отдельный Job (или init-container) с migration bundle или SQL-скриптом перед обновлением Deployment (Helm hook pre-upgrade). Так миграция выполняется один раз, с отдельными правами и логами.

Что такое HasData и когда не стоит его использовать?

Seed-данные в модели: EF генерирует InsertData/UpdateData в миграциях. Подходит для небольших неизменных справочников с фиксированными ключами. Для тестовых данных, больших объёмов и данных, зависящих от окружения, — отдельные скрипты или UseSeeding (EF Core 9).

Итог

Миграции — версионированная история схемы: создаются сравнением модели со снапшотом, применяются идемпотентным скриптом или bundle в пайплайне и всегда требуют ревью. Для продакшена думайте о совместимости версий (expand/contract), предпочитайте roll forward откату, а накопившуюся историю сжимайте squash-ем после синхронизации всех окружений.