Database Version Control with Liquibase and Spring Boot

TL;DR · AI 摘要
Liquibase 通过代码控制数据库变更,解决多环境数据库不一致问题,确保数据库与代码同步。
核心要点
- Liquibase 将数据库变更作为代码管理,避免手动 SQL 脚本带来的问题。
- Spring Boot 应用启动时,Liquibase 自动执行数据库迁移,确保环境一致性。
- 手动管理数据库变更会导致部署失败和团队协作困难。
结构提纲
按章节快速跳转。
- §引言
描述数据库变更管理不善导致的常见问题和团队协作挑战。
Liquibase 通过代码控制数据库变更,确保数据库与代码同步。
Spring Boot 应用启动时,Liquibase 自动执行数据库迁移。
手动管理数据库变更会导致部署失败和团队协作困难。
数据库版本控制确保多环境数据库的一致性,避免 schema drift。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- 数据库版本控制
- Liquibase
- 代码控制数据库变更
- 与 Spring Boot 集成
- 自动执行数据库迁移
- 问题
- schema drift
- 手动管理数据库变更
- 部署失败
金句 / Highlights
值得收藏与分享的关键句。
Liquibase solves this problem by bringing version-control discipline to your database changes.
When the CI/CD pipeline (or a teammate) pulls that code, the Spring Boot application starts.
Manual database updates guarantee failure at scale.
使用 Liquibase 和 Spring Boot 实现数据库版本控制
2026年6月9日
/
#数据库
Ashutosh Krishna
想象一下这个熟悉的场景:你正在开发一个需要新数据库列的新功能。你打开本地数据库客户端,编写了一个 ALTER TABLE 语句并执行了它。你的代码运行得非常顺利。你提交了 Java 代码,将其推送到仓库,然后去喝咖啡。
几个小时后,一位队友拉取了你的分支,运行了应用程序,但一切崩溃了。
“嘿,”他们从房间的另一边(或在 Slack 频道中)问,“你修改了数据库吗?”
你很快意识到自己忘记分享 SQL 脚本。你将其粘贴到聊天中。他们运行了它。一切正常。然后,一周后,由于同样的原因,向预发布环境部署失败。当这段代码到达生产环境时,每个人都在问一个类似、令人恐惧的问题:“我应该运行哪个 SQL 脚本?”
这种情况被称为模式漂移(schema drift)。当数据库的状态在不同环境中发生分歧时,就会发生这种情况。预发布环境有一个模式,生产环境有另一个,每个开发人员的本地机器都是一个独特、未经测试的数据库修改雪球。
手动管理数据库更改会导致部署的头痛和团队协作的挑战。应用程序代码是无状态的,易于替换。数据库是有状态的。数据库出人意料地有很好的记忆,它们很少忘记一次糟糕的迁移。
Liquibase 通过将版本控制的纪律引入数据库更改来解决这个问题。与其传递 SQL 文件并希望人们记得运行它们,不如在代码中定义数据库更改。这些更改会随着应用程序仓库一起旅行,并自动执行。
以下是对这种架构工作方式的高层次概述:
想象一下单个数据库更改的旅程。开发人员将他们的数据库迁移与 Java 代码一起提交到 Git。当 CI/CD 管道(或队友)拉取该代码时,Spring Boot 应用程序启动。但在此应用程序完全启动并接受网络流量之前,Liquibase 会拦截这个过程。它充当守门人,连接到数据库并应用所需的模式更改。这确保了在任何用户发出请求之前,数据库与代码的期望完全匹配。
为什么数据库版本控制很重要
如果你花过时间在基于团队的应用程序上工作,你可能见过这样的文件夹结构:
project-sql-scripts/
├── create_employee_table.sql
├── create_employee_table_final.sql
├── create_employee_table_final_v2.sql
├── add_email_column.sql
├── latest.sql
└── definitely_latest_use_this_one.sql“只需手动运行这个 SQL 脚本”这句话引发了无数令人难忘的事件。
当你依赖手动数据库更新时,你保证了在大规模时的失败。为新开发人员提供培训变成了一次考古探险,以弄清楚如何构建本地模式。部署变成了一场需要运行一系列手动查询的紧张事件,这些查询必须按照高度特定的顺序执行。
版本控制的数据库更改将你的模式视为代码。当数据库更改与你的应用程序逻辑一起存在时,你可以立即获得以下好处:
- 一致性:每个环境(本地、预发布、生产)以完全相同的顺序应用完全相同的更改。
- 安全性:你消除了跳过脚本或运行过时查询的人为错误。
- 可视性:你可以查看一个 Git 提交,清楚地看到 Java 代码和数据库模式是如何一起变化以支持新功能的。
Git 解决了代码的版本控制问题。Liquibase 帮助防止数据库变成那个叛逆的兄弟。
Liquibase 是什么?
本质上,Liquibase 是一个数据库迁移工具,它以可预测和可重复的方式跟踪并应用模式变更。
你不需要编写松散的 SQL 脚本,而是编写“迁移”(也称为 changeSets)。Liquibase 读取这些文件,将其与数据库中的跟踪表进行比较,并确定需要执行哪些操作以使数据库保持最新。
要有效使用 Liquibase,你只需要理解几个概念性术语:
- changeLog:主文件。这基本上是一个列表,告诉 Liquibase 需要执行哪些迁移文件以及执行顺序。
- changeSet:对数据库的单个原子更改。创建一个表是一个 changeSet。添加一个列是另一个 changeSet。
- 迁移历史:Liquibase 在数据库中自动生成的一个表(名为 DATABASECHANGELOG),用于记录哪些 changeSets 已经执行过。
- 校验和:为每个 changeSet 生成的唯一哈希。Liquibase 使用它来检测是否有人在文件已经执行后秘密修改了文件。
当你将 Liquibase 与 Spring Boot 集成时,迁移过程会在应用程序启动阶段完全自动完成。
在启动过程中,Liquibase 在 Web 服务器被允许接收 HTTP 流量之前接管控制。它会进入数据库,检查跟踪表以查看哪些迁移已经运行。如果在本地文件中发现新的迁移,它会锁定数据库以防止并发更新,执行更改,记录新的历史记录,然后释放锁。只有在完成整个过程后,Spring Boot 才会完成启动。
由于 Liquibase 在 Spring Boot 完全初始化 Web 服务器之前运行,你的应用程序将永远不会使用过时的数据库模式提供流量。如果迁移失败,应用程序将无法启动,从而保护系统不进入损坏状态。
项目设置
现在你已经了解了理论,让我们构建一个实际的东西。我们将为一个员工管理 API 构建数据库层。
对于这个项目,我们将使用:
- Java 17+
- Spring Boot 3.x
- Maven
- Liquibase
- H2 数据库
我们使用 H2 是因为它是一个内存数据库,不需要任何安装。你可以在不配置 Docker 容器或安装数据库服务器的情况下立即运行此项目。但在这里学到的一切同样适用于 PostgreSQL、MySQL、SQL Server 或 Oracle。
如果你通过 Spring Initializr 生成这个项目,请选择以下依赖项:Spring Web、Spring Data JPA、Liquibase 迁移和 H2 数据库。
在你的 pom.xml 中,你会看到使这个项目能够运行的关键依赖项:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency><dependency> <groupId>org.liquibase</groupId> <artifactId>liquibase-core</artifactId> </dependency> </dependencies>
接下来,配置 Spring Boot 以连接到 H2 并找到你的 Liquibase 文件。打开你的 src/main/resources/application.properties 文件并添加以下内容:
H2 数据库配置
spring.datasource.url=jdbc:h2:file:./data/employeedb;DB_CLOSE_DELAY=-1 spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=
启用 H2 控制台,以便在浏览器中查看数据库
spring.h2.console.enabled=true spring.h2.console.path=/h2-console
Liquibase 配置
spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.xml
最后一行是最关键的。它告诉 Spring Boot 精确的位置,以便找到你的数据库更改的“主列表”。
注意:我们使用的是基于文件的 H2 数据库,而不是内存数据库。使用内存数据库的问题在于,每次重启 Spring Boot 时,它都会完全清除自己。
虽然 Liquibase 会在每次启动时高兴地从头开始重建模式,但对于本教程(以及实际的本地开发)来说,基于文件的数据库要好得多。使用基于文件的数据库,你的数据(更重要的是你的 Liquibase 历史记录)实际上会在应用程序重启之间持久化。
## 理解 Liquibase 的核心概念
在我们编写第一个表之前,我们需要了解 Liquibase 如何组织文件。Liquibase 使用一种分层结构。
可以把它想象成一本书。changeLog 就是目录,而 changeSets 是实际的章节。
- 主 changeLog:这是入口点。它很少包含实际的数据库更改。相反,它的唯一工作是按特定顺序包含其他文件。
- 子 changeLogs:这些将相关更改组合在一起。
- changeSets:这些是实际的、原子的数据库命令(如创建表或添加列)。
下面是这种层次结构在实际 Spring Boot 项目中如何工作的视觉分解:
Liquibase 以分层方式组织迁移。你维护一个单一的主文件,它充当目录。这个主文件很少包含实际的 SQL 命令。相反,它明确地按严格的执行顺序包含子 XML 文件。每个子文件(如 01-create-employees.xml)包含一个或多个单独的数据库命令,Liquibase 将其称为 changeSets。
一个 changeSet 由以下三个要素唯一标识:
- id:一个唯一的字符串(通常是数字或 Jira 任务 ID)。
- author:编写迁移的人。
- 文件路径:文件所在的位置。
当 Liquibase 运行时,它会查看一个 changeSet,计算其内容的密码哈希(校验和),并将 id、author 和校验和记录在数据库中。如果在下一次启动时,它在数据库中看到 id、author 和文件路径的精确组合,它会跳过它。
## 创建初始员工模式(版本 1)
让我们编写第一个版本。我们需要一个表来存储员工。
首先,在 src/main/resources/db/changelog/db.changelog-master.xml 中创建主文件:
<?xml version="1.0" encoding="UTF-8"?> <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-4.20.xsd">
<include file="db/changelog/changes/01-create-employees.xml"/>
</databaseChangeLog>
接下来,在 src/main/resources/db/changelog/changes/01-create-employees.xml 路径下创建实际的迁移文件:
<?xml version="1.0" encoding="UTF-8"?> <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-4.20.xsd">
<changeSet id="1" author="ashutoshkrris"> <createTable tableName="employees"> <column name="id" type="BIGINT" autoIncrement="true"> <constraints primaryKey="true" nullable="false"/> </column> <column name="first_name" type="VARCHAR(50)"> <constraints nullable="false"/> </column> <column name="last_name" type="VARCHAR(50)"> <constraints nullable="false"/> </column> </createTable> </changeSet>
</databaseChangeLog>
让我们看一下刚刚做了什么。我们定义了一个 changeSet,其 id 为 "1",作者为 "ashutoshkrris"。在其中,我们使用 Liquibase 的 XML 语法来定义一个表。
为什么使用 XML 而不是普通的 SQL?因为 Liquibase 是与数据库无关的。这个确切的 XML 将为 PostgreSQL(SERIAL)、MySQL(AUTO_INCREMENT)或 Oracle(IDENTITY)生成正确的自增语法。你定义结构,Liquibase 会将其翻译为特定数据库的方言。
现在,运行你的 Spring Boot 应用程序。查看终端输出。你将看到类似以下的日志:
Liquibase 发现数据库是空的。它自动创建了其跟踪表(DATABASECHANGELOG),读取了我们的 changeSet,执行了表的创建,并记录了事件。
如果你现在重新启动应用程序,Liquibase 会再次运行。但这一次,它会检查 DATABASECHANGELOG 表,发现 id="1" 和 author="ashutoshkrris" 已经执行过,然后静默跳过。你的数据库现在已安全地进行版本控制。
## 刚刚发生了什么?
到目前为止,Liquibase 可能看起来有点像魔法。你只是将一个 XML 文件放入文件夹中,启动 Spring Boot,数据库模式就发生了变化。
但理解 Liquibase 实际上是如何工作的至关重要。如果你了解启动顺序,你将知道在事情最终出错时如何精确地调试部署。
当你的 Spring Boot 应用程序启动时,它不会立即开始处理网络请求。首先,它会初始化其内部组件。当它创建 Liquibase 组件时,迁移过程就开始了。
在启动阶段,确切发生的事情如下:
让我们跟踪确切的顺序。当 Spring Boot 初始化 Liquibase 时,该工具做的第一件事是查询锁表,以确保没有其他应用程序实例正在迁移数据库。如果一切安全,它将获取锁。然后,它会计算本地 XML 文件的加密校验和,将其与数据库历史记录进行比较,执行任何缺失的更改,并记录它们。最后,它释放锁,以便 Tomcat Web 服务器可以安全启动。
这个顺序确保了你的应用程序在数据库模式完全准备好处理请求之前,永远不会为用户请求提供服务。
## 检查数据库:Liquibase 元数据表
让我们来看看数据库内部的历史记录和锁定机制到底是什么样子。由于我们之前配置了 H2 控制台,因此可以查看原始表。
当你的 Spring Boot 应用程序正在运行时,打开浏览器并导航到 http://localhost:8080/h2-console 。使用 JDBC URL `jdbc:h2:file:./data/employeedb`、用户名 `sa` 和空密码进行连接。
在控制台中,你会看到你的 `employees` 表。你还会看到 Liquibase 自动创建的两个额外表:`DATABASECHANGELOG` 和 `DATABASECHANGELOGLOCK` 。
### DATABASECHANGELOG 表
这个表是你的迁移策略的核心。它作为这个环境中所有已应用数据库更改的永久账本。
如果你运行 `SELECT * FROM DATABASECHANGELOG;`,你会看到如下输出:
| ID | AUTHOR | FILENAME | DATEEXECUTED | ORDEREXECUTED | EXECTYPE | MD5SUM | DESCRIPTION | COMMENTS | TAG | LIQUIBASE | CONTEXTS | LABELS | DEPLOYMENT_ID |
|------------|----------------|-----------------------------------------------|------------------------|---------------|----------|------------------|---------------------|----------|----------|-----------|----------|--------|----------------|
| 1 | ashutoshkrris | db/changelog/changes/01-create-employees.xml | 2026-05-30 13:11:35.937919 | 1 | EXECUTED | 9:66e7dcffb2b1902a4e9f01670cb5f192 | createTable tableName=employees | null | | 4.31.1 | | | 0126894849 |
让我们分解最重要的列:
- **ID、AUTHOR、FILENAME**:这三列组成一个复合键。它们共同唯一标识一个迁移。
- **DATEEXECUTED & ORDEREXECUTED**:告诉你脚本运行的确切时间和顺序。
- **MD5SUM**:这是你的 XML 文件的加密哈希。当 Liquibase 启动时,它会哈希你的本地 XML 文件,并将其与该列进行比较。如果你在文件执行后秘密编辑了文件,这个哈希将不匹配,Liquibase 会崩溃以保护你的数据库。
- **EXECTYPE**:大多数情况下,这只是一个 `EXECUTED`。但它提供了关键的审计追踪:如果你使用 Liquibase 命令故意跳过一个迁移但将其标记为完成,你会看到 `MARK_RAN`。如果由于先决条件失败而跳过迁移,你会看到 `SKIPPED`。
- **TAG**:将其视为数据库模式的 Git 标签。在重大、高风险部署之前,你可以配置 Liquibase 将数据库的当前状态标记为(例如 `v1.4.0`)。如果部署失败,你可以触发回滚命令,告诉 Liquibase 撤销所有在 `v1.4.0` 标签之后应用的更改。
- **CONTEXTS**:这是你管理特定环境更改的方式。通过在 `changeSet` 中添加上下文属性(例如 `<changeSet id="7" author="ashutoshkrris" context="dev, qa">`),该迁移仅在 Spring Boot 在启动时向 Liquibase 传递 "dev" 或 "qa" 时才会执行。生产环境将安全地忽略它。
- **LABELS**:虽然上下文针对环境,但标签针对工作类别。你可以用 Jira 问题编号(如 `issue-842`)或发布列车(如 `Q3-release`)对 `changeSet` 进行标记。这允许高级团队选择性地执行或回滚特定功能子集,而不会影响数据库的其余部分。
### DATABASECHANGELOGLOCK 表
这个表很小,但在现代部署中起着至关重要的作用。
如果你运行 `SELECT * FROM DATABASECHANGELOGLOCK;`,你会看到一行:
| LOCKED | LOCKGRANTED | LOCKEDBY |
|--------|-------------|----------|
| FALSE | | |
想象一下,你正在将 Spring Boot 应用程序部署到 Kubernetes 集群。你告诉 Kubernetes 同时启动三个相同的实例。这三个实例都连接到同一个数据库。
如果三个实例在同一毫秒尝试运行 CREATE TABLE 迁移,数据库将抛出并发错误。锁表可以防止这种情况发生。第一个到达数据库的实例会将 LOCKED 设置为 TRUE。另外两个实例检查表,发现锁后会礼貌地等待。
实用故障排除技巧:有时,部署在迁移过程中会突然失败(例如服务器断电)。发生这种情况时,Liquibase 可能在将 LOCKED 设置回 FALSE 之前就崩溃了。
下次启动应用程序时,日志将无限期地挂起,不断重复:Waiting for changelog lock....
如果你绝对确定没有其他应用程序正在运行迁移,可以通过在数据库客户端中运行一个简单的 SQL 命令手动解决这个问题:
UPDATE DATABASECHANGELOGLOCK SET LOCKED = FALSE;
这将强制解除锁,使应用程序可以继续运行。
## 演进 Employee API
软件永远不会完成。在你成功部署 Version 1 两周后,业务团队又提出了新的需求。
由于你现在已经了解 Liquibase 如何跟踪历史记录,演进数据库变得简单。你只需将新的文件追加到主列表中即可。
### Version 2:添加电子邮件字段
人力资源团队需要与员工取得联系。你需要一个电子邮件列。
在 src/main/resources/db/changelog/changes/02-add-employee-email.xml 中创建一个新文件:
<?xml version="1.0" encoding="UTF-8"?> <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-4.20.xsd">
<changeSet id="2" author="ashutoshkrris"> <addColumn tableName="employees"> <column name="email" type="VARCHAR(100)"> <constraints nullable="false" unique="true"/> </column> </addColumn> </changeSet>
</databaseChangeLog>
将以下内容立即添加到你的 db.changelog-master.xml 文件中第一个 include 的下方:
<include file="db/changelog/changes/02-add-employee-email.xml"/>
当你重新启动应用程序时,Liquibase 会检查 DATABASECHANGELOG 表。它会发现 id="1" 已经存在,因此会跳过它。它会发现 id="2" 缺失,因此会执行它,并在跟踪表中添加一行新记录。
### Version 3:添加部门支持
公司正在发展。员工现在属于不同的部门。你需要一个部门表,并且需要一个外键约束来连接这两个表。
创建 03-add-departments.xml:
<?xml version="1.0" encoding="UTF-8"?> <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-4.20.xsd">
<changeSet id="3" author="ashutoshkrris"> <createTable tableName="departments"> <column name="id" type="BIGINT" autoIncrement="true"> <constraints primaryKey="true" nullable="false"/> </column> <column name="name" type="VARCHAR(50)"> <constraints nullable="false" unique="true"/> </column> </createTable> </changeSet>
<changeSet id="4" author="ashutoshkrris">
<addColumn tableName="employees">
<column name="department_id" type="BIGINT"/>
</addColumn>
<addForeignKeyConstraint baseTableName="employees"
baseColumnNames="department_id"
constraintName="fk_employee_department"
referencedTableName="departments"
referencedColumnNames="id"/>
</changeSet>
</databaseChangeLog>请注意,我们在一个文件中使用了两个独立的 changeSet。这是一种最佳实践。每个 changeSet 表示一个逻辑操作。如果外键创建(id="4")失败,部门表创建(id="3")仍将被记录为成功,只有 id="4" 会回滚。
版本 4 & 5:员工状态和性能索引
最后,HR 想要跟踪在职员工和不在职员工,并且数据库团队注意到通过姓氏搜索变得越来越慢。
创建 04-status-and-indexes.xml:
请记住将所有新文件添加到你的 db.changelog-master.xml 中。include 语句的顺序就是 Liquibase 执行它们的确切顺序。
黄金规则:永远不要修改已执行的 changeSet
最终,团队中的一位开发人员会查看你的 01-create-employees.xml 文件,并注意到一个错误。也许他们发现列名中有一个拼写错误,或者他们意识到某个列缺少严格的非空约束。
基于多年编写标准 Java 代码的直觉,他们的第一反应会是打开该 XML 文件,修复错误,保存文件,然后重新启动应用程序。
让我们实际这样做,看看会发生什么。
打开你的 src/main/resources/db/changelog/changes/01-create-employees.xml 文件。将 first_name 列改为 given_name:
<column name="given_name" type="VARCHAR(50)">
<constraints nullable="false"/>
</column>保存文件并重新启动你的 Spring Boot 应用程序。
与平滑启动不同,你的应用程序会立即崩溃,终端会输出大量堆栈跟踪。仔细查看错误日志的顶部。你应该看到以下确切消息:
Caused by: liquibase.exception.ValidationFailedException: 验证失败:
1 个 changesets 校验和
db/changelog/changes/01-create-employees.xml::1::ashutoshkrris 原为:9:66e7dcffb2b1902a4e9f01670cb5f192 但现在为:9:2bd3ef21343d3b5c9448cc50bc35deef为什么会发生这种情况。一旦 changeSet 在某个环境中运行,它就成为不可更改的历史。你不能改变过去。
当 Liquibase 启动时,它会计算你本地 XML 文件的加密哈希(一个 MD5 校验和)。然后它会查询 DATABASECHANGELOG 表,并将新计算的哈希与文件最初执行时记录的哈希进行比较。
如果你更改了已执行文件中的任何一个字符,哈希值就会改变。Liquibase 会检测到这种篡改并拒绝启动。它这样做是为了保护你的数据。如果你的 XML 代码说一个列名为 first_name,但数据库最初是使用 fist_name 构建的,你的 Spring Data JPA 仓库无论如何都会失败。
如何正确修复它
如果你在本地犯了这个错误,你可能会想进入数据库,从 DATABASECHANGELOG 表中删除该行,然后再次尝试。不要这样做。如果这段代码到达了预发布环境或生产环境,你不能在生产服务器上手动删除行。
正确修复模式错误的方法是向前滚动。
首先,在 01-create-employees.xml 中撤销你的更改,使哈希值与数据库再次匹配。然后,编写一个新的 changeSet 来应用修复:
<changeSet id="7" author="ashutosh">
<renameColumn tableName="employees"
oldColumnName="first_name"
newColumnName="given_name"
columnDataType="VARCHAR(50)"/>
</changeSet>将其包含在你的主变更日志中,重启应用程序,数据库将安全地进化到正确的状态。
使用种子数据
有时,模式更改需要初始数据才能发挥作用。
例如,在版本 3 中,我们创建了一个 departments 表。目前,该表完全为空。当新开发人员克隆仓库并在本地启动项目时,他们必须手动编写 SQL INSERT 语句,只是为了测试 API。
我们可以通过将基础数据插入作为迁移策略的一部分来自动化这一过程。
在 src/main/resources/db/changelog/changes/05-seed-departments.xml 中创建一个新文件:
<?xml version="1.0" encoding="UTF-8"?>
<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-4.20.xsd">
<changeSet id="8" author="ashutoshkrris">
<insert tableName="departments">
<column name="name" value="Engineering"/>
</insert>
<insert tableName="departments">
<column name="name" value="Human Resources"/>
</insert>
<insert tableName="departments">
<column name="name" value="Finance"/>
</insert>
</changeSet>
</databaseChangeLog>将 include 语句添加到你的 db.changelog-master.xml 文件中。当你重启应用程序时,Liquibase 会插入这些行。现在你的 API 可以立即开箱即用。
数据迁移的危险
虽然种子数据功能强大,但需要纪律。这里有一个实用的工程经验法则:
使用 Liquibase 的情况包括:
- 静态查找表(状态码、国家列表、默认部门)。
- 应用程序启动所需系统配置标志。
不要使用 Liquibase 的情况包括:
- 为测试生成数千个假用户。
- 迁移大量事务数据(例如,将 500 万条记录从一个表迁移到另一个表)。
大规模数据迁移可能会锁定数据库表数小时。如果在部署期间锁定核心表,你的应用程序将面临大规模停机。请将 changeSets 专注于模式结构和必要的基础数据。对于大量数据操作,请使用专用脚本或后台任务。
回滚
在一个理想的世界里,代码总是能正常工作。但在现实中,你最终会部署一个破坏关键生产查询或损坏数据的数据库更改。当这种情况发生时,你需要一种方式来撤销更改。
Liquibase 支持回滚,但你必须了解它是如何解释这些回滚的。
自动回滚与显式回滚
许多 Liquibase 命令都是自动可逆的。例如,如果你编写一个 changeSet 来 <createTable> 或 <addColumn>,Liquibase 隐式地知道添加列的反向操作是删除列。你不需要告诉它如何撤销这些操作。
但有些操作本质上是破坏性的或模棱两可的。如果你使用自定义的 <sql> 标签,或者使用 <dropTable>,Liquibase 将无法知道如何恢复数据。在这些情况下,你必须提供明确的回滚指令。
让我们模拟一个场景:我们添加一个临时的访问码列,但我们要确保知道如何安全地删除它。
创建 06-temporary-access.xml:
<?xml version="1.0" encoding="UTF-8"?>
<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-4.20.xsd">
<changeSet id="9" author="ashutosh">
<addColumn tableName="employees">
<column name="temp_access_code" type="VARCHAR(10)"/>
</addColumn>
<rollback>
<dropColumn tableName="employees" columnName="temp_access_code"/>
</rollback>
</changeSet>
</databaseChangeLog>将此文件添加到你的主文件中并运行应用程序。该列将被添加。
如果你是通过 CI/CD 管道部署此内容,并且部署失败,你可以触发 Liquibase Maven 命令,通过特定的步骤数来回滚(例如 mvn liquibase:rollback -Dliquibase.rollbackCount=1),或者回滚到我们之前讨论的特定标签。
对回滚的现实检验
虽然了解回滚的工作原理很重要,但这里有一个来自后端工程一线的现实情况:回滚经常被讨论,但在生产环境中很少能干净地执行。
删除一列在数学上是简单的。但要恢复在那列中写入的客户数据,这些数据是在那15分钟内坏代码运行时写入的,这非常困难。
正因为如此,现代工程团队通常更倾向于采用“向前推进”的策略。如果迁移导致了问题,他们不会运行一个令人害怕的数据库回滚命令,而是快速编写一个新的 changeSet 来修复问题(例如,添加缺失的索引或放松约束),然后重新部署应用程序。
强烈建议你设计数据库更改时,使其具有可添加性和非破坏性,以避免一开始就需要复杂的回滚。
常见的初学者错误
采用数据库版本控制是任何工程团队向前迈出的一大步,但它伴随着学习曲线。当开发人员从编写松散的 SQL 脚本过渡到使用 Liquibase 时,他们往往会陷入一些可预测的陷阱。
以下是最常见的初学者错误以及如何避免它们的确切方法。
1. “巨型” changeSet
刚开始时,将整个初始模式全部放入一个 XML 文件下的单个 changeSet 中是很诱人的。你可能会将 15 个 createTable 语句和 20 个 addForeignKeyConstraint 语句放入 id="1" 中。
这是个糟糕的想法,原因很简单:事务失败。
如果数据库引擎在第 14 张表上失败(可能由于语法错误),前 13 张表会怎样?一些数据库引擎支持事务性 DDL(数据定义语言),这意味着它会自动回滚前 13 张表。但许多数据库并不支持。
如果在执行过程中失败,数据库现在会处于一个损坏的状态。Liquibase 没有将 id="1" 标记为成功,所以下次启动应用程序时,它会再次尝试创建所有 15 个表。由于表 1 已经存在,应用程序会立即崩溃。
解决方法:坚持“每个 changeSet 只执行一个逻辑操作”的规则。如果你要创建三个表,就写三个独立的 changeSet。如果其中一个失败,成功的操作会被永久记录,你只需要修复出问题的那个。
2. 手动调整数据库(幽灵威胁)
这是最危险的习惯之一。开发人员发现生产环境中缺少一个索引,而不是编写 Liquibase 迁移、进行代码审查并部署,他们直接登录到生产数据库,手动运行 CREATE INDEX 命令以节省时间。
一周后,另一位开发人员编写了一个正确的 Liquibase 迁移来创建相同的索引并部署。应用程序启动时崩溃了。Liquibase 尝试执行 CREATE INDEX 命令,但数据库抛出错误,提示该索引已经存在。
当你采用 Liquibase 时,你必须接受一个基本规则:Liquibase 是你数据库模式的绝对权威来源。人类不应该直接触碰数据库结构。
解决方法:如果有人不小心这样做,你有两个选择来修复部署流程。你可以手动从数据库中删除该索引,以便 Liquibase 能够正确地重新创建它,或者你可以在 Liquibase 中使用 <preConditions> 标签,在尝试创建索引之前检查它是否已经存在。
3. 忽略“从零开始”的构建
当你在一个项目上工作数月后,本地数据库会积累大量的历史记录。你编写迁移时假设某些表或测试数据已经存在。
然后,新成员加入团队。他们拉取代码,启动一个空数据库,运行 Spring Boot,迁移在中途崩溃。
这是因为迁移依赖于一个假设的状态(例如,期望在创建外键之前某个特定的行已经存在),而不是一个保证的状态。
解决方法:你应该定期在完全空白的数据库上测试你的迁移。如果你使用 Docker,可以拆除数据库容器并重新构建它。如果你使用的是像我们之前设置的基于文件的 H2 数据库,只需从项目文件夹中删除 ./data/employeedb.mv.db 文件并重新启动 Spring Boot。如果应用程序无法从完全空的状态成功启动,说明你的迁移历史已经损坏。
4. 硬编码环境细节
初学者有时会直接在 XML 文件中硬编码特定于环境的细节。例如,他们可能会硬编码一个特定的模式名称(schemaName="dev_schema")或授予特定本地用户权限(GRANT ALL ON employees TO my_local_user)。
当这段代码进入预发布环境时,预发布数据库使用的是不同的模式名称,部署会失败。
解决方法:保持迁移的抽象性。让 Spring Boot 通过 application.properties 文件处理连接细节。如果你确实需要在 Liquibase 文件中使用动态值,请使用属性替换。你可以在 Liquibase 中定义变量,并在 Spring Boot 启动时从外部传入这些变量。
5. 迁移顺序出错
Liquibase 按照你 db.changelog-master.xml 文件中列出的顺序执行文件。
如果开发人员 A 在一个分支中创建了 departments 表,而开发人员 B 在另一个分支中创建了一个指向 departments 的外键,那么谁先合并代码就决定了顺序。如果开发人员 B 的代码在开发人员 A 的代码之前被合并到主文件中,Liquibase 将尝试在目标表存在之前创建外键。
解决方案:主变更日志是数据库变更的最终控制点。在代码审查过程中,始终验证 <include> 语句是否按时间顺序排列,并确保依赖关系合理。
Liquibase 与 Flyway 与手动 SQL 脚本的比较
当你决定实施数据库版本控制时,你将立即面临一个选择。Liquibase 并不是 Java 生态系统中唯一的工具。管理模式演进的三种最常见的方法是 Liquibase、Flyway 和手动 SQL 脚本。
你应该了解每种方法的实际权衡,以便为你的特定团队和项目选择合适的工具。
1. 手动 SQL 脚本(基线)
这是大多数初学者的默认方法。你编写一个 script.sql 文件,并使用 DBeaver、pgAdmin 或 DataGrip 等工具直接在数据库上执行它。
- 优势:不需要任何设置。你对确切的语法有完全的控制权,每个后端开发人员都已经知道如何编写 SQL。
- 劣势:完全没有执行跟踪。这种方法几乎可以保证在不同环境中出现模式漂移。部署变得紧张,因为它们依赖于人类记住执行正确的脚本并按照正确的顺序执行。
- 评价:手动脚本对于个人周末项目或快速原型设计来说是完全可行的,只要你不关心数据库是否被破坏。但一旦有第二个开发人员加入团队或创建了预发布环境,它们就会变成巨大的负担。
2. Flyway(SQL 纯主义者)
Flyway 是 Liquibase 最受欢迎的替代工具。与使用 XML 或 YAML 抽象不同,Flyway 采用原始 SQL。你使用严格的命名约定编写纯 SQL 文件(例如,V1__Create_employee_table.sql)。
- 优势:不需要学习新的语法。如果你知道 SQL,你就已经知道如何使用 Flyway。设置起来非常快速,观点明确,并且与 Spring Boot 完美集成。
- 劣势:由于你编写原始 SQL,你的迁移与特定数据库方言紧密相关。如果你为 MySQL 编写了 Flyway 脚本,之后决定将项目迁移到 PostgreSQL,你必须手动重写迁移历史。此外,无缝的自动回滚是 Flyway 商业版本的付费功能。
- 评价:Flyway 非常适合那些在 SQL 方面技能高超、永久承诺使用单一数据库供应商,并且更喜欢严格约定而非灵活配置的团队。
3. Liquibase(抽象层)
正如我们在本教程中所看到的,Liquibase 通过将数据库更改抽象为 XML、YAML 或 JSON 采用了一种不同的方法。
- 优势:它真正与数据库无关。你定义逻辑结构,Liquibase 自动将其转换为 H2、PostgreSQL 或 Oracle 的正确 SQL 方言。它开箱即用地支持强大的自动回滚、先决条件、上下文和部署标签,而且完全免费。
- 劣势:与 Flyway 相比,它的学习曲线更陡峭。XML 语法无疑非常冗长,对于非常简单、单表的应用程序来说可能会显得笨重。
- 结论:Liquibase 在复杂应用、多租户系统、支持多种数据库供应商的项目以及需要对 CI/CD 部署管道进行精细控制的企业环境中表现出色。
Liquibase 最佳实践
现在你已经了解了 Liquibase 的工作原理,接下来需要知道如何在专业环境中使用它。编写一个在本地机器上运行的迁移脚本只是成功的一半。要编写一个整个团队可以安全部署到生产环境的迁移脚本,需要有纪律性。
以下是管理数据库变更时应采用的工程最佳实践。
1. 每个 changeSet 只包含一个逻辑变更(原子规则)
我们在常见错误部分已经讨论过这一点,但重复强调也很重要。永远不要将表的创建、索引的创建和数据插入操作放在同一个 changeSet 中。
如果你要添加一个 salary 列和一个 idx_employee_salary 索引,应将它们放在同一文件中的两个独立 changeSet 中。这样可以确保如果索引创建失败,列的创建仍然可以安全地记录,从而避免数据库处于不一致的状态。
2. 有意义的文件组织和命名
不要将文件命名为 update1.xml 或 new_changes.xml。你的文件名应该讲述数据库是如何演变的故事。
采用严格的前缀系统。在我们的项目中,我们使用了 01-create-employees.xml 和 02-add-employee-email.xml。在一个实际的团队中,你可能会使用 Jira 任务编号或版本号(例如,v1.2.0_ticket-482_add_email.xml)。无论你选择哪种命名约定,都要在代码审查过程中严格执行。
3. 将数据库变更视为应用程序代码
数据库迁移应与 Java 代码一起放在源代码控制中。它们应受到与应用程序代码相同的严格审查。
在审查包含 Liquibase 文件的拉取请求时,工程师应提出以下问题:
- 这个列是否需要索引?
- 这是一个会破坏当前运行应用程序的变更(如重命名列)吗?
- 作者是否为自定义 SQL 提供了明确的回滚指令?
4. 将迁移集成到 CI/CD 中
绝不应手动在生产服务器上运行数据库迁移。你的部署管道应自动处理这一过程。
当你将代码合并到主分支时,CI/CD 管道(如 GitHub Actions 或 GitLab CI)应构建你的 Spring Boot 应用并进行部署。由于我们将 Liquibase 集成到了 Spring Boot 的启动流程中,应用会在开始接收网络流量之前自动迁移生产数据库。
一个安全、自动化的部署管道应该如下所示:
在一个成熟的部署管道中,人类永远不会直接操作生产数据库。当你合并一个拉取请求时,CI/CD 管道会构建代码并运行单元测试。它会将 Spring Boot 应用部署到一个预发布环境,Liquibase 会在启动时自动获取锁并运行迁移。验证通过后,相同的构建产物会被提升到生产环境,触发完全相同的自动化迁移过程。
5. 永远不要通过删除历史来修复问题
如果某个迁移在上层环境(如预发布或生产)中失败,永远不要登录数据库删除 DATABASECHANGELOG 行以重新尝试。
你必须尊重变更日志的不可变性。如果你犯了错误,请编写一个新的 changeSet 来删除损坏的表或修复数据类型,并通过你的 Git 工作流程将其推送,就像你修复 Java 缺陷一样。
最后的想法
管理数据库模式变更并不必然是令人焦虑的来源。
将数据库模式视为代码,可以消除手动 SQL 脚本带来的混乱。你可以防止令人讨厌的“模式漂移”问题,即每个开发者的本地机器行为不一致。最重要的是,你可以使部署变得可预测且无聊(这正是你希望部署所具备的特性)。
在本教程中,你从零开始构建了一个实用的 Spring Boot 应用程序。你学习了 Liquibase 如何拦截应用程序启动、锁定数据库、计算加密校验和,并安全地应用增量变更。你将一个单一的表演进为关系型模式,添加了初始数据,并学习了如何避免初学者常犯的常见陷阱。
下一次你开始一个 Spring Boot 项目时,不要使用手动 SQL 客户端。添加 Liquibase 依赖,创建主变更日志,并从第一天起就开始对数据库进行版本控制。未来的你(以及你的团队)会感谢你的。
OpenText 的软件工程师,热衷于构建可扩展的 Web 应用程序和后端系统。我主要使用 Java、Spring Boot、Python 和现代 Web 技术。我喜欢为互联网创造东西,探索新技术,并分享我在过程中的学习成果。
如果你读到这里,请感谢作者,以表达你对他们的关心。说声谢谢
免费学习编程。freeCodeCamp 的开源课程已帮助超过 40,000 人成为开发人员。立即开始
ADVERTISEMENT