FluentMigrator — система версионных миграций


Здравствуйте. Что такое миграции и зачем они нужны хорошо рассказано в статье Версионная миграция структуры базы данных: основные подходы.
Я же хочу вам рассказать о системе версионных миграций: FluentMigrator. Почему мне нравится именно этот проект? Из-за приятного синтаксиса миграций и поддержки различных СУБД. Заинтересовались? Добро пожаловать под кат.

Содержание статьи


О проекте
Кратко о возможностях
Составные части системы
Как это работает
Тонкости
Заключение

О проекте


Сам проект базируется на GitHub: github.com/schambers/fluentmigrator
Мой форк: github.com/tabushi/fluentmigrator
История изменений репозитория начинается с 17.12.2008
Распространяется под лицензией: Apache License 2.0
Написано на C# под .Net Framework 3.5

Кратко о возможностях


Поддерживаемые СУБД
  • Jet
  • MySQL
  • Oracle
  • PostgreSQL
  • SQLite
  • Microsoft SQL Server
Поддерживаемые операции
  • Создание, удаление таблиц
  • Добавление, удаление, модификация колонок
  • Создание, удаление первичных, внешних ключей, индексов
  • Вставка, обновление, удаление данных (довольно скромные по возможностям)
  • Проверка существования в базе данных схем, таблиц, колонок, индексов
  • Выполнение произвольного sql, sql скрипта из ресурса или из внешнего файла (когда всего предыдущего не хватает)
Составные части системы
  • FluentMigrator — собственно ядро программы, в нем реализован весь синтаксис миграций
  • FluentMigrator.Console — консольно приложение для запуска миграций, имеет большое количество параметров
  • FluentMigrator.MSBuild — запуск миграций для MSBuild
  • FluentMigrator.NAnt — запуск миграций для NAnt
  • FluentMigrator.Runner — ядро программ установки скриптов, является основой для предыдущих трех проектов, которые по сути являются мелкими оболочками в 1 .cs файл. Содержит в себе всю логику по выполнению миграций и преобразованию миграций в sql под требуемую СУБД. Используя его очень легко написать свой runner с блекджеком с красивыми окошками и требуемым функционалом.
  • FluentMigrator.SchemaDump — мной лично не пользовался, но специально для этой статьи я его просмотрел. Он предназначен для сохранения структуры БД в sql файл. Реализован только для MS SQL Server
  • FluentMigrator.Tests — тесты, куда ж без них. Использует NUnit

Как это работает


А теперь давайте поподробнее остановимся на файле миграций. Итак, файл миграций может содержать в себе следующее:
  • миграции
  • профайлы
  • описание таблицы версий
  • sql скрипты как внедренные ресурсы
  • возможно еще что-то, с чем я еще не сталкивался
Подробнее об этих сущностях

Миграцию рассмотрим на следующем примере:
using System;
using FluentMigrator;

namespace ExampleDatabaseMigrations
{
  [Migration(2011091900)]
  public class ExampleMigration : Migration
  {
    public override void Up()
    {
      if (!Schema.Table("EXAMPLE_TABLE").Exists())
      {
        Create.Table("EXAMPLE_TABLE")
          .WithColumn("ID").AsInt16().NotNullable().PrimaryKey("PK_EXAMTABL_ID")
          .WithColumn("NAME").AsAnsiString(100).NotNullable()
          .WithColumn("SHORT_NAME").AsAnsiString(50).Nullable()
          .WithColumn("START_DATE").AsDate().NotNullable()
          .WithColumn("END_DATE").AsDate().Nullable();
        Insert.IntoTable("IDX_EXAMTABL_NAME")
          .Row(new {Id = 1, Name = "TEST", Start_Date = new DateTime(2011, 9, 19)});
      }
      if (!Schema.Table("EXAMPLE_TABLE").Index("IDX_EXAMTABL_NAME").Exists())
      {
        Create.Index("IDX_EXAMTABL_NAME")
          .OnTable("EXAMPLE_TABLE")
          .OnColumn("NAME").Ascending();
      }
      if (!Schema.Table("EXAMPLE_TABLE").Index("IDX_EXAMTABL_STARDATE_ENDDATE").Exists())
      {
        Create.Index("IDX_EXAMTABL_STARDATE_ENDDATE")
          .OnTable("EXAMPLE_TABLE")
          .OnColumn("START_DATE").Ascending()
          .OnColumn("END_DATE").Ascending();
      }
    }

    public override void Down()
    {
      if (Schema.Table("EXAMPLE_TABLE").Exists())
        Delete.Table("EXAMPLE_TABLE");
    }
  }
}


* This source code was highlighted with Source Code Highlighter.

