Управление миграциями БД с Liquibase

http://blog.synyx.de/2012/08/migrating-data-with-liquibase/
  • Перевод
  • Tutorial
Не так давно мы начали внедрять Liquibase в качестве инструмента миграций схемы данных в большинстве наших проектов, новых и уже существующих. Система миграций схемы базы данных Liquibase хороша тем, что позволяет использовать системы контроля версий, VCS, (например, Git) для управления ревизиями базы данных приложения. Говоря более точно, VCS содержит описание изменений, необходимые для миграции схемы базы данных из одной ревизии в другую.

Хотя миграция схемы базы данных кажется довольно простой задачей изначально, задача становится сложнее после того, как появляется желание откатывать изменения схемы без ее создания заново.
Кроме схемы и операций DDL, Liquibase позволяет мигрировать данные приложения, с поддержкой наката изменений данных и их отката.

Давайте начнем с простого. Для примера, рассмотренного в этой статье, я буду использовать Liquibase, запускаемый из командной строки, а также простой CLI-клиент для MySQL.
Liquibase также хорошо интегрируется с Maven (как goal) или Spring (как бин, запускаемый во время инициализации контекста).

Начнем с очень простой таблицы PERSON, состоящей только из ID (первичный ключ) и имени:

mysql> describe Person; 
+-------+--------------+------+-----+---------+----------------+ 
| Field | Type         | Null | Key | Default | Extra          | 
+-------+--------------+------+-----+---------+----------------+ 
| id    | bigint(20)   | NO   | PRI | NULL    | auto_increment | 
| name  | varchar(255) | NO   | UNI | NULL    |                | 
+-------+--------------+------+-----+---------+----------------+ 
2 rows in set (0.00 sec)


Liquibase использует так называемые «чейнджсеты» (changeset — набор изменений), XML-код для описания операторов DDL. Они составляют файлы чейнджлогов (changelog). Следующий чейнджсет создаст таблицу (тэг «createTable») и два столбца (тэг «column»).

<databasechangelog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd">
  <changeset id="1" author="mueller@synyx.de" runonchange="true"> 
    <createtable tablename="Person"> 
      <column autoincrement="true" name="id" type="BIGINT"> 
        <constraints nullable="false" primarykey="true"> 
        </constraints>
      </column> 
      <column name="name" type="VARCHAR(255)"> 
        <constraints nullable="false"> 
        </constraints>
      </column> 
    </createtable> 
  </changeset>
</databasechangelog>


Используя этот XML-код, Liquibase добавит таблицу «Person». Команда, выполняющая это из интерфейса командной строки — «update»:

./liquibase --url=jdbc:mysql://localhost:3306/liquiblog --driver=com.mysql.jdbc.Driver --username=root --password="" --changeLogFile=db.changelog-0.1.0.xml update


Liquibase имеет встроенную поддержку отката некоторых типов чейнджсетов, к примеру «createTable». Если мы вызовем Liquibase через командную строку с аргументом «rollbackCount 1» вместо «update», произойдет откат последнего чейнджсета: таблица PERSON будет удалена.

Другие типы чейнджсетов не могут быть удалены автоматически. Для примера рассмотрим следующие чейнджсеты, добавляющие данные в PERSON (тэг «insert»):

<databasechangelog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd">
  <changeset id="init-1" author="mueller@synyx.de"> 
    <insert tablename="Person"> 
      <column name="name" value="John Doe"> 
      </column>
    </insert>
    <rollback> 
      DELETE FROM Person WHERE name LIKE 'John Doe'; 
    </rollback>
  </changeset>
</databasechangelog>


Я вручную добавил тэг «rollback», содержащий SQL-операторы, откатывающие изменения в этом чейнджсете. Этот тэг может содержать как SQL-операторы, так и обычные тэги Liquibase.
Так как теперь мы имеем два XML файла чейнджлогов, я создал «главный» файл, импортирующий другие файлы в порядке, необходимом для получения корректной ревизии БД:

<databasechangelog xmlns="http://www.liquibase.org/xml/ns/dbchangelog/1.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://www.liquibase.org/xml/ns/dbchangelog/1.9 
                      http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-1.9.xsd"> 
    <include file="db.changelog-0.1.0.xml"></include>
    <include file="db.changelog-0.1.0.init.xml"></include>
