Flyway: управление миграциями баз данных

    В этой статье я расскажу об одном из средств обеспечения версионности схем и управления миграциями БД — библиотеке Flyway. С поблемой версионности схемы базы данных рано или поздно приходится сталкиваться разработчикам любого приложения, опирающегося на СУБД. Увы, иногда эта проблема принимается в рассмотрение слишком поздно — например, если вопрос о внесении изменений в структуру базы встаёт, когда приложение уже находится в эксплуатации. Но и на этапе разработки контроль схемы базы данных причиняет не меньше проблем, чем все прочие аспекты версионности приложения: в отсутствие чёткой системы управления миграциями локальная, стендовая и эксплуатационная базы могут быстро «разъехаться», не предоставляя при этом никакой информации относительно своего текущего состояния.


    Перзистенс-провайдеры штатно позволяют лишь в том или ином виде экспортировать актуальную объектную модель в виде схемы базы данных. Этот процесс может быть выполнен в режиме пересоздания (с полным удалением всей структуры), обновления (с внесением изменений) или сверки (без внесения изменений). Например, в Hibernate это делается с помощью инструмента hbm2ddl, работа которого может быть настроена единственным конфигурационным параметром в файле hibernate.cfg.xml или persistence.xml. Однако пересоздание (режим create) бывает нежелательным, если в базе уже есть данные, а обновление (режим update) вносит не все изменения, а только недеструктивные (например, не удаляются столбцы и таблицы) и не учитывает требующуюся реструктуризацию данных. Зачастую, если модель данных претерпела множество изменений, применить их к эксплуатационной базе бывает непросто, особенно если текущая версия базы неизвестна. Так или иначе, но приходится «опускаться» до SQL-скриптов — тут-то и встаёт вопрос управления версионностью.

    Flyway


    На главной странице проекта приведена наглядная таблица сравнения библиотеки с аналогичными решениями, и здесь основное внимание хочется обратить на богатую функциональность, работу с миграциями в виде простых SQL-файлов или Java-классов (последние по сути основываются на Spring JDBC Template) и поддержку нативного SQL популярных СУБД (Oracle PL/SQL, SQL Server T/SQL, хранимые процедуры MySQL и PostgreSQL).

    Flyway хорошо интегрируется с Ant, Maven и инструментами командной строки, имеет API для программного вызова и интеграцию со Spring, работает со множеством СУБД. Я приведу пример подключения Flyway к уже существующему проекту, сборка которого основывается на Maven, а вызов Flyway производится при старте контекста Spring. В качестве базы данных в проекте используется MySQL.

    Подключение Flyway к проекту


    Для начала создадим папку db/migration в подкаталоге src/main/resources проекта: в ней будут храниться скрипты миграции. Поместим туда предварительно экспортированный скрипт базы данных — со всеми таблицами, представлениями, индексами и т.д. Назовём файл V1__Base_version.sql. Подробно соглашения по именованию миграций описаны в документации, пока достаточно сказать, что имя файла начинается с V, далее следует номер версии (с произвольным количеством точек-разделителей), двукратный символ подчёркивания и описание миграции.

    Добавим в зависимости проекта (раздел dependencies) ядро библиотеки Flyway:

    <dependency>
        <groupId>com.googlecode.flyway</groupId>
        <artifactId>flyway-core</artifactId>
        <version>1.5</version>
    </dependency>
    

    А в сборочные плагины (раздел build/plugins) — плагин Flyway:

    <plugin>
        <groupId>com.googlecode.flyway</groupId>
        <artifactId>flyway-maven-plugin</artifactId>
        <version>1.5</version>
        <configuration>
            <driver>com.mysql.jdbc.Driver</driver>
            <url>jdbc:mysql://localhost:3306/flywaytest?autoReconnect=true&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;connectionCollation=utf8_general_ci&amp;characterSetResults=UTF-8</url>
            <baseDir>db/migration</baseDir>
        </configuration>
    </plugin>
    

    Для запуска Flyway через плагин лучше создать отдельную учётную запись в базе. Можно указать пользователя и пароль для подключения к базе здесь же, в конфигурации плагина:

    <configuration>
        <user>flyway</user>
        <password>mySecretPassword</password>
        ...
    </configuration>
    

    Или в параметрах командной строки:

    -Dflyway.user=flyway -Dflyway.password=mySecretPwd
    

    Но более удобным способом, в случае сборки на Maven, будет помещение типовых параметров в файл настроек Maven (файл settings.xml) и дальнейшее использование их во всех аналогичных проектах:

    <servers>
        <server>
          <id>flyway-db</id>
          <username>flyway</username>
          <password>mySecretPassword</password>
        </server>
    </servers>
    

    Если необходимо инициализировать текущую базу с нуля, то можно выполнить её очистку. При этом всё содержимое базы будет удалено:

    mvn flyway:clean
    

    При успешном выполнении задачи база окажется пустой, а в логе Maven появятся следующие строки:

    [INFO] --- flyway-maven-plugin:1.5:clean (default-cli) @ flyway-test-project ---
    [INFO] Cleaned database schema 'flywaytest' (execution time 00:03.911s)
    

    Если же база находится в актуальном состоянии (соответствует выгруженному ранее скрипту), необходимо выполнить задачу, которая создаст в ней необходимую для поддержания версионности структуру:

    mvn flyway:init -Dflyway.initialVersion=1 -Dflyway.initialDescription="Base version"
    

    Далее можно убедиться, что в базе появилась таблица schema_version с единственной записью, соответствующей текущему состоянию базы:



    Интеграцию Flyway с приложением выполним в виде бина Spring, стартующего перед entityManagerFactory:

    <bean id="flyway" class="com.googlecode.flyway.core.Flyway" init-method="migrate">
        <property name="dataSource" ref="..."/>
        ...
    </bean>
    <!-- Ставим фабрику менеджеров сущностей в зависимость от Flyway, чтобы убедиться, что она будет выполнена после внесения изменений в базу -->
    <bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" id="entityManagerFactory" depends-on="flyway">
        ...
    </bean>
    

    После запуска приложения на чистой базе она будет инициализирована скриптом V1__Base_version.sql, кроме того, будет создана таблица schema_version. В логе при этом можно наблюдать следующее:

    2012-04-04 06:42:09,279 INFO [com.googlecode.flyway.core.metadatatable.MetaDataTable] -- <Metadata table created: schema_version (Schema: flywaytest)>
    2012-04-04 06:42:09,318 INFO [com.googlecode.flyway.core.migration.DbMigrator] -- <Current schema version: null>
    2012-04-04 06:42:09,320 INFO [com.googlecode.flyway.core.migration.DbMigrator] -- <Migrating to version 1>
    2012-04-04 06:42:24,897 INFO [com.googlecode.flyway.core.migration.DbMigrator] -- <Successfully applied 1 migration (execution time 00:15.615s).>
    

    Если же приложение было запущено на базе, идентичной последней миграции, то никаких изменений в схеме не произойдёт, что будет отражено в логе приложения следующими строками:

    2012-04-04 06:36:14,081 INFO [com.googlecode.flyway.core.migration.DbMigrator] -- <Current schema version: 1>
    2012-04-04 06:36:14,085 INFO [com.googlecode.flyway.core.migration.DbMigrator] -- <Schema is up to date. No migration necessary.>
    

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

    Создание миграции


    Создадим в папке db/migration файл с названием V2__Test_change.sql и со следующим содержимым:
    create table test_table (
      id bigint(20) not null,
      primary key(id)
    );
    

    После запуска приложения обнаружим в логе следующие строки:

    2012-04-04 06:51:02,708 INFO [com.googlecode.flyway.core.migration.DbMigrator] -- <Current schema version: 1>
    2012-04-04 06:51:02,710 INFO [com.googlecode.flyway.core.migration.DbMigrator] -- <Migrating to version 2>
    2012-04-04 06:51:03,137 INFO [com.googlecode.flyway.core.migration.DbMigrator] -- <Successfully applied 1 migration (execution time 00:00.480s).>
    

    И убедимся, что таблица test_table была успешно создана, а в таблице schema_version появилась запись о применённой миграции:



    Откат миграции


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

    Share post

    Comments 15

      0
      В ряде случаев (да, далеко не во всех) откат изменений вполне возможен. Поэтому, на мой взгляд, разработчики зря отказались от этой фичи, которая лично меня в dbdeploy время от времени выручает. Откаты БД, конечно же, автоматом никто не делает, но порой иметь на руках готовый для этого скрипт — архиполезно!
        +3
        Статья будет неполной без упоминания ближайшего аналога — пакета liquibase, который позволяет описывать структуру объектов БД в виде XML-описания и, как следствие, позволяет делать откаты и многое другое.

        www.liquibase.org/

          0
          Я решил ограничиться упоминанием сравнительной таблички различных решений (в том числе LiquiBase) на странице Flyway. От LiquiBase меня отпугнуло именно XML-описание изменений. Когда идёт речь о миграциях, то хочется быть «ближе к телу», т.е. к SQL. Меньше будет turnaround при написании и отладке скрипта, да и опыт показывает, что случается необходимость быстро накатить изменения скриптом, минуя систему миграции.
          Кроме того, не вполне понимаю, как XML-формат связан с возможностью отката миграций. В случае Flyway это вполне обоснованное техническое решение by design, т.к. ничего не стоило бы включать в пакет изменений SQL-скрипт, восстанавливающий базу.
            0
            Тем, что в XML описывается суть изменений в виде мета-описания, а не сам SQL-скрипт, отягощенный подробностями реализации конкретной СУБД. Это позволяет описать изменение один раз — вместо двух SQL-скриптов: на создание изменений и на их откат (что увеличивает риск ошибки вдвое).

            Но, если это необходимо Liquibase позволяет внедрить в описание и обычный SQL-скрипт.

            И в XML нет ничего страшного.
              0
              Я не имею ничего принципиального против XML. Но в том-то и дело, что при достаточно сложных изменениях (а они никогда не бывают простыми, особенно на непустой базе, когда требуется определить логику реструктуризации данных) без SQL-скрипта не обойтись. Стоит ли городить ради этого XML, если всё равно придётся предусматривать SQL-вкрапления.
          0
          Странно, но практически ни один инструмент миграции не позволяет делать следующее: Залезть ручками в БД, поменять все так, как нужно, а затем, перед коммитом, сгенерить скрипт различий со старой структурой. ПОЧЕМУ??? Я знаю только пару инструментов, позволяющих такое безобразие…
            0
            Думаю, это связано с тем, что эта задача достаточно сложная, и в общем виде нерешаемая, особенно когда речь заходит об изменениях в непустой базе.
            Например, буквально вчера мне понадобилось грохнуть справочник и подставить вместо него в ссылающуюся таблицу обычное текстовое поле, взятое из поля справочника. Ни один инструмент анализа ни за что в жизни не «догадается», что это изменение было произведено именно так, а не как-то иначе, и, таким образом, сгенерированный им скрипт будет не работоспособен на базе с другим наполнением.
              0
              Ну проблема-то в общем решаемая, только пользователям выполняющим генерацию миграций требуются права на создание временной базы.
                +1
                Я имел ввиду, что она не решаема в общем виде автоматически средствами генерации миграций, без ручного написания дополнительного кода.
                Речь ведь не только о том, чтобы сгенерировать скрипт различий, но и чтобы он корректно отрабатывал на любой базе, неважно, какие данные в ней уже есть. Простейший пример:

                Было:
                create table person (
                  id bigint(20) not null,
                  post_id bigint(20) not null,
                  primary key(id),
                  foreign key (fk_post) references post(id)
                );
                
                create table post (
                  id bigint(20) not null,
                  description varchar(20),
                  primary key(id)
                );
                

                Стало:
                create table person (
                  id bigint(20) not null,
                  post_description varchar(20),
                  primary key(id)
                );
                

                В поле human.postDescription я переместил данные из поля post.description.

                Написать скрипт, производящий такие изменения на существующей непустой базе — несложно. Сгенерировать такой скрипт автоматически на основании анализа двух схем — невозможно.
                  0
                  С этим согласен, но это частный случай (в моей практике). У меня обычно добавляются поля и таблицы. При этом мне не удобно писать где-то код миграции, а затем запускать ее. Я люблю покопаться через консольный клиент MySQL, может быть даже несколько раз с перерывом в час-два, а затем, перед коммитом сгенерить миграцию.
              +1
              А какие инструменты вы знаете?
              Проблема есть, сам мучаюсь, но использую MySQL Workbench заставляя ее сравнивать структуры БД и находя различия.

          Only users with full accounts can post comments. Log in, please.