任何依赖数据库的应用程序都会从数据库管理工具中受益,这在企业项目中是必不可少的。两种最受欢迎的解决方案是 Liquibase 和 Flyway。
我不会深入探讨它们之间的差异:两者都相当相似,有一些区别使得Liquibase对我来说更吸引人(例如生成差异)。相反,我将引导您在Spring Boot项目中设置Liquibase,并描述两种创建变更日志的方式:手动和自动方式。
我还将简单介绍一下Liquibase的结构和特性,让您在学习教程过程中更清楚地理解核心概念。
Liquibase 的基本结构和命令Liquibase 可以让你通过 SQL 脚本或与数据无关的格式(如 XML、JSON 或 YAML)来逐步演化你的数据库模式。这种与数据无关的脚本尤其在你需要跨多个数据库管理同一模式时很有帮助。
Liquibase的工作流程基于以下核心概念:
-
Changelogs 是用于记录数据库变更的文件。
-
Changesets 是一组应用于数据库的更改,它们被包含在 changelog 文件中。
-
改变类型是指在 changesets 中指定的用于更新数据库的类型(例如,添加一列)。
- Changelog 标签帮助我们控制更改何时以及应用于哪个数据库。
有三种类型的变更日志标签:
- 前置条件标签添加到变更日志或变更集,并用于根据数据库状态控制其执行。
- 上下文标签用于在运行时决定是否执行某些变更集。这些标签以表达式形式添加到变更集中,然后在运行时使用
--context-filter
指定,仅部署匹配指定过滤器的变更集。 - 标签类似于上下文标签,区别在于它们作为标签添加到变更集,并在运行时通过
--label-filter
指定。
此外,大约有40个命令,被分成了六类。
- 启动以在您的项目中启动Liquibase,
- 更新以执行未应用的更改,
- 回滚以撤销更改,
- 比较差异以比较数据库之间的差异,
- 状态以获取更改的状态,
- 管理相关文件以管理Liquibase文件和资源。
好了,理论讲完了,现在开始干活吧!
要完成本教程,你需要:
- JDK 17 或更高版本,这是因为 Liquibase 自带 Java 17,并且 Spring Boot 3 至少需要 Java 17。我将使用 Spring 团队推荐的 Liberica JDK,这是一个开源的 Java 开发工具包。如果没有安装 JDK 17,可以从这里下载:下载 JDK 17;
- 您喜欢的 IDE。
以下的代码可在 GitHub 上找到。
创建一个新的Spring Boot项目首先,让我们通过Spring Initializr创建一个新项目。选择Java、Maven以及最新稳定的Spring Boot版本(我用的是3.2.4版本),Java 17版本。将组名和项目名改为你喜欢的名称,然后添加以下依赖项。
- Lombok,
- Spring Web,
- Spring Data JPA,
- MySQL 驱动程序,
- Liquibase 升级。
用 Spring Initializr 创建项目
下载项目然后在您的IDE里打开它。
需要注意的是,如果你使用你自己的项目,需要在pom.xml文件中加入Liquibase依赖项。
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
下一步是创建一个简单的实体类、存储库和RestController。
名为 _Student_
的实体类的代码如下:
@Entity
@Table(name = "students")
@@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Student implements Serializable {
// 表示实体
@Id
// 自动生成ID
@GeneratedValue(strategy = GenerationType.IDENTITY)
// 设置字段名为id且不能为空
@Column(name = "id", nullable = false)
private Long id;
// 设置字段名为first_name且不能为空
@Column(name = "first_name", nullable = false)
private String firstName;
// 设置字段名为last_name且不能为空
@Column(name = "last_name", nullable = false)
private String lastName;
// 设置字段名为email且不能为空
@Column(name = "email", nullable = false)
private String email;
// 学生类,实现序列化接口,包含ID、名字、姓氏和邮箱
}
代码库接口的代码可能像这样:
public接口学生仓库(StudentRepository)继承JpaRepository<学生, 长整型> { }
以下是RestController类中包含一个方法实现的代码:
@RestController
public class 学生控制器 {
private final 学生仓库 repository;
public 学生控制器(学生仓库 repository) {
this.repository = repository;
}
@GetMapping("/students")
List<Student> 查找所有() {
return repository.findAll();
}
}
最后的一步是在 application.properties 文件里配置数据库连接信息(假设您已经在本地机器上运行了 MySQL 服务器):
# 数据源驱动类名
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 数据库连接URL
spring.datasource.url=jdbc:mysql://localhost:3306/student_db?createDatabaseIfNotExist=true&allowPublicKeyRetrieval=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
# 数据库用户名
spring.datasource.username=root
# 数据库密码
spring.datasource.password=test123
# 是否显示SQL语句
spring.jpa.show-sql=true
# 是否自动创建或更新数据库表
spring.jpa.hibernate.ddl-auto=none
# Liquibase变更日志文件路径
spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.yaml
您可以根据需要更改设置(例如:用户名或密码。)。需要注意的两个重要事项是:
spring.jpa.hibernate.ddl-auto=none
告诉Hibernate不要自动修改数据库模式,因为这是Liquibase现在要处理的任务。spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.yaml
指向主变更日志文件,记录所有数据库变更。
顺便说一下,运行应用程序时可能会遇到 java.lang.IllegalStateException: Cannot load driver class: com.mysql.jdbc.Driver
异常。添加指定版本的 mysql-connector-java 依赖项可以解决这个问题:
以下是Maven项目的依赖配置:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
生成Liquibase的变更日志文件.
是时候为我们的数据库加入一个Liquibase脚本来管理它了。
当我们创建项目时添加了Liquibase依赖项,Spring Boot 会自动在资源目录中添加必要的_db/changelog_文件夹。如果你是在现有项目中添加Liquibase,则需要自行创建这些文件夹。
在_db/changelog_目录下,创建一个名为db/changelog-master.yaml的文件,文件内容如下:
databaseChangeLog: # 数据库变更日志 (数据库变更日志)
- include: # 包含文件
file: 文件 # 文件路径:db/changelog/changelog-1.yaml
db/changelog/changelog-1.yaml
有时,变更集会直接添加到主变更日志文件中。不过,最好将其视为指向具体更改记录的链接集合。
在这种情形下,我们将指向 changelog-1.yaml 文件。让我们在同一 db/changelog 目录下创建该文件,并用以下内容填充它:
数据库变更日志:
- 变更集:
id: 1
author: Nemo
preConditions:
- 失败时标记已运行:
not:
表存在:
tableName: students
changes:
- 创建表:
tableName: students
列:
- 列:
name: id
type: BIGINT
autoIncrement: true
constraints:
primaryKey: true
非空: false
- 列:
name: first_name
type: varchar(250)
constraints:
非空: false
- 列:
name: last_name
type: varchar(250)
constraints:
非空: false
- 列:
name: email
type: varchar(250)
constraints:
非空: false
- 变更集:
id: 2
author: Nemo
changes:
- 插入:
tableName: students
列:
- 列:
name: first_name
value: "John"
- 列:
name: last_name
value: "Doe"
- 列:
name: email
value: "doe@sample.com"
我们再仔细瞧瞧。
该文件包含两个版本集:一个用于创建表,另一个用于填充测试数据。每个版本集都有一个唯一的ID。请注意,最好每个版本集只包含一个变更,这样可以更容易回滚,因此更便于管理和追踪。
第一个变更集包含一个预设部分,指示 Liquibase 如果该表已存在,则标记该变更集为已成功运行而不实际执行它。
注意,创建表并插入数据的脚本是与特定数据库无关的。无论使用什么数据库,Liquibase 会相应生成脚本。
第二个数据更改集用示例数据填充了我们的表格。
运行应用程序。你应该能在你用的数据库管理工具里看到新创建的 student_db 数据库。这个数据库有三个表,其中两个是 Liquibase 特有的并且是自动创建的两个表。
我刚创建了一个表
另外,我们的学生表有我们在第二个更新的数据集里添加的测试数据。
带有示例数据的数据库表
自动生成变更日志,使用Liquibase的diff功能
如果你想更改你的数据库模式:调整现有列的同时添加新的列。你可以手动将变更添加到变更日志中,或者利用Liquibase的差异功能特性。
差异功能允许我们比较两个数据库或一个数据库和新的持久化实体。要使用它,您需要在 pom.xml(项目对象模型文件)中添加一个 liquibase-maven-plugin
和一个 Liquibase Hibernate 扩展插件。
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>4.27.0</version>
<configuration>
<changeLogFile>src/main/resources/db/changelog/db.changelog-master.yaml</changeLogFile>
<diffChangeLogFile>src/main/resources/db/changelog/${maven.build.timestamp}_changelog.yaml</diffChangeLogFile>
<driver>com.mysql.cj.jdbc.Driver</driver>
<url>jdbc:mysql://localhost:3306/student_db?createDatabaseIfNotExist=true&allowPublicKeyRetrieval=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC</url>
<defaultSchemaName />
<username>root</username>
<password>test123</password>
<referenceUrl>hibernate:spring:dev.cat.liquibasedemo?dialect=org.hibernate.dialect.MySQLDialect
&hibernate.physical_naming_strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
</referenceUrl>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>org.liquibase.ext</groupId>
<artifactId>liquibase-hibernate6</artifactId>
<version>4.27.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.2.4</version>
</dependency>
</dependencies>
</plugin>
插件配置当中有不少东西,例如:
changeLogFile
指向我们的主变更日志。diffChangeLogFile
指向数据库比较生成的变更日志。文件名包含时间戳,便于版本追踪。url
指向我们用于比较的目标数据库地址。referenceURL
指向应用程序中存放我们 JPA 实体的包。它还指定了 Hibernate 物理和隐式命名策略,以避免 API 不兼容性。
现在我们来更新学生类,
@Column(name = "email_address", nullable = false)
private String email;
// 电子邮件地址,不能为空
@Column(name = "phone_number")
private int phoneNumber;
// 电话号码,可为空
Liquibase:diff 使用来自 /target 目录的编译后的类进行比较,因此需要重新构建项目。运行以下命令:
mvn clean install liquibase:diff -DskipTests=true
…
[INFO] 使用方言 org.hibernate.dialect.MySQLDialect
[INFO] 对数据库 root@localhost @ jdbc:mysql://localhost:3306/student_db?createDatabaseIfNotExist=true&allowPublicKeyRetrieval=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC (默认模式: student_db) 进行差异比较
[INFO] 创建快照
[INFO] 找到表 students 和 students
[INFO] 找到主键 studentsPK
[INFO] 找到列 id bigint
[INFO] 找到列 email_address varchar(255)
[INFO] 找到列 first_name varchar(255)
[INFO] 找到列 last_name varchar(255)
[INFO] 找到列 phone_number integer
[INFO] 创建快照
[INFO] 差异比较命令已完成
[INFO] 最佳实践: 通过 diffChangeLog/generateChangeLog 生成的变更日志在部署之前,应检查变更日志的正确性和完整性。一些数据库对象及其依赖项无法自动表示,可能需要在部署前手动更新。
[INFO] 变更集计数: 5
[INFO] src/main/resources/db/changelog/20240404-114913515_changelog.yaml 不存在,创建并添加了 5 个变更集。
[INFO] 差异变更日志命令成功执行
[INFO] 命令执行完成
[INFO] 差异已写入变更日志文件,src/main/resources/db/changelog/20240404-114913515_changelog.yaml
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] 构建成功完成
[INFO] 总耗时: 3.751 s
[INFO] 完成时间: 2024年4月4日14点49分17秒+03:00
[INFO] ------------------------------------------------------------------------
如下内容类似的文件应该出现在_db/changelog_目录里:
数据库变更日志:
- 变更集:
id: 1712231357215-3
author: Nemo
变更:
- 添加列:
列:
- 列:
约束:
非空约束: true
名称: 电子邮件地址
类型: varchar(255)
表名: students
- 变更集:
id: 1712231357215-4
author: Nemo
变更:
- 添加列:
列:
- 列:
名称: 电话号码
类型: integer
表名: students
- 变更集:
id: 1712231357215-5
author: Nemo
变更:
- 删除列:
列名: email
表名: students
- 变更集:
id: 1712231357215-1
author: Nemo
变更:
- 修改数据类型:
列名: 名字
新数据类型: varchar(255)
表名: students
- 变更集:
id: 1712231357215-2
author: Nemo
变更:
- 修改数据类型:
列名: 姓氏
新数据类型: varchar(255)
表名: students
如你所见,Liquibase 检测到了我们在 Student 类中引入的所有变更。
现在你可以将此文件添加到我们的主变更日志文件中。
- 包含文件 `db/changelog/20240404-114913515_changelog.yaml`
运行应用程序后,数据库模式应该已经发生了相应的变化。
把数据库表改了
但等一等,你有没有注意到邮件列的数据不见了?这是因为当我们改了列名后,Liquibase 删除了该列并插入了一列新的,导致了数据的丢失。
自动变更日志生成并不完美,可能会有一些问题。我将在系列文章的下一篇文章中告诉你这可能会遇到什么问题以及如何应对(剧透警告:你不必手动编写迁移脚本)。
结尾最后的思考
如你所见,Liquibase 是一个强大的工具,用来帮助你迁移 Java 应用程序的数据库模式,并记录项目的所有变更。你可以手动创建变更日志,或者依赖于 liquibase:diff 自动生成变更日志,这能省下不少时间。