</databasechangelog> 


При вызове команды «update» для каждого из чейнджсетов происходит проверка, был ли он применен к схеме. Если чейнджсет еще не был применен, происходит его выполнение. Для этого Liquibase сохраняет данные во вспомогательной таблице DATABASECHANGELOGS, содержащей уже примененные чейнджсеты, а также их хэш-значения. Хэши используются для того, чтобы нельзя было изменить уже выполненные чейнджсеты.

mysql> select id, md5sum, description from DATABASECHANGELOG; 
+--------+------------------------------------+--------------+ 
| id     | md5sum                             | description  | 
+--------+------------------------------------+--------------+ 
| 1      | 3:5a36f447e90b35c3802cb6fe16cb12a7 | Create Table | 
| init-1 | 3:43c29e0011ebfcfd9cfbbb8450179a41 | Insert Row   | 
+--------+------------------------------------+--------------+ 
2 rows in set (0.00 sec)


Теперь, когда простой пример заработал, давайте попробуем что-нибудь посложнее: изменение схемы, требующее миграции схемы и обновления данных. Таблица PERSON в настоящий момент имеет только столбец имени NAME, и я хочу разделить NAME на два столбца — FIRSTNAME и LASTNAME. Перед началом миграция БД я собираюсь проставить Liquibase «тэг», чтобы можно было откатить все изменения к этому тэгу в дальнейшем:

./liquibase --url=jdbc:mysql://localhost:3306/liquiblog --driver=com.mysql.jdbc.Driver --username=root --password="" --changeLogFile=changelog-master.xml tag liquiblog_0_1_0


Я создал новый чейнджсет, добавляющий два новых столбца:

<changeset id="1" author="mueller@synyx.de" runonchange="true"> 
  <addcolumn tablename="Person"> 
    <column name="firstname" type="VARCHAR(255)"> 
      <constraints nullable="false"> 
      </constraints>
    </column> 
    <column name="lastname" type="VARCHAR(255)"> 
      <constraints nullable="false"> 
      </constraints>
    </column> 
  </addcolumn> 
</changeset>


И в этот раз Liquibase знает как откатить этот чейнджсет, так что мы можем не добавлять тэг «rollback».

Теперь таблица PERSON имеет два дополнительных столбца и мы должны позаботится о миграции уже существующих данных в новую схему перед тем, как удалим устаревшый столбец NAME. Так как манипуляция данным не поддерживается Liquibase «из коробки», мы должны использовать тэг «sql» для включения нативного SQL в чейнджсет.

<changeset author="mueller@synyx.de" id="2"> 
  <sql> 
    UPDATE Person SET firstname = SUBSTRING_INDEX(name, ' ', 1); 
    UPDATE Person SET lastname = SUBSTRING_INDEX(name, ' ', -1); 
  </sql> 
  <rollback> 
    UPDATE Person SET firstname = ''; 
    UPDATE Person SET lastname = ''; 
  </rollback> 
</changeset>


Следует учесть, что содержимое тэга «rollback» кажется излишним, но сам тэг необходим из-за того, что Liquibase позволяет откатывать только чейнджсеты:
  • которые неявно имеют тэг «rollback», например «createTable»
  • тэг «rollback» был добавлен явно


После запуска Liquibase с опцией «update», новый чейнджсет применяется к схеме: созданные столбцы FIRSTNAME и LASTNAME уже содержат данные.

Далее я хочу избавиться от старого столбца NAME.

<changeset id="3" author="mueller@synyx.de" runonchange="true"> 
  <dropcolumn tablename="Person" columnname="name"> 
  </dropcolumn>
  <rollback> 
    <addcolumn tablename="Person"> 
      <column name="name" type="VARCHAR(255)"> 
        <constraints nullable="false"> 
        </constraints>
      </column> 
    </addcolumn> 
    <sql> 
      UPDATE Person SET name = CONCAT(firstname, CONCAT(' ', lastname)); 
   </sql> 
  </rollback>
</changeset>


