EF Core + Oracle: как сделать миграции идемпотентными



    Обычно фреймворк EF Core используют в сочетании с MS SQL — другим продуктом Microsoft. Однако это не догма. Например, мы в CUSTIS пишем бизнес-логику на C#, а для управления базами данных используем Oracle. В EF Core есть замечательный механизм миграций, но в нашем случае они не идемпотентны. Дело в том, что Oracle и ряд других БД, например MySQL, не поддерживают транзакционный DDL. Значит, если миграция упадет где-то посередине, ее не получится ни накатить, ни откатить. Как же реализовать идемпотентные миграции на EF Core без MS SQL?

    Предыстория


    В нашей компании есть достаточно мощный инструмент для установки патчей на БД Oracle, который мы используем в ряде проектов. Его писали, когда еще не было Liquibase, миграций EF и других открытых инструментов. Патчер позволяет работать с сотней БД, отслеживать историю установок, просматривать логи, хранить секреты и многое другое. Скрипты для изменения БД пишутся в виде SQL- или m4-макросов. С их помощью можно в числе прочего модифицировать структуру: создавать таблицы, колонки и другие объекты. При этом m4-макросы идемпотентны. Это значит, что при повторной попытке создать, например, таблицу скрипт не упадет, а увидит, что она уже существует, и пропустит создание.

    Предположим, скрипт по установке патча состоит из двух операций:

    1. Создание таблицы А.
    2. Создание таблицы В.

    Если скрипт упадет после первой операции, в Oracle останется таблица А. Повторное применение патча отработает корректно: скрипт проверит, что А уже существует, поэтому сразу же перейдет ко второй операции.

    Помимо достоинств у патчера все же есть недостаток — инструмент закрытый и используется только в CUSTIS. Разработчикам приходится учиться работать с ним, а за пределами компании такой опыт не очень ценен. Кроме того, патчер не поддерживает режим работы Code First, поэтому все скрипты для изменения структуры БД приходится писать вручную.

    Мы хотели попробовать какой-то готовый механизм установки патчей и выбрали миграции. В конце 2019 года как раз стартовал очередной проект для заказчика, на котором мы решили протестировать новый подход. Главной проблемой этого механизма оказалась неидемпотентность миграций.

    Проблема


    В MS SQL цепочка DDL-операторов или миграция выполняется в виде одной составной транзакции. В случае прерывания операция полностью отменяется. В Oracle DDL нетранзакционный, поэтому падение миграции приведет к неконсистентному состоянию БД.

    Вернемся к патчу, состоящему из двух операций: создания таблиц А и B. Если мигратор упадет после первой, в Oracle останется таблица А. Повторный запуск ничего не даст — оператору CREATE TABLE не понравится, что А уже существует. Откатить миграцию также не удастся: EF Core пишет в системную таблицу, что миграция выполнена, только в самом конце процесса. С точки зрения EF Core, если миграция еще не завершена, то и откатывать нечего.

    Решение


    Поиск готового решения для Oracle в интернете не дал результатов. Все, что я нашел, — статьи про способы написания и установки патчей при работе с EF. Чуть позже на StackOverflow натолкнулся на идею — сделать свой IMigrationsSqlGenerator. Этот интерфейс отвечает за формирование SQL-кода, обрабатывающего операции EF.

    В пакет Oracle.EntityFrameworkCore включен OracleMigrationsSqlGenerator, реализующий IMigrationsSqlGenerator. К примеру, если требуется добавить колонку, будет сгенерирован такой код:

    ALTER TABLE MY_TABLE ADD (MY_COLUMN DATE)

    Затем код передается в другие классы для запуска в БД.

    Для начала я попробовал переопределить пару операций OracleMigrationsSqlGenerator. Задача оказалась вполне посильной, и я приступил к написанию идемпотентного мигратора. Так появился CUSTIS.OracleIdempotentSqlGenerator.

    Перед операцией EF наш мигратор проверяет, была ли она выполнена ранее. Например, колонка добавляется так:

    DECLARE
        i NUMBER;
    BEGIN
        SELECT COUNT(*) INTO i
        FROM user_tab_columns
        WHERE table_name = UPPER('MY_TABLE') AND column_name = UPPER('MY_COLUMN');
        IF I != 1 THEN
            EXECUTE IMMEDIATE 'ALTER TABLE MY_TABLE ADD (MY_COLUMN DATE)';  
        END IF;       
    END;

    Использование


    Использовать пакет очень просто — необходимо лишь подменить IMigrationsSqlGenerator в нужном контексте:

    public class MyDbContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.ReplaceService<IMigrationsSqlGenerator, IdempotentSqlGenerator>();
        }
    }

    Миграции формируются и устанавливаются стандартными для EF Core средствами:

    dotnet ef migrations add v1.0.1
    dotnet ef database update

    Общий подход, заложенный в CUSTIS.OracleIdempotentSqlGenerator, может быть реализован в генераторах, написанных для MySQL, MariaDB, Teradata, AmazonAurora и других БД, в которых DDL не является транзакционным.

    Ссылки


    Пакет доступен в NuGet
    Исходники на GitHub
    CUSTIS
    Компания

    Комментарии 2

      0
      Спасибо, выглядит полезно, будем иметь ввиду(=

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое