В предыдущей части мы разобрались с основными возможностями Liquibase и написали базовый пример Spring boot приложения, который использует Liquibase для инициализации БД. Полный код базового приложения можно увидеть тут на GitHub. В этой статье мы поговорим про liquibase-maven-plugin и те дополнительные возможности, которые он нам дает для версионирования структуры БД. Начнем с того, как автоматически создавать скрипты при помощи функции сравнения.
Предположим, что нам понадобилось внести какие-либо изменения в структуру нашей БД. Например, мы хотим, чтобы email не мог быть null. Безусловно, для такого маленького изменения можно было бы подкорректировать код и скрипты вручную, но как быть если изменений будет больше? В этом случае к нам на помощь придет встроенная в Liquibase возможность сравнения БД. Интересной её особенностью является то, что сравнивать можно не только две базы данных, но и базу данных с набором JPA сущностей в нашем приложении. Именно так мы сейчас и поступим!
В раздел plugins файла pom.xml добавляем вот такую довольно сложную конструкцию. Это liquibase-maven-plugin, к которому подключена зависимость для анализ hibernate-сущностей и работы с файлами в формате YAML. Плагин поможет нам автоматически генерировать liquibase-скрипты через сравнение структур в двух БД или даже через сравнение структуры данных в БД и набора hiberante-сущностей в нашем приложении (именно для этого добавлен liquibase-hibernate5).
Стоит обратить внимание на настройку user.name. Она не обязательна, но без нее Liquibase, будет указывать в создаваемых скриптах имя текущего пользователя ОС под которым плагин запущен на выполнение.
Настройки для плагина можно прописать непосредственно в pom.xml или передать как параметры командной строки при вызове maven, но мне больше нравится вариант с отдельным файлом liquibase-maven-plugin.properties. Его содержимое будет выглядеть примерно так.
Теперь нам нужно настроить maven-resource-plugin для замены плейсхолдеров в фале настроек, таких как
К слову, Spring boot изменяет стандартный формат для плейсхолдеров, заполняемых при помощи maven-resource-plugin с
Также немного меняем раздел properties в pom.xml, чтобы присвоить значение переменной timestamp в нужном нам формате. Увы, стандартный формат может содержать символы, которые запрещены в именах файлов для некоторых ОС.
Теперь давайте изменим поле email в классе User
И наконец запустим команду сборки, использующую liquibase-maven-plugin для сравнения.
В данном случае нам нужно полностью пересобрать проект, потому что плагин (liquibase:diff) будет использовать для анализа не исходники, а скомпилированные файлы классов сущностей из папке target.
Если все сделано правильно, то после успешного выполнения команды в папке resources/db/changelog у вас появится файл с именем вида db.changelog-20190723-100748666.yaml. Благодаря тому, что мы используем текущую дату и время в имени файла, при каждом запуске у нас будет появляться новый файл, что довольно удобно. Если у вас уже создана БД со структурой таблиц, соответствующей прошлому уроку, то содержимое файла должно быть таким.
Далее мы можем просто скопировать changeSet из этого файла в db.changelog-master.yaml или можем подключить данный файл в него инструкцией
Также в этом файле нужно указать
Это позволит справится с некоторыми проблемами, которые возможны при совместном использовании встроенного в приложение liquibase бина и liquibase-maven-plugin. После этого перезапустите приложение или выполните команду:
Давайте попробуем внести какое-то более сложное изменение в код. Например добавим таблицу ролей у которой будет связь типа многие ко многим с таблицей пользователей.
А в таблицу Users добавляем
После запуска сравнения получим файл со следующим содержимым
Этот файл мы тоже можем легко добавить к рабочим скриптам.
Теперь давайте посмотрим, как откатить внесенные изменения. По какой-то причине те идентификаторы которые мы указывали в changeSet-ах не могут быть использованы для отката к ним. Есть три варианта, указать точку отката
Тег устанавливается следующим образом.
Ну и команды для трех перечисленных способов сделать rollback
Ну и напоследок некоторые дополнительные материалы
Разумеется, буду очень рад любым замечаниям, дополнениям, уточнениям и т.д.
Предположим, что нам понадобилось внести какие-либо изменения в структуру нашей БД. Например, мы хотим, чтобы email не мог быть null. Безусловно, для такого маленького изменения можно было бы подкорректировать код и скрипты вручную, но как быть если изменений будет больше? В этом случае к нам на помощь придет встроенная в Liquibase возможность сравнения БД. Интересной её особенностью является то, что сравнивать можно не только две базы данных, но и базу данных с набором JPA сущностей в нашем приложении. Именно так мы сейчас и поступим!
Создаем скрипт с изменениями при помощи liquibase-diff
В раздел plugins файла pom.xml добавляем вот такую довольно сложную конструкцию. Это liquibase-maven-plugin, к которому подключена зависимость для анализ hibernate-сущностей и работы с файлами в формате YAML. Плагин поможет нам автоматически генерировать liquibase-скрипты через сравнение структур в двух БД или даже через сравнение структуры данных в БД и набора hiberante-сущностей в нашем приложении (именно для этого добавлен liquibase-hibernate5).
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<propertyFile>${project.build.outputDirectory}/liquibase-maven-plugin.properties</propertyFile>
<systemProperties>
<user.name>your_liquibase_username</user.name>
</systemProperties>
<logging>info</logging>
</configuration>
<dependencies>
<dependency>
<groupId>org.liquibase.ext</groupId>
<artifactId>liquibase-hibernate5</artifactId>
<version>3.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
</dependencies>
</plugin>
Стоит обратить внимание на настройку user.name. Она не обязательна, но без нее Liquibase, будет указывать в создаваемых скриптах имя текущего пользователя ОС под которым плагин запущен на выполнение.
Настройки для плагина можно прописать непосредственно в pom.xml или передать как параметры командной строки при вызове maven, но мне больше нравится вариант с отдельным файлом liquibase-maven-plugin.properties. Его содержимое будет выглядеть примерно так.
changeLogFile= @project.basedir@/src/main/resources/db/changelog/db.changelog-master.yaml
url= jdbc:mysql://localhost:3306/geek_db?createDatabaseIfNotExist=true&allowPublicKeyRetrieval=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username= dbusername
password= dbpassword
driver= com.mysql.cj.jdbc.Driver
referenceUrl= hibernate:spring:ru.usharik.liquibase.demo.persist.model?dialect=org.hibernate.dialect.MySQLDialect
diffChangeLogFile= @project.basedir@/src/main/resources/db/changelog/db.changelog-@timestamp@.yaml
ignoreClasspathPrefix= true
Здесь стоит обратить внимание на параметры url и referenceUrl. Скрипт, который создаст liquibase после сравнения будет представлять из себя разницу между базой по ссылке referenceUrlи базой по ссылке url. Если потом этот скрипт запустить на базе по ссылке url, то она станет такой же как и та, которая находится по ссылке referenceUrl. Особое внимание стоит обратить на ссылку referenceUrl. Как видите она ссылается не на БД, а на пакет нашего приложения в котором находятся классы сущностей. Благодаря этому мы сейчас сможем найти скрипт, который добавит в БД изменения, которые были сделаны в коде.Теперь нам нужно настроить maven-resource-plugin для замены плейсхолдеров в фале настроек, таких как
@project.basedir@
и @timestamp@
. Для этого добавим в раздел build раздел resources следующего вида<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>*.properties</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>**/*.*</include>
</includes>
</resource>
</resources>
К слову, Spring boot изменяет стандартный формат для плейсхолдеров, заполняемых при помощи maven-resource-plugin с
${smth}
на @smth@
. Дело в том, что плейсхолдеры вида @smth@
в Spring Boot используются для для подстановки переменных окружения и параметров самого Spring Boot приложения.Также немного меняем раздел properties в pom.xml, чтобы присвоить значение переменной timestamp в нужном нам формате. Увы, стандартный формат может содержать символы, которые запрещены в именах файлов для некоторых ОС.
<properties>
<java.version>1.8</java.version>
<timestamp>${maven.build.timestamp}</timestamp>
<maven.build.timestamp.format>yyyyMMdd-HHmmssSSS</maven.build.timestamp.format>
</properties>
Теперь давайте изменим поле email в классе User
@Column(name = "email", nullable = false)
private String email;
И наконец запустим команду сборки, использующую liquibase-maven-plugin для сравнения.
mvn clean install liquibase:diff -DskipTests=true
В данном случае нам нужно полностью пересобрать проект, потому что плагин (liquibase:diff) будет использовать для анализа не исходники, а скомпилированные файлы классов сущностей из папке target.
Если все сделано правильно, то после успешного выполнения команды в папке resources/db/changelog у вас появится файл с именем вида db.changelog-20190723-100748666.yaml. Благодаря тому, что мы используем текущую дату и время в имени файла, при каждом запуске у нас будет появляться новый файл, что довольно удобно. Если у вас уже создана БД со структурой таблиц, соответствующей прошлому уроку, то содержимое файла должно быть таким.
databaseChangeLog:
- changeSet:
id: 1563876485764-1
author: your_liquibase_username (generated)
changes:
- addNotNullConstraint:
columnDataType: varchar(255)
columnName: email
tableName: users
Как видите, этот скрипт вносит именно то изменение, которое и было сделано в коде. В качестве упражнения, рекомендовал бы вам запустить данный скрипт против пустой базы данных и посмотреть на результат.Далее мы можем просто скопировать changeSet из этого файла в db.changelog-master.yaml или можем подключить данный файл в него инструкцией
- include:
file: db.changelog-20190723-100748666.yaml
relativeToChangelogFile: true
Также в этом файле нужно указать
logicalFilePath: db/changelog/db.changelog-20190723-100748666.yaml
по аналогии с тем, как это сделано в db.changelog-master.yaml. Это позволит справится с некоторыми проблемами, которые возможны при совместном использовании встроенного в приложение liquibase бина и liquibase-maven-plugin. После этого перезапустите приложение или выполните команду:
mvn liquibase:update
Давайте попробуем внести какое-то более сложное изменение в код. Например добавим таблицу ролей у которой будет связь типа многие ко многим с таблицей пользователей.
@Entity
@Table(name = "roles")
public class Role implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name", unique = true, nullable = false)
private String name;
@ManyToMany(mappedBy = "roles")
private Set<User> users;
// далее конструкторы, геттеры, сеттеры
}
А в таблицу Users добавляем
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "users_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles;
После запуска сравнения получим файл со следующим содержимым
databaseChangeLog:
- changeSet:
id: 1563877765929-1
author: your_liquibase_username (generated)
changes:
- createTable:
columns:
- column:
autoIncrement: true
constraints:
primaryKey: true
primaryKeyName: rolesPK
name: id
type: BIGINT
- column:
constraints:
nullable: false
name: name
type: VARCHAR(255)
tableName: roles
- changeSet:
id: 1563877765929-2
author: your_liquibase_username (generated)
changes:
- createTable:
columns:
- column:
constraints:
nullable: false
name: user_id
type: BIGINT
- column:
constraints:
nullable: false
name: role_id
type: BIGINT
tableName: users_roles
- changeSet:
id: 1563877765929-3
author: your_liquibase_username (generated)
changes:
- addPrimaryKey:
columnNames: user_id, role_id
tableName: users_roles
- changeSet:
id: 1563877765929-4
author: your_liquibase_username (generated)
changes:
- addUniqueConstraint:
columnNames: name
constraintName: UC_ROLESNAME_COL
tableName: roles
- changeSet:
id: 1563877765929-5
author: your_liquibase_username (generated)
changes:
- addForeignKeyConstraint:
baseColumnNames: user_id
baseTableName: users_roles
constraintName: FK2o0jvgh89lemvvo17cbqvdxaa
deferrable: false
initiallyDeferred: false
referencedColumnNames: id
referencedTableName: users
- changeSet:
id: 1563877765929-6
author: your_liquibase_username (generated)
changes:
- addForeignKeyConstraint:
baseColumnNames: role_id
baseTableName: users_roles
constraintName: FKj6m8fwv7oqv74fcehir1a9ffy
deferrable: false
initiallyDeferred: false
referencedColumnNames: id
referencedTableName: roles
Этот файл мы тоже можем легко добавить к рабочим скриптам.
Откат изменений
Теперь давайте посмотрим, как откатить внесенные изменения. По какой-то причине те идентификаторы которые мы указывали в changeSet-ах не могут быть использованы для отката к ним. Есть три варианта, указать точку отката
- через количество changeSet-ов считая от текущего
- через дату выполнения изменений
- через tag (задается при помощи changeSet-а специального вида)
Тег устанавливается следующим образом.
- changeSet:
id: some_uniqui_id
author: liquibase_user_name
changes:
- tagDatabase:
tag: db_tag
Ну и команды для трех перечисленных способов сделать rollback
mvn liquibase:rollback -Dliquibase.rollbackTag=db_tag
mvn liquibase:rollback -Dliquibase.rollbackCount=1
mvn liquibase:rollback "-Dliquibase.rollbackDate=Jun 03, 2017"
Ну и напоследок некоторые дополнительные материалы
- Код к данной статье
- Про rollback в Liquibase
- Про миграцию при помощи Liquibase
- Liquibase на GitHub
- Очень хорошая статья о различных подходах к версионированию БД
Разумеется, буду очень рад любым замечаниям, дополнениям, уточнениям и т.д.