一、核心概念
数据库版本管理(也称数据库迁移)是现代应用开发中不可或缺的实践,它确保数据库模式(Schema)与应用程序代码同步演进,支持团队协作、持续集成/持续部署(CI/CD)和回滚。
Liquibase 与 Flyway 核心概念对比
概念 | Liquibase | Flyway |
---|---|---|
核心思想 | 基于变更集 (ChangeSet)。将每次数据库变更(如建表、加字段、改数据)定义为一个可重复执行的 ChangeSet 。 |
基于版本化迁移脚本 (Versioned Migration)。每个迁移脚本代表数据库从一个版本到下一个版本的变更,通常按版本号排序执行。 |
元数据表 | DATABASECHANGELOG :记录已执行的 ChangeSet 的 ID、作者、文件路径、执行时间、哈希值等。 |
flyway_schema_history :记录已执行的迁移脚本的版本号、描述、类型、执行时间、校验和等。 |
变更定义方式 | 支持多种格式:XML(最常用)、YAML、JSON、Groovy。使用标签描述变更。 | 主要使用 SQL 脚本。也支持 Java 类(实现 Callback 或 Migration 接口)。 |
ID 管理 | ChangeSet 有 id 和 author 。Liquibase 使用 id + author + filename 的哈希值来唯一标识一个 ChangeSet 。 |
脚本文件名遵循严格命名约定:V<version>__<description>.sql (版本化) 或 R__<description>.sql (可重复)。版本号是核心标识。 |
可重复迁移 (Repeatable) | runAlways="true" 的 ChangeSet 会在每次启动时检查并执行(如果内容改变)。 |
R__<description>.sql 脚本。Flyway 会比较其校验和,如果改变则重新执行。 |
状态检查 | 启动时,Liquibase 会读取 DATABASECHANGELOG 表,计算本地 ChangeSet 的哈希值,并与表中记录的哈希值比较,判断是否需要执行。 |
启动时,Flyway 会读取 flyway_schema_history 表,获取已执行脚本的列表和校验和,然后扫描文件系统/类路径,按版本号排序未执行的脚本并执行。 |
优势 | - 变更逻辑抽象化(非纯 SQL),跨数据库兼容性好。 - 强大的预条件( preConditions )支持。- 自动生成回滚脚本(部分情况)。 - 更灵活的变更集管理。 |
- 简单、直观、轻量级。 - 直接使用 SQL,对 DBA 更友好,控制力强。 - 执行速度快,逻辑清晰。 - 广泛采用,社区成熟。 |
劣势 | - 学习曲线稍陡(需学习其 DSL)。 - XML/YAML 文件可能冗长。 - 回滚并非总是完美。 |
- 依赖 SQL,跨数据库兼容性差。 - 版本号管理需谨慎,避免冲突。 - 一旦脚本执行,修改需谨慎(通常通过新脚本修复)。 |
二、操作步骤(非常详细)
我们将分别演示 Liquibase 和 Flyway 的完整配置和使用。
方案一:使用 Liquibase
步骤 1:添加 Liquibase 依赖
在 pom.xml
(Maven) 或 build.gradle
(Gradle) 中添加 spring-boot-starter-data-jpa
(或 spring-boot-starter-jdbc
)和 liquibase-core
。
<!-- pom.xml -->
<dependencies>
<!-- Spring Boot Web Starter (包含 JPA) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot JPA Starter (包含 HikariCP, Hibernate) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 数据库驱动 (MySQL 8) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Liquibase Core -->
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
</dependencies>
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'mysql:mysql-connector-java'
implementation 'org.liquibase:liquibase-core'
}
步骤 2:配置 Liquibase (application.properties
)
在 src/main/resources/application.properties
中配置数据库连接和 Liquibase。
# --- 数据源配置 ---
spring.datasource.url=jdbc:mysql://localhost:3306/liquibasedb?useSSL=false&serverTimezone=UTC
spring.datasource.username=liquiuser
spring.datasource.password=liquipass
# driver-class-name usually not needed
# --- JPA/Hibernate 配置 ---
# 禁用 JPA 自动生成 DDL,让 Liquibase 接管
spring.jpa.hibernate.ddl-auto=none
# 显示 SQL (可选)
spring.jpa.show-sql=true
# 格式化 SQL (可选)
spring.jpa.properties.hibernate.format_sql=true
# --- Liquibase 配置 ---
# 主要的 changelog 文件路径 (classpath 下)
# **必须配置!**
spring.liquibase.change-log=classpath:/db/changelog/db.changelog-master.xml
# Liquibase 上下文 (可选,用于条件执行)
# spring.liquibase.contexts=dev,test,prod
# Liquibase 标签 (可选,用于标记版本)
# spring.liquibase.label-filter=prod
# 是否在启动时自动运行 Liquibase
spring.liquibase.enabled=true
# 数据库默认 Schema (可选,如 PostgreSQL)
# spring.liquibase.default-schema=public
# 数据库默认 Catalog (可选)
# spring.liquibase.default-catalog-name=mycatalog
# Liquibase 用户 (如果与数据源用户不同)
# spring.liquibase.user=liquibase_user
# Liquibase 密码 (如果与数据源密码不同)
# spring.liquibase.password=liquibase_pass
步骤 3:创建主变更日志文件 (db.changelog-master.xml
)
创建目录 src/main/resources/db/changelog/
,并创建主文件 db.changelog-master.xml
。这是 Liquibase 的入口点,它会包含其他具体的变更文件。
<!-- src/main/resources/db/changelog/db.changelog-master.xml -->
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd
http://www.liquibase.org/xml/ns/pro
http://www.liquibase.org/xml/ns/pro/liquibase-pro-4.27.xsd">
<!-- 包含第一个变更文件 -->
<include file="db/changelog/changes/changelog-1.0.0.xml" relativeToChangelogFile="true"/>
<!-- 在此处包含后续的变更文件,例如:
<include file="db/changelog/changes/changelog-1.1.0.xml" relativeToChangelogFile="true"/>
-->
</databaseChangeLog>
步骤 4:创建具体的变更文件 (changelog-1.0.0.xml
)
创建目录 src/main/resources/db/changelog/changes/
,并创建第一个变更文件 changelog-1.0.0.xml
。
<!-- src/main/resources/db/changelog/changes/changelog-1.0.0.xml -->
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<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.27.xsd">
<!-- ChangeSet 1: 创建 users 表 -->
<changeSet id="1.0.0-1" author="zhangsan">
<preConditions onFail="HALT" onError="HALT">
<!-- 确保表不存在才执行 -->
<not>
<tableExists tableName="users"/>
</not>
</preConditions>
<createTable tableName="users">
<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>
<column name="email" type="VARCHAR(100)">
<constraints nullable="false" unique="true"/>
</column>
<column name="created_at" type="DATETIME">
<constraints nullable="false"/>
</column>
<column name="updated_at" type="DATETIME">
<constraints nullable="false"/>
</column>
</createTable>
<!-- 创建索引 -->
<createIndex tableName="users" indexName="idx_users_email">
<column name="email"/>
</createIndex>
</changeSet>
<!-- ChangeSet 2: 插入初始数据 -->
<changeSet id="1.0.0-2" author="zhangsan">
<preConditions onFail="CONTINUE"> <!-- 如果数据已存在,跳过此 ChangeSet -->
<sqlCheck expectedResult="0">
SELECT COUNT(*) FROM users WHERE email = 'admin@company.com'
</sqlCheck>
</preConditions>
<insert tableName="users">
<column name="first_name" value="Admin"/>
<column name="last_name" value="User"/>
<column name="email" value="admin@company.com"/>
<column name="created_at" valueComputed="NOW()"/>
<column name="updated_at" valueComputed="NOW()"/>
</insert>
</changeSet>
</databaseChangeLog>
步骤 5:启动应用并验证
- 启动 Spring Boot 应用。
- Liquibase 会在启动时自动执行:
- 检查
DATABASECHANGELOG
表是否存在,不存在则创建。 - 读取
db.changelog-master.xml
。 - 找到
changelog-1.0.0.xml
。 - 依次执行其中的
ChangeSet
。 - 执行成功后,将
ChangeSet
的信息(id, author, filename, hash)插入DATABASECHANGELOG
表。
- 检查
- 检查数据库:
- 应存在
users
表。 - 应存在
DATABASECHANGELOG
表,且有两条记录对应两个ChangeSet
。
- 应存在
方案二:使用 Flyway
步骤 1:添加 Flyway 依赖
在 pom.xml
(Maven) 或 build.gradle
(Gradle) 中添加 spring-boot-starter-data-jpa
(或 spring-boot-starter-jdbc
)和 flyway-core
。
<!-- pom.xml -->
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot JPA Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 数据库驱动 (MySQL 8) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Flyway Core -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
</dependencies>
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'mysql:mysql-connector-java'
implementation 'org.flywaydb:flyway-core'
}
步骤 2:配置 Flyway (application.properties
)
在 src/main/resources/application.properties
中配置数据库连接和 Flyway。
# --- 数据源配置 ---
spring.datasource.url=jdbc:mysql://localhost:3306/flywaydb?useSSL=false&serverTimezone=UTC
spring.datasource.username=flyuser
spring.datasource.password=flypass
# driver-class-name usually not needed
# --- JPA/Hibernate 配置 ---
# 禁用 JPA 自动生成 DDL
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# --- Flyway 配置 ---
# 迁移脚本的位置 (classpath 下)
# **必须配置!**
spring.flyway.locations=classpath:db/migration
# Flyway 的基线版本 (可选,用于已有数据库)
# spring.flyway.baseline-on-migrate=true
# spring.flyway.baseline-version=1.0.0
# 是否在启动时自动运行 Flyway
spring.flyway.enabled=true
# 编码
spring.flyway.encoding=UTF-8
# 是否允许无版本迁移 (不推荐)
# spring.flyway.out-of-order=false
# 是否自动修复损坏的校验和 (谨慎使用)
# spring.flyway.clean-disabled=true # 确保 clean 命令被禁用 (生产环境)
# 数据库默认 Schema (可选)
# spring.flyway.schemas=public
# 数据库默认 Catalog (可选)
# spring.flyway.default-catalog-name=mycatalog
# Flyway 用户/密码 (如果与数据源不同)
# spring.flyway.user=flyway_user
# spring.flyway.password=flyway_pass
步骤 3:创建迁移脚本目录和文件
创建目录 src/main/resources/db/migration/
。
创建第一个迁移脚本 V1.0.0__Create_users_table_and_insert_data.sql
。文件名命名规则至关重要:
V
: 表示版本化迁移(Versioned Migration)。1.0.0
: 版本号,按字典序排序。__
: 两个下划线,分隔版本号和描述。Create_users_table_and_insert_data
: 描述,可包含下划线_
。
-- src/main/resources/db/migration/V1.0.0__Create_users_table_and_insert_data.sql
-- 创建 users 表
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
-- 创建索引
CREATE INDEX idx_users_email ON users(email);
-- 插入初始数据
INSERT INTO users (first_name, last_name, email, created_at, updated_at)
VALUES ('Admin', 'User', 'admin@company.com', NOW(), NOW());
步骤 4:启动应用并验证
- 启动 Spring Boot 应用。
- Flyway 会在启动时自动执行:
- 检查
flyway_schema_history
表是否存在,不存在则创建。 - 扫描
db/migration
目录下的 SQL 脚本,按版本号排序。 - 找到
V1.0.0__...
脚本。 - 执行该脚本中的 SQL 语句。
- 执行成功后,将脚本信息(版本号、描述、校验和、执行时间等)插入
flyway_schema_history
表。
- 检查
- 检查数据库:
- 应存在
users
表。 - 应存在
flyway_schema_history
表,且有一条记录对应V1.0.0
脚本。
- 应存在
三、常见错误与解决方案
错误 | 原因 | 解决方案 |
---|---|---|
Liquibase: Unexpected error running Liquibase: Validation Failed |
ChangeSet 内容改变但未正确处理(如修改了已执行的 ChangeSet )。 |
1. 不要修改已执行的 ChangeSet 。2. 如果必须修改,使用 <validCheckSum> 标签标记旧哈希值为有效,或使用 runOnChange="true" 。3. 优先创建新的 ChangeSet 来修复问题。 |
Flyway: Validate failed: Migration checksum mismatch |
已执行的迁移脚本文件内容被修改。 | 1. 绝对不要修改已执行的迁移脚本文件。2. 如果修改了,Flyway 会拒绝启动。3. 解决方案:创建一个新的迁移脚本来修复(如 V1.0.1__Fix_users_table.sql ),或(仅在开发环境)使用 flyway repair 命令重置校验和(生产环境禁用)。 |
Liquibase: Could not find database driver |
数据库驱动未正确添加或版本不兼容。 | 1. 检查 pom.xml /build.gradle 是否包含正确的数据库驱动依赖。2. 确认驱动版本与数据库和 Spring Boot 兼容。 |
Flyway: Unable to connect to database |
数据库连接配置错误或数据库未启动。 | 1. 检查 spring.datasource.url , username , password 。2. 确认数据库服务正在运行。3. 检查网络和防火墙。 |
Liquibase: Precondition Error |
preConditions 不满足(如表已存在但 ChangeSet 要求不存在)。 |
1. 检查 preConditions 逻辑是否正确。2. 如果是预期情况(如重复执行),调整 onFail 策略(如 CONTINUE )。3. 确认数据库状态。 |
Flyway: Found non-empty schema(s) ... without schema history table |
目标数据库非空,且没有 flyway_schema_history 表。 |
1. 首次迁移:使用 spring.flyway.baseline-on-migrate=true 和 spring.flyway.baseline-version=<current_version> 命令基线。2. 已有迁移:确保 locations 配置正确,脚本能应用到当前状态。 |
通用: Failed to execute SQL script |
SQL 语法错误或违反数据库约束。 | 1. 检查 SQL 脚本的语法(特别是分号、引号)。2. 确保数据库支持该语法。3. 检查外键、唯一键等约束。 |
四、注意事项
ddl-auto=none
:必须将spring.jpa.hibernate.ddl-auto
设置为none
,否则 JPA 会尝试根据实体生成 DDL,与 Liquibase/Flyway 冲突。- 不可变性:
- Liquibase: 避免修改已执行的
ChangeSet
。使用runOnChange="true"
或创建新ChangeSet
。 - Flyway: 绝对禁止修改已执行的迁移脚本文件。任何变更都必须通过新的迁移脚本。
- Liquibase: 避免修改已执行的
- 版本号管理 (Flyway):团队协作时,版本号可能冲突。约定规则(如
V<feature_branch>_<seq>
临时,合并后重排)或使用分支版本(V1.1.0_a_1
)。 locations
/change-log
路径:确保路径正确,文件在src/main/resources
下。- 数据库兼容性:
- Liquibase 的 XML/YAML 更易跨数据库。
- Flyway 的 SQL 需针对目标数据库编写。可使用
spring.flyway.locations=classpath:db/migration/{vendor}
实现数据库特定脚本。
- 生产环境安全:
- 禁用
flyway.clean
(spring.flyway.clean-disabled=true
)。 - 禁用
liquibase.dropAll
。 - 仔细审查每个迁移脚本。
- 禁用
- 回滚策略:
- Liquibase 可尝试生成回滚 SQL(
rollback
标签或命令),但需测试。 - Flyway 不支持自动回滚。回滚必须通过编写新的反向迁移脚本(如
V1.0.1__Undo_something.sql
)实现。
- Liquibase 可尝试生成回滚 SQL(
flyway_schema_history
/DATABASECHANGELOG
表:不要手动修改这些表,它们由工具管理。
五、使用技巧
Liquibase 技巧
runOnChange="true"
:当ChangeSet
内容改变时,每次启动都会重新执行。适合存储过程、视图、可重复数据加载。runAlways="true"
:每次启动都执行(如果内容改变)。类似runOnChange
。preConditions
:强大的条件控制,如tableExists
,columnExists
,sqlCheck
,dbms
。<rollback>
:为ChangeSet
定义回滚操作。<modifySql>
:修改生成的 SQL 以适应特定数据库。--diff
命令:比较数据库与实体类,生成变更脚本。--generateChangeLog
:从现有数据库生成初始变更日志。
Flyway 技巧
- 可重复迁移
R__
:脚本名以R__
开头。Flyway 会比较其校验和,如果改变则重新执行。适合视图、存储过程、参考数据。 - 回调 (Callbacks):在特定生命周期点执行 SQL 或 Java 代码。
V1__before_migrate.sql
(文件名模式)afterMigrate.sql
(文件名模式)- 实现
SpringLiquibase.Callback
接口(Flyway 也有类似机制)。
- 占位符替换:
- 在 SQL 中使用
${placeholder}
。 - 配置
spring.flyway.placeholders.key=value
。
- 在 SQL 中使用
outOfOrder
:允许执行版本号小于当前版本的迁移(谨慎使用)。repair
命令:修复元数据表(如解决校验和不匹配、标记为未执行)。info
命令:查看迁移状态。validate
命令:验证已应用的迁移与文件系统上的迁移是否匹配。
六、最佳实践
- 选择工具:
- 优先 Flyway:简单、直接、社区广泛采用。适合大多数项目。
- 选择 Liquibase:需要强跨数据库兼容性、复杂变更逻辑、或团队偏好声明式变更。
- 原子化变更:每个
ChangeSet
或迁移脚本应尽可能小且原子化,只做一件事。 - 清晰的命名和描述:
- Liquibase:
id
和author
要有意义。 - Flyway: 脚本描述要清晰 (
V1.0.0__Add_email_index_to_users.sql
)。
- Liquibase:
- 版本控制:将变更文件/脚本纳入 Git 等版本控制系统。
- 代码审查:所有数据库变更必须经过代码审查。
- 测试:
- 在 CI/CD 流水线中测试迁移。
- 使用测试数据库或内存数据库(H2)进行集成测试。
- 备份:在生产环境执行重大迁移前,务必备份数据库。
- 文档:在变更中添加注释,说明变更原因。
- 环境一致性:确保开发、测试、生产环境的迁移流程一致。
- 监控:监控迁移执行时间和状态。
七、性能优化
- 批量操作:
- 在迁移脚本中执行大量
INSERT
/UPDATE
时,使用批量语句(INSERT INTO ... VALUES (...), (...), (...)
)或数据库特定的批量导入工具。
- 在迁移脚本中执行大量
- 索引管理:
- 在大量数据插入/更新前,考虑暂时删除非关键索引,操作完成后再重建,以提高性能。
- 长事务:
- 避免在单个迁移脚本中执行耗时过长的操作,防止锁表时间过长。可拆分脚本或在低峰期执行。
- 连接池:
- 确保数据源连接池(如 HikariCP)配置合理,有足够的连接用于迁移。
- 并行执行 (Flyway Pro / Liquibase Pro):
- 专业版支持并行执行独立的迁移,可显著缩短大型迁移时间。
- 增量迁移:
- 避免一次性迁移巨大变更。将大变更拆分为多个小的、可独立部署的迁移。
validateOnMigrate
:Flyway 默认true
,会验证所有已应用的迁移。如果迁移历史很长,可考虑在受控环境中设为false
(但有风险)。- Liquibase
diff
性能:对大型数据库执行diff
可能很慢。确保在性能足够的环境中运行。
总结
Liquibase 和 Flyway 都是优秀的数据库版本管理工具。
- Flyway:推荐大多数场景。简单、可靠、基于 SQL,学习成本低,社区支持好。坚持“一次编写,永不修改”的原则。
- Liquibase:适合需要跨数据库兼容性、复杂变更逻辑或声明式变更管理的场景。提供了更丰富的抽象和功能。
快速上手步骤:
- 选工具:根据项目需求选择 Liquibase 或 Flyway。
- 加依赖:添加
spring-boot-starter-data-jpa
和liquibase-core
或flyway-core
。 - 配数据源:
application.properties
中配置spring.datasource.*
。 - 禁 JPA DDL:
spring.jpa.hibernate.ddl-auto=none
。 - 配工具:
- Liquibase:
spring.liquibase.change-log=classpath:/db/changelog/db.changelog-master.xml
- Flyway:
spring.flyway.locations=classpath:db/migration
- Liquibase:
- 写变更:
- Liquibase: 创建
db.changelog-master.xml
->include
-> 写changelog-X.X.X.xml
。 - Flyway: 在
db/migration
下创建V1.0.0__Description.sql
。
- Liquibase: 创建
- 启动应用:工具会自动执行未应用的变更。
- 牢记原则:不要修改已执行的变更!
通过遵循最佳实践,你可以确保数据库模式与应用代码协同演进,构建健壮、可维护的应用系统。