Pull to refresh

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

Reading time 6 min
Views 134K
Original author: Sven Müller
Не так давно мы начали внедрять 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 несколько надуманный, но тот же принцип может быть применен для более серьезных изменений данных.
На идею для этого поста я наткнулся во время работы над разделением существующего доменного класса (соответсвующего одной таблице) на три части: абстрактный базовый класс и два подкласса, с учетом необходимости сохранения целостности данных.
Tags:
Hubs:
+6
Comments 33
Comments Comments 33

Articles