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

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


    Пакеты опубликованы в 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, попробуйте новый мигратор. Скорее всего, вам тоже понравится.

    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 24
      +2
      Например, миграции Entity Framework почему-то обязательно должны быть в одной сборке с DbContext

      Это не совсем так. Вы можете указать другую сборку при настройке контекста. Например так:

      services.AddDbContextPool<Context>(options =>
      {
           options.UseNpgsql("connection string",
               builder => builder.MigrationsAssembly("some assembly"));
      });
      
        0
        О, спасибо! Почему-то раньше этого не нашел. Буду иметь в виду.
        +2
        Во FluentMigrator можно заменить реализацию IMigrationInformationLoader и IVersionLoader на свои…
          0
          Спасибо! В документации про это ничего нет, поэтому не нашел его. Попробую разобраться в коде, как будет время.
          0
          Мне, как пользователю, в миграции нужны следующие вещи:
          1) Создание автодифов по двум коннекшенам (ну т.е. я сам как угодно ворочу базой, а мне потом выдается готовый скрипт/dsl и т.д.).
          2) Reduce через несколько версий, например сейчас актуальная версия 4, но есть база с версией 2, мигратор это все видит и вычисляет шаги с 2 по 4, отбрасывая некоторые элементы (например, в версии 3 была удалена таблица, а в 4 я ее опять добавил и при миграции с 2 на 4 — мы этот шаг пропускаем, а не удаляем и создаем еще раз).

          К сожалению, такого нигде найти не смог, и поэтому все руками.
            0
            Выглядит, как будто вы хотите получать diff двух состояний БД. Это немного другой подход. Для этого даже не нужны номера версий.

            В простых случаях он позволяет делать руками меньше действий для обновления БД. В сложных случаях может потребоваться сложная магия. Такие инструменты тоже существуют (кажется в Visual Studio раньше была такая штука), но они, как правило, сложные, дорого стоят и них графический интерфейс (чтобы человек контролировал правильность того, что получилось).

            Версионная миграция БД — второй подход, в котором вы описываете инкрементальные изменения (т.е. изменения для перехода от текущей версии к следующей). Этот подход выигрывает в плане простоты и надежности. Его легко автоматизировать.
              +1
              С версионным подходом все ясно. Тут дело в другом, что все те миграторы, что я смотрел и использовал просили ручного труда. А мне бы хотелось, чтобы мигратор оперировал неким diff для перехода из одной версии к другой и все это вычислял сам, через две базы (одна production, другая develop). Есть такие diff утилиты (и я ими активно пользуюсь), но они на выходе дают голый SQL, поэтому я и пишу — «все руками».
                0
                Согласен такая отдельная утилита была бы очень полезна.
                Например, я в процессе ускорения работы базы. Добавляю прямо в нее индексы, новые колонки, таблицы. И тут ты понимаешь что все это надо повторить в миграции — я бы прыгал от счастья если бы мне сгенерили миграцию по diff, которую я бы ревьювнул и добавил в проект.
                  0
                  Пользоваться автогенерировангыми скриптами для миграции продакшен БД можно только если данные в ней не нужны, а вывести систему в даунтайм на час другой вообще не разу не проблема.
                    0
                    автогенерировангыми скриптами для миграции продакшен БД можно только если данные в ней не нужны

                    Это кто так сказал?
                    Или Вы за то, что писать весь SQL «руками»?
                      0
                      Это мне опыт говорит. Я за то чтобы думать когда применяешь к продакшен БД какие либо скрипты. Автогенеренные или «самописные» не важно. Не важно как именно вы их получили, от разработчиков которые их писали руками на каждый таск в jira или с помощью сравнения БД и текущего проекта БД каким нибудь инструментом типа SSDT.
                      Перед тем как применять их к продакшен их нужно проанализировать, возможно переписать какие-то части чтобы, 1) минимизировать вероятность потери данных, 2) сократить время миграции данных 3) исключить взаимоисключающие действия: добавили колонку, потом передумали и удалили колонку (в случае самописных скриптов) 3) сократить или исключишь даунтайм БД (иначе какой смысл в Green blue deployment?)
                0
                У MS этот инструмент называется SQL Server Data Tools
                  0
                  Пользовался, но там многие огрехи руками приходилось исправлять.
                0
                А бывают реально случаи, когда надо даунгрейдить базу? Это же обычно с потерей данных происходит, что звучит совсем критично.
                  0
                  это случае такого же масштаба, что и восстановление рухнувшей базы с последнего бэкапа. Т.е. очень желательно иметь процедуру реверса/отката, но не использовать ее на практике.
                    0
                    То, что не используется на практике, никогда не будет работать. Ну, формально будет, но криво, неожиданно и прочая.
                    +1
                    Да. Это происходит при слиянии веток где успели накопиться свои миграции.
                      0
                      Можете расписать подробнее?

                      Т.е. я пилил фичу А, со своими миграциями. В мастер залили фичу Б, со своими миграциями.

                      Я вмержил мастер себе — получил миграции Б, накатил их. Где даунгрейд?
                        0
                        Во-первых, миграции запросто могут конфликтовать. Во-вторых, некоторые миграторы нумеруют миграции — и в таком случае можно получить конфликт номеров даже при отсутствии других конфликтов.

                        В обоих случаях придется откатывать свои миграции перед мержем мастера.
                          0
                          Эм, откатывать, а не смержить миграции? Не понимаю всё равно.
                            0
                            Сначала откатить, потом смержить и накатить обратно.
                    0

                    Использую dotnet ef add migration с дополнительным инструментом сидирования при миграции (в момент накатывания миграции на базу можно исполнить любой код). Пока не подводил, единственно, что иногда нужно контролировать тот sql, который он сгенерировал.

                      0
                      В EF6 была очень удобная с точки зрения быстрой разработки прототипа или домашнего проекта концепция автоматических миграций. Достаточно было зарегистрировать мигратор при старте приложения или в веб.конфиге, например,
                      <databaseInitializer type="System.Data.Entity.MigrateDatabaseToLatestVersion`2[[GreatPlace.Data.GreatPlaceContext, GreatPlace.Data], [GreatPlace.Data.MigrationConfiguration, GreatPlace.Data]], EntityFramework" />

                      Я ей часто пользовался, это экономило кучу времени на начальных этапах разработки. К сожалению, для EF Core такого мигратора нет и не планируется (здесь обсуждение: github.com/aspnet/EntityFrameworkCore/issues/6214). Найти что-то стороннее тоже не получилось. Писать свое решение — сложно и долго. Может быть, вы сталкивались с чем-то похожим, или вашу библиотеку можно приспособить для этой задачи относительно просто?
                        0

                        При условии, что вы пишете seed’в есть такое «костыль»: делаете батник с миграцией init, пишете код, удаляете миграции, вызываете init и migrate. В каждый момент у вас одна миграция. Менее изящно, но на этапе прототипирования бывает удобно. Если pk надо заменить, например.

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

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