В примере я не указывал нигде схему создания таблицы\индексов, но такая возможность конечно же есть.
Нумерация миграций должна идти по возрастанию, что логично. Но просто по порядку нумеровать нам показалось не удобным, поэтому была принята нумерация виду yyyyMMddxx, где: yyyy — год, MM — месяц, dd — день, xx — номер по порядку от 00 до 99. Таким образом и порядок соблюдается и нумерация говорит подробнее о времени создания миграции.
Данный пример дает приблизительное представление о том, как выглядит миграция, на этом и остановимся.

Шаблон профайла

using FluentMigrator;

namespace ExampleDatabaseMigrations
{
  [Profile("Example")]
  public class ExampleProfile : Migration
  {
    public override void Up()
    {
      //do something
    }

    public override void Down()
    {
      //empty, not used
    }
  }
}


* This source code was highlighted with Source Code Highlighter.

Как вы видите, профайл так же наследуется от класса Migration, но помечается атрибутом Profile. В профайле выполняется только метод Up (хотя в примере в документации на github'е метод Down тоже заполнен, может быть когда-то он тоже выполнялся). Файл миграций может содержать неограниченное количество профайлов с разными именами.

Чем же отличается профайл от миграции кроме атрибутов? Тем, что:
  1. профайл выполняется только если это явно задать, указав его имя при выполнении миграций;
  2. профайл выполняется всегда после успешного выполнения миграций, если из миграций устанавливать было нечего, то профайл все равно выполняется.
Таким образом профайл можно использовать для каких-либо сервисных функций. Сбора статистики или еще чего. Мы его используем для запуска процедур перекомпиляции инвалидных объектов БД, ставших таковыми из-за внесения изменений в структуру.

Совсем недавно на гитхабе была просьба по добавлению гибрида профайла и миграций — профайлы, которые имеют номер миграции. Дело в том, что человек использовал профайл для заливки тестовых данных в БД разработчиков когда это необходимо, а так как структура БД менялась при добавлении миграций, то ему приходилось каждый раз менять профайл. Это пример неудачного применения профайла. Ему посоветовали использовать для этого миграции, внутри которых через if проверять некое условие, например переменную окружения, т.к. параметры командной строки в миграции, естественно, не доступны.

Таблица версий

using FluentMigrator.VersionTableInfo;

namespace ExampleDatabaseMigrations
{
  [VersionTableMetaData]
  public class ExampleVersionInfo : IVersionTableMetaData
  {
    public string SchemaName { get { return "EXAMPLE_SCHEMA"; } }
    public string TableName { get { return "EXAMPLE_VERSION_TABLE"; } }
    public string ColumnName { get { return "EXAMPLE_VERSION_COLUMN"; } }
  }
}


* This source code was highlighted with Source Code Highlighter.

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

Внедренные скрипты

Внедренные скрипты — обычные sql скрипты, файлы которых подключены к проекту, и имеющие свойство «Build Action»=«Embedded Resource». Больше о них говорить, я думаю, и не надо.

Как это работает


Далее, предположим, что мы выполняем миграции вверх, т.е. устанавливаем последнюю версию структуры БД.

Алгоритм работы
  1. Программе (Runner) передаются параметры, содержащие в себе, если кратко, данные о БД, файле миграций и само задание (на самом деле параметров много, и то, что я указал выше задается более чем тремя параметрами).
  2. Runner подключается к БД и выбирает максимальный номер в таблице версий, просматривает файл миграций и выполняет миграции.
  3. В случае ошибки runner делает откат миграции, на которой запнулся (за некоторым исключением, о котором позже в разделе тонкости).
Выполнение одной миграции

Выполнение миграции делится на 2 этапа
  1. Выполнение метода Up() (или Down(), в случае установки миграций вниз), который добавляет выражения (expressions) в список выражений
  2. Преобразование выражений в sql и отправка на выполнение в БД

Тонкости


Откат изменений производится не всегда, так как не все СУБД поддерживают откат DDL операций. Для таких СУБД миграции желательно делать как можно более мелкими, из одной операции. Если эта одна операция не установится, то ее и не нужно будет откатывать.

Похоже, что изначально проект задумывался для написания миграций единожды под все поддерживаемые СУБД (и для основных простых операций это работает). Но все же стоит признаться, что возможности и синтаксис у СУБД сильно отличается (например, в одних СУБД используются автоинкрементные поля, а в других — сиквенсы), поэтому с недавнего времени внутри миграций стала доступна функция с говорящим названием IfDatabase.

Заключение


Проект опенсорсный, поэтому не стоит ожидать от него чудес. Проверяйте что получилось на требуемых СУБД, так как может случится такое, что что-то не реализовано для данной СУБД. Описывайте баги — исправим, или же сами помогите их исправить.

PS: Хочу попросить вас высказаться о том, что еще вам интересно было бы узнать. Я обязательно напишу об этом.
Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 12
  • +2
    Когда ж уже народ угомонится со всем этим Fluent'ом. API ж совершенно непродираемый получается.

        public class V1 : MigrationDefinition
        {
            public override Migration Up()
            {
                return new Migration {
                    new Table("example_table") {
                        { "id", DbType.Int16, NotNull, PrimaryKey },
                        { "name", DbType.AnsiString, 100, Null },
                    },
                    new Index("example_table", Unique) {
                        { "name", Asc }        
                    }
                };
            }
        }
    • +3
      К чему вы здесь привели свой код. Вы считаете что он чем-то лучше?
      И как между собой связаны Fluent и «непродираемый» API?
      • 0
        Да, я считаю, что он лучше: там меньше «церемоний» и больше дела.

        А непродираемость заключается во всех этих точках: Create.Table.WithColumn.AsInt64.Nullable.PrimaryKey.

        FI хорош в меру — от силы пара-тройка вызовов.
        • +2
          Вы меня удивляете. С этой точки зрения ваши запятые между параметрами ничем не лучше.
          Код миграции в FluentMigrator'е читается не чуть не сложнее чем в Вашем. К тому же Ваш проект из серьезных СУБД поддерживает только SQL Server. Если добавите поддержку Oracle (который больше всего меня интересует) и еще парочки СУБД, тогда можно будет сравнивать удобство API.
    • 0
      На одном из предыдущих проектов мы сделали утилитку. Утилитка делала следующее:

      — брала папку «diffs», находящуюся под SVN, вынимала оттуда все файлы с ревизиями, на которых был создан файл (не изменен, это важно)

      — лезла в базу, в спец. таблицу, и смотрела последнюю ревизию, на которую были успешно выполнены скрипты

      — прогоняла все скрипты с этой ревизии. Если какой-то скрипт давал сбой — все останавливалось. Это давало возможность поправить скрипт не изменяя порядка

      — затем из базы сносились все хранимки, вьюхи, функции и все такое

      — из других папок брались скрипты для создания хранимок, вьюх и прочего. Если прогон скрипта давал ошибку «нет зависимого объекта», скрипт ставился в начало очереди. Так мы разрулили связи между объектами (если какая-нибудь хранимка использует вьюху или другую хранимку, например)

      Скрипты были обычные SQL-ки, плюс была возможность заливать данные из CSV, это юзалось для справочников.

      Я это все к чему. Такая система нужна, безусловно. Но все что делается на этом поприще — не в тему вообще. Вместо того, чтобы решать всякие нужные вещи (апдейт хранимок тот же, привязка к SVN и прочее), все зачем-то придумывают замену DML. Чем вам DML-то не угодил?
      • 0
        Кстати, в случае MSSQL, diff-ы на DML делаются в два счета. Открываем схему базы в Management Studio, меняем там что нужно (добавляем поля, таблицы, связи, индексы и т.п.), и потом сверху давим кнопку «generate script». Все.

        И еще — зачем нужны скрипты обратной миграции? Если в базе есть данные, то в общем виде их никак не сделать. Если данных нет — проще все с нуля прогнать от начала.
        • 0
          Генерировать различия нам, к сожалению, не подходит по двум причинам:
          1. Около 100 клиентов, у каждого свой независимый сервер. Черт его знает что там творится со структурой и данными.
          2. Не до всех серверов еще и доступ есть. Некоторые в такой глуши находятся, что особо к ним не наездишься (стоимость поддержки не окупит таких путешествий).

          Я нигде не говорил, что не люблю DML. Более того, у FluentMigrator'а очень скромные возможности DML.
          Если кто-то любит чистый SQL (PL/SQL, T-SQL и т.д.), то FluentMigrator позволяет выполнять прикрепленные скрипты.
          • 0
            Скрипты обратной миграции больше нужны для разработчиков и DBA, например чтобы можно было развернуть эталоную структуру и сгенерировать diff'ы во время плановых сервисных работ с сервером.
            Клиенту миграции назад не нужны. Именно поэтому мой самописный гуевый runner умеет устанавливать миграции только вперед.
            • 0
              Ну почему же. Клиентам вполне может быть нужен откат на предыдущую версию, к примеру, если в новой версии уже после выката в продакшн обнаружен критический баг, который не даёт приложению нормально работать.
              • 0
                Кому-то — вполне возможно. Но нашим клиентам я такой возможности не дам, потому как многие из них не обременены знаниями в IT.
        • 0
          Для своего фреймворка я реализовал похожий мигратор, основанный на fluent-интерфейсе. Для Java есть достаточно известный LiquiBase, но он основан на XML.
          • 0
            Для Java значит. Спасибо, запомню.

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

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