ThinkingHome.Migrator — версионная миграция схемы базы данных на платформе .NET Core

в 9:48, , рубрики: .net, C#, database migrations, Microsoft SQL Server, mssql, mysql, postgres, postgresql, sqlite, базы данных

Привет! Сегодня я выпустил новую версию ThinkingHome.Migrator — инструмента для версионной миграции схемы базы данных под платформу .NET Core.
ThinkingHome.Migrator — версионная миграция схемы базы данных на платформе .NET Core - 1

Пакеты опубликованы в NuGet, написана подробная документация. Вы уже можете пользоваться новеньким мигратором, а я расскажу, как он появился, почему у него номер версии 3.0.0 (хотя это первый релиз) и зачем он нужен, когда есть EF Migrations и FluentMigrator.

Как всё начиналось

9 лет назад, в 2009 году я работал ASP.NET разработчиком. Когда мы релизили наш проект, специальный человек оставался на работе допоздна и, одновременно с обновлением файлов на сервере, руками выполнял SQL скрипты, обновляющие БД в проде. Мы искали инструмент, который делал бы это автоматически, и нашли проект Migrator.NET.

Мигратор предлагал новую для того времени идею — задавать изменения БД в виде миграций. Каждая миграция содержит маленькую порцию изменений и имеет номер версии, в которую перейдет БД после её выполнения. Мигратор сам вел учет версий и выполнял нужные миграции в нужном порядке. Особенно круто было то, что мигратор позволял для каждой миграции задать обратные изменения. Можно было при запуске мигратора задать версию, ниже текущей, и он автоматически откатил бы БД до этой версии, выполняя нужные миграции в обратном порядке.

[Migration(1)]
public class AddAddressTable : Migration
{
    override public void Up()
    {
        Database.AddTable("Address", 
            new Column("id", DbType.Int32, ColumnProperty.PrimaryKey),
            new Column("street", DbType.String, 50),
            new Column("city", DbType.String, 50)
        );
    }
    override public void Down()
    {
        Database.RemoveTable("Address");
    }
}

В том миграторе было много ошибок. Он не умел работать со схемами БД, отличными от схемы по умолчанию. В некоторых случаях генерировал некорректные SQL запросы, а если указать ему несуществующий номер версии — впадал в бесконечный цикл. В результате мы с коллегами форкнули проект и чинили там баги.

GitHub.com с его форками и пулл реквестами тогда еще не было (код мигратора лежал на code.google.com). Поэтому мы особенно не заморачивались с тем, чтобы наши изменения попали обратно в оригинальный проект — просто пилили свою копию и сами этим пользовались. Со временем мы переписали бо́льшую часть проекта, а я стал его основным мэйнтейнером. Потом я выложил код нашего мигратора на google code и написал статью на хабр. Так появился ECM7.Migrator.

За время работы над мигратором мы почти полностью его переписали. Заодно немного упростили API и покрыли всё автотестами. Лично мне очень нравилось пользоваться тем, что получилось. В отличие от оригинального мигратора, было ощущение надежности и не было ощущения, что происходит непонятная магия.

Как оказалось, наш мигратор нравился не только мне. Насколько я знаю, он использовался в довольно крупных компаниях. Мне известно про ABBYY, БАРС Груп и concert.ru. Если наберете в поиске запрос "ecm7 migrator", то можете встретить в результатах статьи о нем, упоминания в резюме, описания использования в студенческих работах. Иногда мне приходили письма от незнакомых людей с вопросами или словами благодарности.

После 2012 года проект почти не развивался. Его текущие возможности покрывали все задачи, которые у меня возникали и я не видел необходимости что-то доделывать.

ThinkingHome.Migrator

В прошлом году я начал работать над проектом на .NET Core. Там нужно было сделать возможность подключения плагинов, а у плагинов должна быть возможность создать себе нужную структуру БД, чтобы хранить там свои данные. Это как раз такая задача, для которой хорошо подходит мигратор.

EF Migrations

Для работы с базой данных я использовал Entity Framework Core, поэтому первое, что я попробовал — это EF Migrations. К сожалению, почти сразу пришлось отказаться от идеи использовать их.

Миграции Entity Framework тащат в проект кучу зависимостей, а при запуске — делают какую-то особую магию. Шаг влево/шаг вправо — упираешься в ограничения. Например, миграции Entity Framework почему-то обязательно должны быть в одной сборке с DbContext. Это значит, что не получится хранить миграции внутри плагинов.

FluentMigrator

Когда стало ясно, что EF Migrations не подходят, я поискал решение в гугле и нашел несколько open source миграторов. Самый продвинутый из них, судя по количеству загрузок в NuGet и звездочек на GitHub, оказался FluentMigrator. FM — очень хорош! Он умеет очень многое и у него очень удобный API. Сначала я решил, что это то, то мне нужно, но позже обнаружилось несколько проблем.

Главная проблема — FluentMigrator не умеет параллельно учитывать несколько последовательностей версий внутри одной БД. Как я писал выше, мне нужно было использовать мигратор в модульном приложении. Нужно, чтобы модули (плагины) можно было устанавливать и обновлять независимо друг от друга. У FluentMigrator сквозная нумерация версий. Из-за этого нельзя выполнить/откатить из БД миграции одного плагина, не затронув структуру БД остальных плагинов.

Я пробовал организовать нужное поведение при помощи тэгов, но это тоже не совсем то, что нужно. FluentMigrator не хранит информацию о тэгах выполненных миграций. Кроме того, тэги привязаны к миграциям, а не к сборкам. Это очень странно, учитывая, что точка входа для поиска миграций — именно сборка. В принципе, наверно было можно таким образом сделать параллельный учет версий, но нужно написать над мигратором довольно сложную обертку.

