Pull to refresh

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

Reading time 7 min
Views 19K

Здравствуйте. Что такое миграции и зачем они нужны хорошо рассказано в статье Версионная миграция структуры базы данных: основные подходы.
Я же хочу вам рассказать о системе версионных миграций: 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: Хочу попросить вас высказаться о том, что еще вам интересно было бы узнать. Я обязательно напишу об этом.
Tags:
Hubs:
+13
Comments 12
Comments Comments 12

Articles