Liquibase в приложении на Spring Boot 4
Впервые опубликовано на Хабре 22 июля 2019; здесь переработанная и обновлённая версия.
В 2019 году я написал на Хабре две статьи о Liquibase в Spring Boot: основы, а затем сравнение схем и откат через Maven-плагин. Идеи с тех пор не устарели, а вот почти все номера версий и несколько ключей конфигурации — да. Здесь обе части в одной, пересобранные на Spring Boot 4.1, Liquibase 5.0 и Hibernate 7 и прогнанные от начала до конца перед написанием. В проекте H2 в файле, чтобы он запускался где угодно; со стороны Liquibase для PostgreSQL или MySQL всё то же самое.
Почему не ddl-auto=update
Большинство руководств ставят spring.jpa.hibernate.ddl-auto=update, и Hibernate сам подгоняет таблицы под сущности. В первый день удобно, потом опасно:
- вы не видите и не ревьюите DDL, который выполняет Hibernate;
- он ничего не удаляет и не переименовывает, и схема расходится с кодом;
- отката нет;
- изменениям данных (заполнить новую колонку, переименовать значения) негде жить.
Liquibase превращает изменения схемы в файлы в репозитории. Каждый changeset выполняется в базе один раз; что выполнено, Liquibase записывает в таблицу DATABASECHANGELOG. Работа Hibernate сводится к проверке, что нужные сущностям таблицы и колонки существуют и типы совместимы:
spring.jpa.hibernate.ddl-auto=validate
С validate недостающая колонка роняет приложение при старте, а не первый запрос в работе. Это не полное сравнение: ограничения вроде NOT NULL и индексы не проверяются.
Проект
Spring Boot 4 разделил автоконфигурацию на модули, и у Liquibase теперь свой стартер:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-liquibase</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
В статье 2019 года liquibase-core подключался вручную. В Boot 4 правильный путь — стартер: он приносит liquibase-core в версии, которой управляет Boot, и модуль, запускающий его при старте.
Одна сущность:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false, unique = true, length = 64)
private String username;
@Column(name = "email", length = 255)
private String email;
// конструкторы и геттеры
}
Changelog
По умолчанию Spring Boot ищет на classpath db/changelog/db.changelog-master.yaml. Пусть главный файл будет списком подключений, по файлу на изменение:
databaseChangeLog:
- include:
file: changes/001-create-users.yaml
relativeToChangelogFile: true
databaseChangeLog:
- changeSet:
id: 001-create-users
author: usharik
changes:
- createTable:
tableName: users
columns:
- column:
name: id
type: bigint
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: username
type: varchar(64)
constraints:
nullable: false
unique: true
- column:
name: email
type: varchar(255)
При запуске приложения Liquibase отрабатывает до проверки Hibernate:
Running Changeset: db/changelog/changes/001-create-users.yaml::001-create-users::usharik
ChangeSet db/changelog/changes/001-create-users.yaml::001-create-users::usharik ran successfully in 5ms
Changeset определяется по id, author и пути к файлу. Выполненный changeset не редактируйте: Liquibase хранит контрольную сумму и откажется стартовать, если применённый changeset изменился. Ошибки исправляются новым changeset.
Генерируем changeset по сущностям
Теперь сущность меняется: email становится обязательным, появляется display_name.
@Column(name = "email", nullable = false, length = 255)
private String email;
@Column(name = "display_name", length = 128)
private String displayName;
Changeset можно написать руками. Для изменений побольше пусть Liquibase сравнит базу с сущностями. Это делает Maven-плагин с расширением для Hibernate. В 2019 году это был liquibase-hibernate5, для Hibernate 7 — liquibase-hibernate7:
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>${liquibase.version}</version>
<configuration>
<propertyFile>liquibase.properties</propertyFile>
</configuration>
<dependencies>
<dependency>
<groupId>org.liquibase.ext</groupId>
<artifactId>liquibase-hibernate7</artifactId>
<version>${liquibase.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
</dependency>
</dependencies>
</plugin>
${liquibase.version} берётся из родительского Spring Boot, так что плагин, расширение и библиотека, которую Boot запускает при старте, остаются на одной версии. Настройки плагина — в liquibase.properties рядом с pom.xml:
changeLogFile=db/changelog/db.changelog-master.yaml
searchPath=src/main/resources
url=jdbc:h2:file:./target/demo-db;AUTO_SERVER=TRUE
username=sa
password=
referenceUrl=hibernate:spring:demo.model?dialect=org.hibernate.dialect.H2Dialect
diffChangeLogFile=target/diff-changelog.yaml
changeSetAuthor=usharik
url — база как она есть, referenceUrl — какой она должна быть, здесь по сущностям из пакета demo.model. changeSetAuthor заменяет трюк 2019 года с системным свойством user.name; без него автором станет имя пользователя ОС.
mvn compile liquibase:diff
compile важен: плагин читает скомпилированные классы сущностей. Результат:
databaseChangeLog:
- changeSet:
id: 1791372710407-2
author: usharik
changes:
- addColumn:
columns:
- column:
name: display_name
type: VARCHAR(128)
tableName: users
- changeSet:
id: 1791372710407-1
author: usharik
changes:
- addNotNullConstraint:
columnDataType: varchar(255)
columnName: email
tableName: users
validate: true
Это черновик. Прочитайте его, дайте changeset осмысленные id (в листинге выше остались сгенерированные) и подумайте о данных: addNotNullConstraint на email упадёт, если у какой-то строки нет почты, поэтому в жизни сначала идёт changeset, заполняющий колонку. Затем сохраните его как changes/002-user-display-name.yaml и подключите в главный файл. При следующем старте Liquibase применит оба changeset, и validate пройдёт.
Откат и ловушка
Большинство изменений Liquibase умеет откатывать сам: обратное к addColumn — dropColumn. Для изменений, которые обратить нельзя, например сырого sql, пишется блок rollback в самом changeset. Откатим два последних changeset:
mvn liquibase:rollback -Dliquibase.rollbackCount=2
В первый раз Liquibase ответил мне:
INFO: 0 changesets rolled back.
Rollback command completed successfully.
«Завершено успешно» — и ничего не произошло. Причина в пути к файлу. Spring Boot читает changelog с classpath, поэтому в DATABASECHANGELOG лежат пути вида db/changelog/changes/002-user-display-name.yaml. Мой первый liquibase.properties указывал плагину на src/main/resources/db/changelog/..., а для Liquibase это другой файл, чьи changeset никогда не применялись. Откатывать нечего.
Исправление — пара настроек, показанная выше: changeLogFile относительно папки ресурсов и searchPath=src/main/resources. Тогда пути совпадают:
Rolling Back Changeset: db/changelog/changes/002-user-display-name.yaml::1791372710407-1::usharik
Rolling Back Changeset: db/changelog/changes/002-user-display-name.yaml::1791372710407-2::usharik
mvn liquibase:update применит их снова. Ещё две команды полезны перед работой с настоящей базой: liquibase:updateSQL и liquibase:rollbackSQL не выполняют SQL, а пишут его в target/liquibase/migrate.sql, а liquibase:tag ставит метку, к которой потом можно откатиться по имени через -Dliquibase.rollbackTag=....
Итоги
ddl-auto=validateплюс Liquibase: Liquibase меняет схему, Hibernate её проверяет.- Одно изменение — один файл, подключённый из главного changelog. Выполненный changeset не редактируется.
- Сравнение с сущностями даёт черновик changeset; дальше его правят как код.
- Пути к changelog у приложения и у плагина должны совпадать (
searchPath), иначе откат молча ничего не найдёт. - Берите версии, которыми управляет Spring Boot; в 2026 году это Spring Boot 4.1 с Liquibase 5.0 и
liquibase-hibernate7.