- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

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

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

[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 [7]). Поэтому мы особенно не заморачивались с тем, чтобы наши изменения попали обратно в оригинальный проект — просто пилили свою копию и сами этим пользовались. Со временем мы переписали бо́льшую часть проекта, а я стал его основным мэйнтейнером. Потом я выложил код нашего мигратора на google code [8] и написал статью на хабр [9]. Так появился ECM7.Migrator.

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

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

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

ThinkingHome.Migrator

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

EF Migrations

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

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

FluentMigrator

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

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

Я пробовал организовать нужное поведение при помощи тэгов [11], но это тоже не совсем то, что нужно. 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 [1]. Настроен запуск тестов для каждой СУБД [12] в Travis CI. Написана утилита командной строки (.NET Core Global Tool [13]), которую можно легко установить из NuGet [14]. Написана документация — я очень старался написать подробно и понятно и, кажется, так и получилось. Можно брать и пользоваться!

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

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

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

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

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

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

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

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

Также класс миграции нужно пометить атрибутом [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 [14].

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 [17] из NuGet и пакет с провайдером трансформации для нужной СУБД [18]. После этого создайте экземпляр класса 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);
}

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

Заключение

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

Автор: dima117

Источник [20]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/c-2/284535

Ссылки в тексте:

[1] ThinkingHome.Migrator: https://github.com/thinking-home/migrator

[2] опубликованы в NuGet: https://www.nuget.org/packages?q=ThinkingHome.Migrator

[3] подробная документация: https://github.com/thinking-home/migrator#readme

[4] EF Migrations: https://docs.microsoft.com/ru-ru/ef/core/managing-schemas/migrations/

[5] FluentMigrator: https://github.com/fluentmigrator/fluentmigrator

[6] Migrator.NET: https://github.com/migratordotnet/Migrator.NET

[7] code.google.com: https://code.google.com/archive/p/migratordotnet/

[8] на google code: https://code.google.com/archive/p/ecm7migrator/

[9] статью на хабр: https://habr.com/post/70884/

[10] проектом: https://github.com/thinking-home/system

[11] тэгов: https://fluentmigrator.github.io/articles/migration/migration-filter-tags.html

[12] запуск тестов для каждой СУБД: https://travis-ci.org/thinking-home/migrator

[13] .NET Core Global Tool: https://habr.com/post/359006/

[14] установить из NuGet: https://www.nuget.org/packages/ThinkingHome.Migrator.CLI

[15] ThinkingHome.Migrator.Framework: https://www.nuget.org/packages/ThinkingHome.Migrator.Framework

[16] специального API: https://github.com/thinking-home/migrator/blob/master/docs/writing-migrations.md

[17] ThinkingHome.Migrator: https://www.nuget.org/packages/ThinkingHome.Migrator

[18] провайдером трансформации для нужной СУБД: https://www.nuget.org/packages?q=ThinkingHome.Migrator.Providers

[19] примером запуска: https://fluentmigrator.github.io/articles/quickstart.html?tabs=runner-in-process#running-your-first-migration

[20] Источник: https://habr.com/post/414867/?utm_campaign=414867