Сам чейнджсет довольно прост, так как Liquibase поддерживает удаление столбцов, но тэг «rollback» стал более сложным:
  1. я заново добавляю старый столбец NAME, используя стандартный тэг «addColumn»
  2. использую SQL-оператор для восстановления данных в этом столбце


Результат преобразований:

mysql> select * from Person; 
+----+-----------+------------+ 
| id | firstname | lastname   | 
+----+-----------+------------+ 
|  1 | John      | Doe        | 
+----+-----------+------------+ 
1 rows in set (0.00 sec) 


В связи с тем, что мы первоначально пометили схему тэгом, а также добавили инструкции для отката изменений во всех наших чейнджсетах, мы можем откатить модификации схемы БД без потери данных! Вызывая…

./liquibase --url=jdbc:mysql://localhost:3306/liquiblog --driver=com.mysql.jdbc.Driver --username=root --password="" --changeLogFile=changelog-master.xml rollback liquiblog_0_1_0


… мы вернулись к опигинальному состоянию схемы БД!

Пример с разделением/слиянием строк имени PERSON несколько надуманный, но тот же принцип может быть применен для более серьезных изменений данных.
На идею для этого поста я наткнулся во время работы над разделением существующего доменного класса (соответсвующего одной таблице) на три части: абстрактный базовый класс и два подкласса, с учетом необходимости сохранения целостности данных.
Поделиться публикацией

Похожие публикации

