Хотя миграция схемы базы данных кажется довольно простой задачей изначально, задача становится сложнее после того, как появляется желание откатывать изменения схемы без ее создания заново.
Кроме схемы и операций 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» стал более сложным:
- я заново добавляю старый столбец NAME, используя стандартный тэг «addColumn»
- использую 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 несколько надуманный, но тот же принцип может быть применен для более серьезных изменений данных.
На идею для этого поста я наткнулся во время работы над разделением существующего доменного класса (соответсвующего одной таблице) на три части: абстрактный базовый класс и два подкласса, с учетом необходимости сохранения целостности данных.
ссылка на оригинал статьи http://habrahabr.ru/post/178665/
Добавить комментарий