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

Ответ

Миграции — это краеугольный камень практики Database-as-Code. Мы использовали миграции EF Core для гарантии идентичности схемы БД на всех стендах (dev, test, production) и ведения истории изменений.

Рабочий процесс (Development)

  1. Изменение модели данных в классах C# (сущностях DbContext).
  2. Создание миграции: Эта команда сравнивает текущую модель с состоянием БД и генерирует файлы миграции.
    dotnet ef migrations add AddUserEmailAndCreatedTimestamp
    # Создаст файлы: 20240515120000_AddUserEmailAndCreatedTimestamp.cs
  3. Проверка сгенерированного кода: Всегда просматривайте файл миграции Up() и Down() методы. Иногда требуется ручная корректировка (например, для сложных индексов или данных по умолчанию).

    public partial class AddUserEmailAndCreatedTimestamp : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.AddColumn<string>(
                name: "Email",
                table: "Users",
                type: "text",
                nullable: false,
                defaultValue: "");
    
            migrationBuilder.AddColumn<DateTime>(
                name: "CreatedAt",
                table: "Users",
                type: "timestamp with time zone",
                nullable: false,
                defaultValueSql: "NOW()"); // Пример использования SQL-выражения
        }
        // ... метод Down для отката
    }
  4. Обновление локальной БД:
    dotnet ef database update

Интеграция в CI/CD (Production) Прямой вызов dotnet ef в production нежелателен. Вместо этого:

  • Генерация SQL-скрипта: На этапе сборки создается idempotent-скрипт, который можно проверить.
    dotnet ef migrations script --idempotent --output migrations.sql
  • Применение через инструменты БД: Сгенерированный migrations.sql применяется к production-БД с помощью надежных инструментов (например, Flyway, Liquibase, или через пайплайн DevOps с проверкой DBA). Это дает контроль, возможность отката и аудит.

Критически важные практики:

  • Миграции должны быть обратимыми (Down метод). Это важно для отката неудачных релизов.
  • Не редактируйте файлы миграций после того, как они применены к общей БД. Создавайте новую миграцию.
  • Для seed-данных используйте отдельные миграции или кастомную логику инициализации, проверяющую наличие данных.
  • Всегда тестируйте миграции на копии production-базы перед деплоем.

Ответ 18+ 🔞

Давай разберем эту всю хуйню про миграции, чтобы у тебя в голове не каша была, а понимание, как это внатуре работает.

Смотри, миграции — это типа священный грааль, когда ты делаешь базу как код. Без них — пиздец и бардак: у тебя на локале одна схема, на тестовом — другая, а на прод влетает третья, и всё падает с душераздирающими ошибками про отсутствующие колонки. Мы на EF Core отбили все зубы, чтобы такого не было. Суть в том, что история всех изменений базы — это просто папка с файликами в твоём репозитории. Красота, а не жизнь.

Как с этим жить, когда пишешь фичу (Development)

  1. Ты че-то там накосячил в моделях. Добавил поле Email пользователю или, там, новую таблицу KarmaPoints. Обычные дела.
  2. Генеришь миграцию. Ты не пишешь SQL руками, как последний лузер. Ты даёшь команду EF Core, чтобы он сам, умный такой, сравнил твои классы C# с тем, что сейчас в базе, и нагенерил разницу.
    dotnet ef migrations add AddUserEmailAndCreatedTimestamp

    Он создаст файл с именем вроде 20240515120000_AddUserEmailAndCreatedTimestamp.cs. Циферки в начале — это timestamp, чтобы порядок соблюдался, а то будет ебучка.

  3. Смотришь, что этот умник нагенерил. Это ОБЯЗАТЕЛЬНО. Потому что EF Core иногда такое выдумает, что волосы дыбом. Откроешь файл, а там в Up() методе какая-нибудь дичь. Или Down() метод для отката кривой. Поправить надо сразу.

    public partial class AddUserEmailAndCreatedTimestamp : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            // Смотри, он хочет добавить колонку Email. Вроде норм.
            migrationBuilder.AddColumn<string>(
                name: "Email",
                table: "Users",
                type: "text",
                nullable: false,
                defaultValue: ""); // А вот дефолтное значение пустой строки — окей.
    
            // А тут добавляет CreatedAt со значением NOW() из самой базы. Уже лучше, чем C#-овый DateTime.UtcNow в коде миграции.
            migrationBuilder.AddColumn<DateTime>(
                name: "CreatedAt",
                table: "Users",
                type: "timestamp with time zone",
                nullable: false,
                defaultValueSql: "NOW()");
        }
        // ... а тут должен быть метод Down(), который это всё откатывает. Если его нет или он хуёвый — пиши пропало.
    }
  4. Запускаешь это добро на свою локальную базу. Просто, как два байта:
    dotnet ef database update

    И всё, база обновилась. Теперь у тебя и в коде, и в базе одно и то же. Можно тестировать.

А вот как это всё должно ебашить на проде, чтобы не обосраться (CI/CD) Тут уже не до шуток. Ты не будешь же на продакшен-сервере, где база на терабайты, запускать dotnet ef database update! Это уровень дилетанта, который хочет всё сломать.

  • Готовим скрипт. На этапе сборки (в CI) мы из этих файлов-миграций генерируем один большой, но идемпотентный SQL-скрипт. Это значит, что его можно нахуй применять сколько угодно раз — если изменение уже есть, оно не выполнится второй раз.
    dotnet ef migrations script --idempotent --output migrations.sql

    Получаем файл migrations.sql. Его можно открыть, показать своему DBA (если он у вас есть, а не вы сами себе DBA) и сказать: "Мужик, смотри, что мы будем в базу пихать".

  • Применяем с умом. Этот скрипт мы не запускаем тупо из пайплайна. Мы используем специальные, взрослые инструменты: Flyway, Liquibase или хотя бы прогоняем через шаг деплоя, где есть ручное подтверждение и откат. Это даёт тебе контроль, аудит и спокойный сон.

Железные правила, которые нарушать — себя не уважать:

  • Метод Down() — это святое. Он должен чисто откатывать то, что делает Up(). Представь, что релиз пошёл по пизде и надо откатиться на прошлую версию приложения. Без рабочего Down() ты в глубокой жопе.
  • Не лезь в старые миграции, которые уже ушли на общие базы. Ты их отредактируешь, а у коллег или на других стендах уже другая история. Полный пиздец и рассинхрон. Нужно новое изменение — создавай новую миграцию.
  • Чтобы накатить тестовые данные (seed), не пихай их прямо в миграции. Либо делай отдельную, чисто дата-миграцию, либо пиши скрипт, который умно проверяет, есть ли уже данные, прежде чем их вставить.
  • И БОГ ТЕБЕ В ПОМОЩЬ, ТЕСТИРУЙ ЭТИ МИГРАЦИИ НА КОПИИ ПРОД БАЗЫ! Перед тем как лезть в боевую, скопируй её куда-нибудь и прогнай там свои скрипты. Иначе можно получить "ой, а мы не думали, что на проде 100 миллионов записей, и наша миграция с добавлением индекса будет выполняться 8 часов, положив сервис". Удивление будет пиздец.