一、核心概念

数据库版本管理(也称数据库迁移)是现代应用开发中不可或缺的实践,它确保数据库模式(Schema)与应用程序代码同步演进,支持团队协作、持续集成/持续部署(CI/CD)和回滚。

Liquibase 与 Flyway 核心概念对比

概念 Liquibase Flyway
核心思想 基于变更集 (ChangeSet)。将每次数据库变更(如建表、加字段、改数据)定义为一个可重复执行的 ChangeSet 基于版本化迁移脚本 (Versioned Migration)。每个迁移脚本代表数据库从一个版本到下一个版本的变更,通常按版本号排序执行。
元数据表 DATABASECHANGELOG:记录已执行的 ChangeSet 的 ID、作者、文件路径、执行时间、哈希值等。 flyway_schema_history:记录已执行的迁移脚本的版本号、描述、类型、执行时间、校验和等。
变更定义方式 支持多种格式:XML(最常用)、YAMLJSONGroovy。使用标签描述变更。 主要使用 SQL 脚本。也支持 Java 类(实现 CallbackMigration 接口)。
ID 管理 ChangeSetidauthor。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:启动应用并验证

  1. 启动 Spring Boot 应用。
  2. Liquibase 会在启动时自动执行:
    • 检查 DATABASECHANGELOG 表是否存在,不存在则创建。
    • 读取 db.changelog-master.xml
    • 找到 changelog-1.0.0.xml
    • 依次执行其中的 ChangeSet
    • 执行成功后,将 ChangeSet 的信息(id, author, filename, hash)插入 DATABASECHANGELOG 表。
  3. 检查数据库:
    • 应存在 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:启动应用并验证

  1. 启动 Spring Boot 应用。
  2. Flyway 会在启动时自动执行:
    • 检查 flyway_schema_history 表是否存在,不存在则创建。
    • 扫描 db/migration 目录下的 SQL 脚本,按版本号排序。
    • 找到 V1.0.0__... 脚本。
    • 执行该脚本中的 SQL 语句。
    • 执行成功后,将脚本信息(版本号、描述、校验和、执行时间等)插入 flyway_schema_history 表。
  3. 检查数据库:
    • 应存在 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=truespring.flyway.baseline-version=<current_version> 命令基线。2. 已有迁移:确保 locations 配置正确,脚本能应用到当前状态。
通用: Failed to execute SQL script SQL 语法错误或违反数据库约束。 1. 检查 SQL 脚本的语法(特别是分号、引号)。2. 确保数据库支持该语法。3. 检查外键、唯一键等约束。

四、注意事项

  1. ddl-auto=none必须spring.jpa.hibernate.ddl-auto 设置为 none,否则 JPA 会尝试根据实体生成 DDL,与 Liquibase/Flyway 冲突。
  2. 不可变性
    • Liquibase: 避免修改已执行的 ChangeSet。使用 runOnChange="true" 或创建新 ChangeSet
    • Flyway: 绝对禁止修改已执行的迁移脚本文件。任何变更都必须通过新的迁移脚本。
  3. 版本号管理 (Flyway):团队协作时,版本号可能冲突。约定规则(如 V<feature_branch>_<seq> 临时,合并后重排)或使用分支版本(V1.1.0_a_1)。
  4. locations / change-log 路径:确保路径正确,文件在 src/main/resources 下。
  5. 数据库兼容性
    • Liquibase 的 XML/YAML 更易跨数据库。
    • Flyway 的 SQL 需针对目标数据库编写。可使用 spring.flyway.locations=classpath:db/migration/{vendor} 实现数据库特定脚本。
  6. 生产环境安全
    • 禁用 flyway.cleanspring.flyway.clean-disabled=true)。
    • 禁用 liquibase.dropAll
    • 仔细审查每个迁移脚本。
  7. 回滚策略
    • Liquibase 可尝试生成回滚 SQL(rollback 标签或命令),但需测试。
    • Flyway 不支持自动回滚。回滚必须通过编写新的反向迁移脚本(如 V1.0.1__Undo_something.sql)实现。
  8. flyway_schema_history / DATABASECHANGELOG:不要手动修改这些表,它们由工具管理。

五、使用技巧

Liquibase 技巧

  1. runOnChange="true":当 ChangeSet 内容改变时,每次启动都会重新执行。适合存储过程、视图、可重复数据加载。
  2. runAlways="true":每次启动都执行(如果内容改变)。类似 runOnChange
  3. preConditions:强大的条件控制,如 tableExists, columnExists, sqlCheck, dbms
  4. <rollback>:为 ChangeSet 定义回滚操作。
  5. <modifySql>:修改生成的 SQL 以适应特定数据库。
  6. --diff 命令:比较数据库与实体类,生成变更脚本。
  7. --generateChangeLog:从现有数据库生成初始变更日志。

