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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!



Обычно фреймворк 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
Источник: https://habr.com/ru/company/custis/blog/494278/


Интересные статьи

Интересные статьи

Продолжение вчерашней статьи, посвящённой fЁлке, ниже. Читать далее
Продолжение цикла о Zip-архивах и PHP. Предыдущие статьи: Часть 1, Часть 2, Часть 3 Доброго времени суток, дорогие читатели. На этот раз я хотел бы представить, наверное, заключительную част...
Часть первая, дополненная. Котаны, привет. Я Саша и я балуюсь нейронками. По просьбам трудящихся я, наконец, собрался с мыслями и решил запилить серию коротких и почти пошаговых инструкций....
Сегодня делимся с вами рекомендациями Люка Геттинга (Luke Goetting) — признанного эксперта по созданию бизнес-презентаций, директора агентства Puffingston Presentations. Но начнем мы со слов Дж...
Тема статьи навеяна результатами наблюдений за методикой создания шаблонов различными разработчиками, чьи проекты попадали мне на поддержку. Порой разобраться в, казалось бы, такой простой сущности ка...