背景
数据库是应用程序的核心,而数据库 schema 的变更管理长期以来都是软件开发中的痛点。当团队成员各自修改本地数据库、测试环境和生产环境出现不一致、手动执行 SQL 脚本导致错误时,你是否曾感到束手无策?
Flyway 正是为解决这些问题而生的。作为一款成熟的数据库版本控制和迁移工具,Flyway 能够帮助团队自动化、安全地管理数据库变更,让数据库 schema 的演进像代码版本管理一样可靠和可控。
本文将带大家系统地理解 Flyway 的工作原理,并通过一个完整的实战示例,掌握在 Java 项目中使用 Flyway 进行数据库迁移的核心技能。无论你是刚刚接触数据库迁移的新手,还是希望规范化现有项目数据库管理的开发者,都能从中获得有价值的参考。
目录
- [一、Flyway 是什么](#一、Flyway 是什么 "#%E4%B8%80flyway-%E6%98%AF%E4%BB%80%E4%B9%88")
- [二、Flyway 工作原理详解](#二、Flyway 工作原理详解 "#%E4%BA%8Cflyway-%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3")
- 三、环境准备
- [四、创建 Maven 项目](#四、创建 Maven 项目 "#%E5%9B%9B%E5%88%9B%E5%BB%BA-maven-%E9%A1%B9%E7%9B%AE")
- 五、配置项目依赖
- 六、编写数据库迁移脚本
- [七、集成 Flyway 到 Java 代码](#七、集成 Flyway 到 Java 代码 "#%E4%B8%83%E9%9B%86%E6%88%90-flyway-%E5%88%B0-java-%E4%BB%A3%E7%A0%81")
- 八、执行迁移验证
- [九、Flyway API 进阶用法](#九、Flyway API 进阶用法 "#%E4%B9%9Dflyway-api-%E8%BF%9B%E9%98%B6%E7%94%A8%E6%B3%95")
- [十、与 Spring Boot 集成](#十、与 Spring Boot 集成 "#%E5%8D%81%E4%B8%8E-spring-boot-%E9%9B%86%E6%88%90")
- 十一、完整项目结构
- 十二、总结与延伸
一、Flyway 是什么
Flyway 是 Redgate 公司开发的一款开源数据库迁移工具,其核心理念是「把数据库当作代码来管理」。它通过版本化的迁移脚本,确保数据库的每一次变更都能被追踪、复现和回滚。
在 Flyway 的世界里,数据库的当前状态由一系列按版本号排列的迁移脚本决定。就像 Git 管理代码版本一样,Flyway 管理数据库 schema 的版本历史。每当需要修改数据库时,你不再直接操作数据库,而是编写一个新的迁移脚本,然后由 Flyway 负责将这个脚本安全地应用到目标数据库。
这种做法的价值在于:数据库变更不再是黑箱操作,而是透明、可审计、可重复的过程。无论是在开发者的本地环境、CI/CD 构建环境,还是在生产服务器上,只要执行相同的迁移脚本序列,数据库最终都会达到一致的状态。
二、Flyway 工作原理详解
理解 Flyway 的工作原理,是正确使用它的前提。让我们从最简单的场景开始,逐步揭开 Flyway 的神秘面纱。
空数据库的初始化
当你第一次在空数据库上运行 Flyway 时,整个流程如下:首先,Flyway 会在数据库中查找一个名为 flyway_schema_history 的元数据表。如果数据库是空的,这个表自然不存在,Flyway 就会自动创建它。这个表是 Flyway 追踪迁移历史的核心,它记录了每个迁移版本的信息,包括版本号、描述、执行时间、成功与否等关键数据。
创建元数据表后,Flyway 会扫描配置好的迁移脚本目录。这些脚本可以存放在文件系统中,也可以打包在应用程序的 classpath 里。Flyway 支持用 SQL 编写的迁移脚本,也支持用 Java 编写的迁移脚本,甚至支持其他脚本语言。
迁移脚本的版本化
每个迁移脚本都有一个版本号,这是 Flyway 确定执行顺序的关键。版本号的格式通常是 V{版本}__{描述}.sql,其中 V 前缀表示这是一个版本化迁移,双下划线用于分隔版本号和描述文字。
markdown
V1__Create_person_table.sql → 版本 1
V2__Add_people.sql → 版本 2
V3__Create_orders_table.sql → 版本 3
Flyway 按照版本号的数字顺序依次执行迁移。如果当前数据库处于版本 1,而文件系统中有版本 1 到版本 5 的迁移脚本,Flyway 会依次执行版本 2、3、4、5,跳过已经执行过的版本 1。
每执行一个迁移脚本,Flyway 都会在 flyway_schema_history 表中记录一条新记录。这条记录包含版本号、迁移描述、检查和、执行时间等字段。通过这张表,Flyway 能够精确地知道数据库当前处于哪个版本,以及哪些迁移已经成功执行。
sql
flyway_schema_history 表结构:
┌──────────┬─────────────────────────┬────────┬──────────────┐
│ version │ description │ type │ installed_on │
├──────────┼─────────────────────────┼────────┼──────────────┤
│ 1 │ Create person table │ SQL │ 2025-01-15 │
│ 2 │ Add people │ SQL │ 2025-01-15 │
└──────────┴─────────────────────────┴────────┴──────────────┘
增量迁移的机制
当数据库已经处于某个版本,Flyway 如何处理新增的迁移脚本?这里需要引入「待处理迁移」的概念。
假设数据库当前版本是 2,而文件系统中有版本 1 到版本 5 的迁移脚本。Flyway 会将版本 1 和版本 2 与 flyway_schema_history 表中的记录进行比对,发现它们已经执行过,于是将它们标记为「已忽略」。剩余的版本 3、4、5 就是「待处理迁移」------它们存在于文件系统中,但尚未被应用到数据库。
这种增量迁移的机制带来了几个重要好处:
- 幂等性:支持多次、重复执行迁移而不会造成数据丢失或重复执行
- 并行开发:允许多个开发者并行工作,每个人的本地环境可能处于不同版本,但最终都能汇聚到一致的状态
- 可追溯 :每一次变更是哪一次部署引入的、对应哪个版本的代码,都能通过
flyway_schema_history表一目了然
三、环境准备
在开始实战之前,确保你的开发环境满足以下要求。
系统要求
Flyway 10.x 及以上版本要求 Java 17 或更高版本运行环境。如果你的项目仍在使用较旧版本的 Java,需要先升级后再使用新版 Flyway。
bash
$ java -version
openjdk version "17.0.9" 2023-10-17
OpenJDK Runtime Environment (Ubuntu)
OpenJDK 64-Bit Server VM
除了 Java,你还需要安装 Maven 3.x 或 Gradle 作为构建工具。如果你更习惯使用命令行工具,也可以单独下载 Flyway 的命令行版本。
选择合适的数据库
Flyway 支持几乎所有主流的关系型数据库,包括 MySQL、PostgreSQL、Oracle、SQL Server、DB2、SQLite、H2 等。对于入门学习来说,H2 数据库是一个理想的选择------它是一款轻量级的内存数据库或文件数据库,配置简单、启动快速,非常适合用于开发和测试环境验证 Flyway 的功能。
当你准备好切换到生产环境时,只需要更换 JDBC 驱动和连接字符串,其他代码几乎不需要改动。
四、创建 Maven 项目
我们使用 Maven 来管理项目依赖和构建流程。首先,通过 Maven Archetype 插件生成一个标准化的 Java 项目骨架:
bash
mvn archetype:generate -B \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.1 \
-DgroupId=com.example \
-DartifactId=flyway-demo \
-Dversion=1.0-SNAPSHOT \
-Dpackage=com.example.flyway
命令执行成功后,进入生成的项目目录:
bash
cd flyway-demo
此时,项目的基本结构如下:
css
flyway-demo/
├── pom.xml
└── src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── flyway/
│ │ └── App.java
│ └── resources/
└── test/
└── java/
└── com/
└── example/
└── flyway/
└── AppTest.java
五、配置项目依赖
打开 pom.xml 文件,在 <dependencies> 节点中添加 Flyway 和 H2 数据库的依赖。完整的配置如下:
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>flyway-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<flyway.version>10.8.1</flyway.version>
<h2.version>2.2.224</h2.version>
</properties>
<dependencies>
<!-- Flyway 数据库迁移核心库 -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>${flyway.version}</version>
</dependency>
<!-- Flyway H2 数据库支持模块 -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-h2</artifactId>
<version>${flyway.version}</version>
</dependency>
<!-- H2 内存数据库 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
</dependency>
<!-- JUnit 测试框架 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
</plugin>
</plugins>
</build>
</project>
【重要说明】 关于 Flyway 的模块化架构,从 Flyway 10 开始,核心库和数据库特定的功能被拆分为独立模块。如果你的应用只需要连接一种数据库,只需要引入 flyway-core 和对应的 flyway-database-xxx 模块即可。
六、编写数据库迁移脚本
迁移脚本是 Flyway 实现数据库版本管理的核心。我们需要在 src/main/resources 目录下创建 db/migration 文件夹,用于存放所有的迁移脚本:
bash
mkdir -p src/main/resources/db/migration
第一个迁移:创建表结构
创建第一个迁移脚本 V1__Create_person_table.sql,用于定义 PERSON 表的结构:
sql
-- V1__Create_person_table.sql
-- 描述:创建 PERSON 表
-- 版本号:1
create table PERSON (
ID int not null,
NAME varchar(100) not null
);
【命名规范】 文件名由三部分组成:
- 版本前缀
V - 版本号(必须是唯一的正整数)
- 双下划线
__分隔符 - 描述性名称
第二个迁移:插入初始数据
创建第二个迁移脚本 V2__Add_people.sql,用于向 PERSON 表插入初始数据:
sql
-- V2__Add_people.sql
-- 描述:插入初始数据
-- 版本号:2
insert into PERSON (ID, NAME) values (1, 'Alice');
insert into PERSON (ID, NAME) values (2, 'Bob');
insert into PERSON (ID, NAME) values (3, 'Charlie');
迁移脚本的最佳实践
可重复执行是基本原则。 每个迁移脚本都应该设计成可以安全地重复执行而不会导致错误。
sql
-- 推荐写法:使用 ON DUPLICATE KEY UPDATE (MySQL)
INSERT INTO PERSON (ID, NAME) VALUES (1, 'Alice')
ON DUPLICATE KEY UPDATE NAME = VALUES(NAME);
回滚脚本的设计。 虽然 Flyway 的免费版本不提供自动回滚功能,但你可以通过编写 U 前缀的撤销迁移(Undo Migration)来实现手动回滚。
七、集成 Flyway 到 Java 代码
现在,我们需要在 Java 代码中初始化并使用 Flyway。打开 src/main/java/com/example/flyway/App.java 文件,将其内容替换为以下代码:
java
package com.example.flyway;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.output.InfoResult;
/**
* Flyway 数据库迁移入门示例
*
* 本示例演示了如何使用 Flyway API 执行数据库迁移,
* 包括配置数据源、执行迁移操作、查询迁移状态等核心功能。
*/
public class App {
public static void main(String[] args) {
// ========== 第一步:配置数据源 ==========
// Flyway 通过 JDBC 连接数据库
// 这里使用 H2 文件数据库,数据库文件存储在 ./target/flyway-demo
String jdbcUrl = "jdbc:h2:file:./target/flyway-demo";
String username = "sa"; // H2 默认用户名
String password = null; // H2 默认无密码
// 创建 Flyway 实例
Flyway flyway = Flyway.configure()
.dataSource(jdbcUrl, username, password)
// 可选:设置迁移脚本的目录位置
// 默认值是 classpath:db/migration
// 如果使用文件系统路径,使用 filesystem:src/main/resources/db/migration
.locations("filesystem:src/main/resources/db/migration")
.load();
// ========== 第二步:查询当前迁移状态 ==========
// 在执行迁移前,先了解数据库当前的状态
System.out.println("=== 数据库当前状态 ===");
InfoResult info = flyway.info();
System.out.println("当前版本: " + (info.current == null ? "空数据库" : info.current.version));
System.out.println("已执行迁移数量: " + info.all.length);
// ========== 第三步:执行迁移 ==========
// migrate() 方法会:
// 1. 检查是否存在 flyway_schema_history 元数据表
// 2. 扫描配置的迁移脚本目录
// 3. 识别待处理的迁移脚本
// 4. 按版本号顺序执行待处理迁移
System.out.println("\n=== 开始执行迁移 ===");
flyway.migrate();
// ========== 第四步:验证迁移结果 ==========
System.out.println("\n=== 迁移后状态 ===");
InfoResult infoAfter = flyway.info();
System.out.println("当前版本: " + infoAfter.current.version);
System.out.println("已执行迁移数量: " + infoAfter.all.length);
System.out.println("\n数据库迁移完成!");
}
}
这段代码涵盖了 Flyway API 最核心的使用模式。创建 Flyway 实例需要两步:configure() 返回一个配置构建器,用于设置数据源和各类参数;load() 方法根据配置创建可执行的 Flyway 实例。migrate() 是最常用的方法,它会智能地检测当前状态并只执行必要的迁移。
八、执行迁移验证
现在让我们运行程序,验证 Flyway 是否能正确执行迁移:
bash
mvn package exec:java -Dexec.mainClass=com.example.flyway.App
首次执行的输出类似于:
vbnet
=== 数据库当前状态 ===
当前版本: 空数据库
已执行迁移数量: 0
=== 开始执行迁移 ===
INFO: Creating schema history table: "PUBLIC"."flyway_schema_history"
INFO: Current version of schema "PUBLIC": << Empty Schema >>
INFO: Migrating schema "PUBLIC" to version 1 - Create person table
INFO: Successfully applied 1 migration to schema "PUBLIC" (execution time 00:00.045s).
INFO: Migrating schema "PUBLIC" to version 2 - Add people
INFO: Successfully applied 1 migration to schema "PUBLIC" (execution time 00:00.012s).
=== 迁移后状态 ===
当前版本: 2
已执行迁移数量: 2
数据库迁移完成!
再次执行后的输出:
makefile
=== 数据库当前状态 ===
当前版本: 2
已执行迁移数量: 2
=== 开始执行迁移 ===
INFO: Current version of schema "PUBLIC": 2
=== 迁移后状态 ===
当前版本: 2
已执行迁移数量: 2
数据库迁移完成!
这正是 Flyway 的增量迁移机制发挥作用的地方------它精确地知道哪些迁移已经执行过,只处理待处理的新迁移。
九、Flyway API 进阶用法
掌握了基本流程后,让我们深入了解 Flyway API 的其他常用方法。
基线操作(Baseline)
如果你的数据库早于 Flyway 管理,存在一些历史遗留的 schema 对象,可以使用 baseline() 方法创建基线版本。
java
// 为现有数据库创建基线
flyway.baseline();
// 或者指定基线版本和描述
flyway.baseline()
.setBaselineVersion("0")
.setBaselineDescription("Initial schema before Flyway");
清理数据库(Clean)
clean() 方法会删除数据库中的所有对象。【警告】生产环境中绝对不要使用这个方法,它会导致数据永久丢失。
java
// 清理数据库(仅限开发测试环境!)
flyway.clean();
flyway.migrate(); // 清理后需要重新迁移
验证与修复(Validate / Repair)
当你手动修改了数据库或迁移脚本后,可以使用 validate() 检查是否存在不一致。如果发现问题,repair() 可以尝试修复元数据表中的错误记录。
java
// 验证迁移状态是否一致
flyway.validate();
// 修复元数据表
flyway.repair();
获取迁移信息(Info)
info() 方法返回当前数据库的完整迁移状态详情:
java
import org.flywaydb.core.api.output.InfoResult;
import org.flywaydb.core.api.info.MigrationInfo;
// 获取迁移详细信息
InfoResult info = flyway.info();
// 遍历所有迁移
for (MigrationInfo migration : info.all) {
System.out.printf("%s: %s [%s]%n",
migration.version, // 版本号
migration.description, // 描述
migration.state.name()); // 状态
}
连接其他数据库
Flyway 的设计使得切换数据库类型非常简单------只需要更换 JDBC 驱动和连接字符串:
java
// MySQL 配置示例
Flyway flyway = Flyway.configure()
.dataSource("jdbc:mysql://localhost:3306/mydb", "root", "password")
.load();
// PostgreSQL 配置示例
Flyway flyway = Flyway.configure()
.dataSource("jdbc:postgresql://localhost:5432/mydb", "postgres", "password")
.load();
十、与 Spring Boot 集成
如果你的项目使用 Spring Boot,Flyway 的集成会更加简单。Spring Boot 提供了自动配置支持,只需要添加依赖并配置数据源即可。
添加依赖
xml
<!-- Spring Boot Flyway 自动配置 -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<!-- 如果使用 MySQL -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-mysql</artifactId>
</dependency>
配置 application.yml
yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: password
flyway:
# 迁移脚本目录(相对于 classpath)
locations: classpath:db/migration
# 启用 Flyway
enabled: true
# 基线版本(数据库已有数据时设置)
baseline-on-migrate: true
配置完成后,Spring Boot 会在应用启动时自动执行 Flyway 迁移。
十一、完整项目结构
本文示例的完整项目结构如下:
css
flyway-demo/
├── pom.xml
└── src/
└── main/
├── java/
│ └── com/
│ └── example/
│ └── flyway/
│ └── App.java
└── resources/
└── db/
└── migration/
├── V1__Create_person_table.sql
└── V2__Add_people.sql
十二、总结与延伸
通过本文的学习,你应该已经掌握了以下核心知识点:
1. Flyway 的本质:通过版本化的迁移脚本和元数据表追踪,实现数据库变更的可重复、可追踪管理。
2. 增量迁移机制:Flyway 总是只执行尚未应用的迁移,支持多次部署而不丢失数据或重复执行。
3. 元数据表 :flyway_schema_history 是 Flyway 的「记忆」,它记录了每个迁移的执行历史。
实践建议
- 团队制定统一的迁移脚本命名规范和目录组织方式
- 将迁移脚本与代码一起纳入版本控制
- 在 CI/CD 流程中集成 Flyway 迁移以实现自动化部署
- 建立测试环境用于验证迁移脚本的正确性
延伸学习资源
附录:常用配置选项参考
java
Flyway flyway = Flyway.configure()
// 数据源配置(必需)
.dataSource("jdbc:h2:file:./target/flyway-demo", "sa", null)
// 迁移脚本位置
.locations("filesystem:src/main/resources/db/migration")
// 编码设置
.encoding("UTF-8")
// 基线配置
.baselineOnMigrate(true)
.baselineVersion("1")
.baselineDescription("Init")
// 验证配置
.validateOnMigrate(true)
.cleanDisabled(false)
// 目标版本
.target("3")
// 表名配置
.table("flyway_schema_history")
.load();