В этой статье я разберу использование утилиты Liquibase в Spring Boot приложениях для версионирования структуры реляционной БД и миграции этой структуры с одной версии на другую. В первой части разберем базовый пример, а во второй поговорим об использовании liquibase-mave-plugin для отката изменений и автоматической генерации скриптов через сравнение структур БД.
Начнем с того, что создадим простейшее приложение на Spring Boot + JPA (Hibernate). В этом нам поможет Spring Initializr. Из зависимостей выбираем JPA, MySQL и Web. Liquibase тоже можно подключить на этом шаге, но для лучшего понимания мы это сделаем далее вручную.
Создаем основу приложения
Добавим в наше приложение класс сущности, а также репозиторий и REST-контроллер для работы с ней. Для конкретности, будем хранить в создаваемой сущности информацию о пользователях.
@Entity
@Table(name = "users")
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "username", unique = true, nullable = false)
private String userName;
@Column(name = "password", nullable = false)
private String password;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
@Column(name = "email")
private String email;
// Далее конструктор по умолчанию, геттеры и сеттеры
// Или можете использовать Lombok
}
Spring Data позволяет сделать код репозитория чрезвычайно кратким
public interface UserRepository extends JpaRepository<User, Long> {
}
REST-контроллер, который будет выдавать все содержимое таблицы пользователей
@RestController
public class UserController {
private UserRepository userRepository;
@Autowired
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping("/user/all")
public List<User> allUsers() {
return userRepository.findAll();
}
}
Настройки в файл application.properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/geek_db?createDatabaseIfNotExist=true&allowPublicKeyRetrieval=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=dbuser
spring.datasource.password=dbpassword
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
Предполагается, что на вашем компьютере запущен сервер MySQL на стандартном порту. При необходимости, подкорректируйте URL сервера в строке соединения, а также логин и пароль. Также стоит обратить внимание на параметр createDatabaseIfNotExist. Благодаря нему мы будем при подключении создавать базу данных с именем geek_db, если её нет на сервере.
Добавляем Liquibase
Наверняка вы обратили внимание, что не хватает одной настройки для hibernate, а именно spring.jpa.hibernate.ddl-auto. В большинстве руководств для начинающих для нее указывают значение update, благодаря которому Hibernate будте сама создавать и корректировать структуру таблиц на сервере, основываясь на присутствующих в проекте классах сущностей. Такой подход вполне может быть использован, если схема данных очень простая или проект учебный, но при сколь-нибудь сложной схеме скорее всего начнутся проблемы хотя бы из-за того, что мы никак не можем контролировать процесс генерации Hibernate-ом DDL скриптов. Ещё одна проблемная состоит в том, что при таком подходе нет простого способа откатить сделанные Hibernate изменения в структуре БД.
Именно для решения выше описанных проблем мы и будем использовать утилиту Liquibase. На наше счастье, она отлично умеет интегрироваться со Spring Boot приложениями! Чтобы начать её использовать, необходимо выполнить следующие действия
Добавляем в файл application.properties настройку
spring.jpa.hibernate.ddl-auto=none
Это нужно для того, чтобы Hibernate не выполнял никаких действий по модификации схемы, т.к. теперь их будет делать Liquibase. Теоретически, тут можно использовать еще и значение validate для дополнительного контроля правильности структуры таблиц.
Добавляем в pom.xml зависимость
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
После её добавления Spring boot автоматически будет создавать специальный бин с именем liquibase, который при каждом запуске приложения будет выполнять все действия по настройке схемы БД на основе скриптов Liquibase.
Теперь необходимо добавить сам Liquibase скрипт, который будет создавать нужную нам таблицу. Создаем в папке /src/main/resources/db/changelog файл с именем db.changelog-master.yaml и добавляем в него следующее содержимое
databaseChangeLog:
- logicalFilePath: db/changelog/db.changelog-lesson1.yaml
- changeSet:
id: 1
author: your_liquibase_username
changes:
- createTable:
tableName: users
columns:
- column:
name: id
type: int
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: username
type: varchar(50)
constraints:
unique: true
nullable: false
- column:
name: password
type: varchar(512)
constraints:
nullable: false
- column:
name: first_name
type: varchar(50)
- column:
name: last_name
type: varchar(50)
- column:
name: email
type: varchar(50)
Разберем содержимое этого скрипта. Прежде всего, в нем содержится один changeSet. ChangeSet представляет из себя аналог коммита в системах контроля версий, таких как Git или SVN. По аналогии с коммитом, изменения, которые были внесены в рамках одного changeSet-а можно накатить или откатить (rollback) на сервер БД. У каждого changeSet-а должен быть уникальный идентификатор при помощи которого liquibase определяет был ли данный changeSet накачен на данную БД или нет.
ChangeSet содержит команду создания таблицы, причем структура таблицы описана средствами liquibase, а не SQL-скриптом. Благодаря этому данный файл становится кросплатформенным. Liquibase будет формировать SQL-скрипт в зависимости от используемого сервера БД. Кроме того, если нам будет нужно откатить данный changeSet, Liquibase сумеет автоматически создать скрипт для удаления данной таблицы. Если бы мы использовали SQL скрипты, то нам пришлось бы вручную писать скрипт для отката изменений. В разделе сhanges у нас всего одна команда и это считается хорошей практикой, не смотря на то, что команд в одном changeSet может быть сколько угодно.
Написанного кода вполне достаточно, чтобы запустить программу, но чтобы более наглядно видеть результаты её работы давайте добавим еще один changeSet, который заполнит таблицу данными.
- changeSet:
id: 2
author: your_liquibase_username
comment: "Create admin user"
changes:
- insert:
tableName: users
columns:
- column:
name: username
value: "admin"
- column:
name: password
value: "admin"
- column:
name: email
value: "admin@server.com"
- insert:
tableName: users
columns:
- column:
name: username
value: "guest"
- column:
name: password
value: "guest"
- column:
name: email
value: "guest@server.com"
rollback:
- delete:
tableName: users
where: username in ('admin', 'guest')
В данном случае нам уже пришлось написать вручную блок для rollback операций, т.к. Liquibase не умеет автоматически создавать rollback SQL при работе с данными. Вообще работа с данными в БД не входит в число ключевых фич Liquibase и ограничивается лишь простейшими операциями вставки и удаления или изменения. К слову, если нужно больше, тот тут можно воспользоваться инструментами от фирмы Red Gate.
И так, давайте запустим наше приложение и попробуем перейти по ссылке http://localhost:8080/user/all. Если приложение запустилась, то вы увидите JSON-ответ с информацией о двух пользователях, которые были добавлены в таблицу. Также стоит взглянуть на логи запуска приложения, в которых можно видеть скрипты, которые Liquibase выполняет для инициализации БД. Особое внимание стоит обратить на таблицу DATABASECHANGELOG. Именно в ней Liquibase хранит лог изменений, внесенных в базу.
На этом пока все. Через некоторое время планирую опубликовать продолжение об использовании liquibase-maven-plugin для автоматической генерации скриптов через сравнение структур БД и отката внесенных изменений.
Буду благодарен за любые дополнения и замечания!
Автор: usharik