Комментарии 33

    0
    South для django работает примерно так-же, только миграции хранятся не в нечитабельном богопротивном XML а в питоньих файлах, что достаточно удобно. Ну и детект изменений есть.
      0
      А до этого я юзал какой-то SQL Diff на джаве кем-то из яндекса нарытый где-то на просторах интернета.
      Делал 2 sql в одну и в обратную сторону и ручками их накатывал.
      Тоже опыт хороший был.
        0
        только миграции хранятся не в нечитабельном богопротивном XML а в питоньих файлах

        Ничего что из-за этого эту утилиту можно спокойно использовать в рамках любого проекта, а South только в рамках django? Опять же всегда интересовало что же такого нечетабельного и богопротивного в XML. Особенно учитывая наличие возможности автоматической валидации данных как на выходе так и на входе.
          0
          Нене, я не говорю что утилита не нужна — просто делюсь опытом. Сравнение различных решений всегда полезно.
          А XML нечИтабелен своей избыточностью. Я в последнее время по возможности использую yaml — выглядит гораздо удобнее.
            0
            Нене, я не говорю что утилита не нужна — просто делюсь опытом. Сравнение различных решений всегда полезно.

            База может обладать существенно большим временем жизни. И я бы лучше держал изменения и проектирование базы отдельно от приложения.

            XML нечИтабелен своей избыточностью. Я в последнее время по возможности использую yaml — выглядит гораздо удобнее.

            А вы его не читайте, это все же формат для машинного обмена данными, а не для чтения человеком. У yaml я не припомню автоматической валидации. Так что если пришел мусор, то отсечь его не получится.

            Опять же для Liquibase есть плагин к эклипсу который упрощает жизнь.
              0
              База может обладать существенно большим временем жизни. И я бы лучше держал изменения и проектирование базы отдельно от приложения.

              Зачем? Ченжлог гораздо нагляднее, причем это довольно странно соотносится с вашим «А вы его не читайте».

              У yaml я не припомню автоматической валидации. Так что если пришел мусор, то отсечь его не получится.

              Вы сейчас про формат или про структуру?
                0
                Зачем? Ченжлог гораздо нагляднее, причем это довольно странно соотносится с вашим «А вы его не читайте».

                А затем, что лучше сначала спроектировать СУБД, а уже заниматься реализацией в приложении. Какой именно ченжлог?

                Вы сейчас про формат или про структуру?

                Про структуру.
              0
              Судя по сайту, Liquibase поддерживает yaml а еще и json.
            0
            XML хорош тем, что он позволяет
            1. Автоматически валидировать по схеме
            2. Автодополнение в IDE на основе, опять же, схемы данных.

            После этого топика опубликую Tips & Tricks liquibase, и использование XML, а не DSL там — одна из хороших практик.
              0
              Ну ямл или жсон тоже можно по схеме валидировать.
              www.kuwata-lab.com/kwalify/

              За автодополнение не скажу — привычки программировать на XML не имею :)
                0
                На стандарт это мало похоже. А у xml это дело стандартизировано.
                  0
                  Вот именно для этого автодополнение и нужно! Для ленивых, не имеющих привычки программистов (типа меня)
                  Я не хочу лезть в документацию, наоборот, я встаю внутрь любого тэга, жму, Ctrl+Space и вижу какие элементы мне разрешены в этом месте!
                    0
                    Эта хернь поддерживает xml, yaml, json и plain sql. Не нужно так ругать ее, это шикарная штука, если бы не была написана на java =) Это ограничивает круг пользователей.
                      0
                      На чем-то ее же надо было создателям написать) Да чтоб еще и работало легко на всех платформах.

                      Какую современную утилиту не возьми, мне приходится устанавливать или perl, или python, или node.js, или ruby, или java…
                      В итоге, я просто поставил однажды все это и больше у меня голова не болит.
                        0
                        Вам как человеку связанному с java, проблем эта платформа не придает, а лишь добавляет удобств, а мне как человеку у которого по нужде уже стоит питон и руби, ставить лишнюю платформу как минимум дискомфортно =)
                0
                Слышал от коллег, что на каждое изменение таблицы он выполняет отдельный запрос. Т.е. если добавляем две колонки и liquibase выполнит два alter запроса. Это так?
                  0
                  Я не проверял такие подробности. Это имеет значение?
                  Можно включить логгирование всех запросов к базе и увидеть, что и как, если очень надо будет.
                    0
                    Конечно имеет. Если таблица большая и весит несколько гигов, то делать два запроса вместо одного — это жесть :)
                      0
                      Если таблица большая и весит несколько гигов, и вы работаете с СУБД, которая не умеет моментально добавлять nullable стольбы или удалять их без перелопачивания всей таблицы и остановки работы (mysql — как раз не умеет), то делать любой альтер, даже одиночный, — это жесть. :)
                        0
                        А как решается эта проблема в таких СУБД?
                          0
                          Сменой СУБД? А как еще?
                            0
                            Менять слой СУБД для миграций? Довольно странно и невыполнимо для большинства проектов.
                            Предполагаю, здесь возможны варианты типа создания таблицы рядом, с данными и нужной колонкой, а потом переименованием… Но это довольно грязный хак.
                  +1
                  А вы не пробовали/сравнивали с FlyWay?
                    0
                    Не пробовал FlyWay, ничего о нем еще не слышал…
                    Liquibase довольно распространен в Java-мире, поэтому я с ним знаком.
                      +1
                      FlyWay тоже, как ни странно, распространён в джава мире, даже плагин для мавена есть.
                      Более того, в случае постгреса он поддерживает нативные функции которые liquibase не поддерживает.
                      0
                      Мы сравнивали когда то. Liqubase выглядит универсальным комбайнов по сравнению с FlyWay. У Liqubase больше возможностей. Решающим фактором для нас оказалось то, что в Liqubase можно писать changesets не зависящие от базы данных.
                      0
                      Бесит что несмотря на наличие Rollback-а нельзя просто откатить changeset. Ибо надо делать тег, либо откатываться назад по истории. Непродумано короче. Но лучше вроде как ничего нет.
                        0
                        А она может генерить из существующей базы файлы с чейнджсетами/чейнджлогами? И как это сделать если да? Документация у него не ахти, опций куча, я пытался что-то хоть сделать, но увы…
                          0
                          Может. С доками у библиотеки все нормально.

                          www.liquibase.org/documentation/generating_changelogs.html
                            0
                            Да, я видел, моя ошибка была в том что я пытался использовать json =) Сходу оно так просто не работает)
                              0
                              Кстати, раз уж пошло такое дело еще спрошу один вопрос =) А может ли она генерить разницу между файлом с миграцией и текущим состоянием бд, ато я нашел лишь генерацию текущего состояния базы, а мне нужна именно разница между предидущей ревизией.
                                  0
                                  Это я тоже видел, и даже пытался использовать, но разве он не выдает разницу между двумя подключениями? Я хотел именно между текущим соединением и ченджсетами в вайле :(

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

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