27 марта

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

Блог компании CUSTISOracle.NETSQLC#


Обычно фреймворк 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
Теги:.net corec#entity framework coreoracleddl
Хабы: Блог компании CUSTIS Oracle .NET SQL C#
+9
1,7k 18
Комментарии 2
Лучшие публикации за сутки