Портировать ECM7.Migrator на .NET Core

В начале этот вариант даже не рассматривал. В то время текущая версия .NET Core была — 1.1 и её API был плохо совместим с .NET Framework, в котором работал ECM7.Migrator. Я был уверен, что портировать его на .NET Core будет сложно и долго. Когда вариантов "взять готовое" не осталось, решил попробовать. Задача оказалась легче, чем я ожидал. На удивление, всё заработало почти сразу. Потребовались лишь небольшие правки. Так как логика мигратора была покрыта тестами, сразу были видны все места, которые сломались и я быстро починил их.

Сейчас я портировал адаптеры только для четырех СУБД: MS SQL Server, PostgreSQL, MySQL, SQLite. Не портировал адаптеры для Oracle (т. к. всё еще нет стабильного клиента под .NET Core), MS SQL Server CE (т.к. он работает только под Windows и мне тупо негде его запускать) и Firebird (т.к. он не очень популярный, портирую позже). В принципе, если нужно будет сделать провайдеры для этих или других СУБД — это довольно просто.

Исходный код нового мигратора лежит на GitHub. Настроен запуск тестов для каждой СУБД в Travis CI. Написана утилита командной строки (.NET Core Global Tool), которую можно легко установить из NuGet. Написана документация — я очень старался написать подробно и понятно и, кажется, так и получилось. Можно брать и пользоваться!

Немного про название...

У нового мигратора нет обратной совместимости со старым. Они работают на разных платформах и у них отличается API. Поэтому проект опубликован под другим названием.

Название выбрано по проекту ThinkingHome, для которого я портировал мигратор. Собственно, ECM7.Migrator тоже назван по проекту, над которым я работал в тот момент.

Возможно, лучше было выбрать какое-то нейтральное название, но мне не пришло в голову хороших вариантов. Если знаете такой — пишите в комментариях. Еще не поздно всё переименовать.

Номер версии указал 3.0.0, т.к. новый мигратор — логическое продолжение старого.

Быстрый старт

Итак, давайте попробуем использовать мигратор.

Все изменения БД записываются в коде миграций — классов, написанных на языке программирования (например, на C#). Классы миграций наследуются от базового класса Migration из пакета ThinkingHome.Migrator.Framework. В них нужно переопределить методы базового класса: Apply (применить изменения) и Revert (откатить изменения). Внутри этих методов разработчик при помощи специального API описывает действия, которые нужно выполнить над БД.

Также класс миграции нужно пометить атрибутом [Migration] и указать версию, в которую перейдет БД после выполнения этих изменений.

Пример миграции

using ThinkingHome.Migrator.Framework;

[Migration(12)]
public class MyTestMigration : Migration
{
    public override void Apply()
    {
        // прямые изменения: создаем таблицу
        Database.AddTable("CustomerAddress",
            new Column("customerId", DbType.Int32, ColumnProperty.PrimaryKey),
            new Column("addressId", DbType.Int32, ColumnProperty.PrimaryKey));
    }

    public override void Revert()
    {
        // обратные изменения: удаляем таблицу
        Database.RemoveTable("CustomerAddress");

        // если откат изменений не нужен, то
        // метод Revert можно не переопределять
    }
}

Как запустить

Миграции компилируются в файл .dll. После этого вы можете выполнить изменения БД с помощью консольной утилиты migrate-database. Для начала, установите её из NuGet.

dotnet tool install -g thinkinghome.migrator.cli

Запустите migrate-database, указав нужный тип СУБД, строку подключения и путь к файлу .dll с миграциями.

migrate-database postgres "host=localhost;port=5432;database=migrations;" /path/to/migrations.dll 

Запускаем через API

Вы можете выполнять миграции через API из собственного приложения. Например, вы можете написать приложение, которое при запуске само создает себе нужную структуру БД.

Для этого подключите в свой проект пакет ThinkingHome.Migrator из NuGet и пакет с провайдером трансформации для нужной СУБД. После этого создайте экземпляр класса ThinkingHome.Migrator.Migrator и вызовите его метод Migrate, передав в качестве параметра нужную версию БД.

var version = -1; // версия -1 означает последнюю доступную версию
var provider = "postgres";
var connectionString = "host=localhost;port=5432;database=migrations;";
var assembly = Assembly.LoadFrom("/path/to/migrations.dll");

using (var migrator = new Migrator(provider, connectionString, assembly))
{
    migrator.Migrate(version);
}

Кстати, можете сравнить с примером запуска FluentMigrator.

Заключение

Я старался сделать простой инструмент без зависимостей и сложной магии. Кажется, получилось неплохо. Проект давно не сырой, всё покрыто тестами, есть подробная документация на русском. Если вы используете .NET Core 2.1, попробуйте новый мигратор. Скорее всего, вам тоже понравится.

Автор: dima117

Источник


  1. Евгений:

    Почитать было интересно) На данный момент как раз озадачен поиском инструмента для решения похожих задач, в ближайшие недели попробуем применить у себя.

    По поводу отсутствия стабильно работающего клиента под Oracle на core, попробуйте использовать продукт от DevArt.
    Он почти полностью обратно совместим с родной библиотекой от Оракл. При переходе в рамках наших корпоративных проектов мы лишь один раз столкнулись с расхождением – dbType.Blob в стандарте = dbType.Lob у DevArt

    Библиотека в рамках единой dll покрывает версии Оракл с 8 по 12.
    У нас весьма самобытная сборка 9, на ней все работает как для NetFrame так и для NetCore

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js