Flyway 技巧

  1. 可重复迁移 R__:脚本名以 R__ 开头。Flyway 会比较其校验和,如果改变则重新执行。适合视图、存储过程、参考数据。
  2. 回调 (Callbacks):在特定生命周期点执行 SQL 或 Java 代码。
    • V1__before_migrate.sql (文件名模式)
    • afterMigrate.sql (文件名模式)
    • 实现 SpringLiquibase.Callback 接口(Flyway 也有类似机制)。
  3. 占位符替换
    • 在 SQL 中使用 ${placeholder}
    • 配置 spring.flyway.placeholders.key=value
  4. outOfOrder:允许执行版本号小于当前版本的迁移(谨慎使用)。
  5. repair 命令:修复元数据表(如解决校验和不匹配、标记为未执行)。
  6. info 命令:查看迁移状态。
  7. validate 命令:验证已应用的迁移与文件系统上的迁移是否匹配。

六、最佳实践

  1. 选择工具
    • 优先 Flyway:简单、直接、社区广泛采用。适合大多数项目。
    • 选择 Liquibase:需要强跨数据库兼容性、复杂变更逻辑、或团队偏好声明式变更。
  2. 原子化变更:每个 ChangeSet 或迁移脚本应尽可能小且原子化,只做一件事。
  3. 清晰的命名和描述
    • Liquibase: idauthor 要有意义。
    • Flyway: 脚本描述要清晰 (V1.0.0__Add_email_index_to_users.sql)。
  4. 版本控制:将变更文件/脚本纳入 Git 等版本控制系统。
  5. 代码审查:所有数据库变更必须经过代码审查。
  6. 测试
    • 在 CI/CD 流水线中测试迁移。
    • 使用测试数据库或内存数据库(H2)进行集成测试。
  7. 备份:在生产环境执行重大迁移前,务必备份数据库。
  8. 文档:在变更中添加注释,说明变更原因。
  9. 环境一致性:确保开发、测试、生产环境的迁移流程一致。
  10. 监控:监控迁移执行时间和状态。

七、性能优化

  1. 批量操作
    • 在迁移脚本中执行大量 INSERT/UPDATE 时,使用批量语句(INSERT INTO ... VALUES (...), (...), (...))或数据库特定的批量导入工具。
  2. 索引管理
    • 在大量数据插入/更新前,考虑暂时删除非关键索引,操作完成后再重建,以提高性能。
  3. 长事务
    • 避免在单个迁移脚本中执行耗时过长的操作,防止锁表时间过长。可拆分脚本或在低峰期执行。
  4. 连接池
    • 确保数据源连接池(如 HikariCP)配置合理,有足够的连接用于迁移。
  5. 并行执行 (Flyway Pro / Liquibase Pro)
    • 专业版支持并行执行独立的迁移,可显著缩短大型迁移时间。
  6. 增量迁移
    • 避免一次性迁移巨大变更。将大变更拆分为多个小的、可独立部署的迁移。
  7. validateOnMigrate:Flyway 默认 true,会验证所有已应用的迁移。如果迁移历史很长,可考虑在受控环境中设为 false(但有风险)。
  8. Liquibase diff 性能:对大型数据库执行 diff 可能很慢。确保在性能足够的环境中运行。

总结

Liquibase 和 Flyway 都是优秀的数据库版本管理工具。

  • Flyway推荐大多数场景。简单、可靠、基于 SQL,学习成本低,社区支持好。坚持“一次编写,永不修改”的原则。
  • Liquibase:适合需要跨数据库兼容性、复杂变更逻辑或声明式变更管理的场景。提供了更丰富的抽象和功能。

快速上手步骤

  1. 选工具:根据项目需求选择 Liquibase 或 Flyway。
  2. 加依赖:添加 spring-boot-starter-data-jpaliquibase-coreflyway-core
  3. 配数据源application.properties 中配置 spring.datasource.*
  4. 禁 JPA DDLspring.jpa.hibernate.ddl-auto=none
  5. 配工具
    • Liquibase: spring.liquibase.change-log=classpath:/db/changelog/db.changelog-master.xml
    • Flyway: spring.flyway.locations=classpath:db/migration
  6. 写变更
    • Liquibase: 创建 db.changelog-master.xml -> include -> 写 changelog-X.X.X.xml
    • Flyway: 在 db/migration 下创建 V1.0.0__Description.sql
  7. 启动应用:工具会自动执行未应用的变更。
  8. 牢记原则不要修改已执行的变更

通过遵循最佳实践,你可以确保数据库模式与应用代码协同演进,构建健壮、可维护的应用系统。