数据库迁移总出问题?学会这个工具,让团队协作效率翻倍!

背景

数据库是应用程序的核心,而数据库 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 追踪迁移历史的核心,它记录了每个迁移版本的信息,包括版本号、描述、执行时间、成功与否等关键数据。

flowchart TD A[空数据库] --> B{Flyway 检测到<br/>元数据表不存在} B --> C[创建 flyway_schema_history<br/>元数据表] C --> D[扫描迁移脚本目录] D --> E[按版本号执行迁移]

创建元数据表后,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 就是「待处理迁移」------它们存在于文件系统中,但尚未被应用到数据库。

flowchart LR subgraph 文件系统 F1[&#34;V1__Create_person.sql<br/>✓ 已执行&#34;] F2[&#34;V2__Add_people.sql<br/>✓ 已执行&#34;] F3[&#34;V3__Create_orders.sql<br/>⏳ 待处理&#34;] F4[&#34;V4__Add_indexes.sql<br/>⏳ 待处理&#34;] F5[&#34;V5__Seed_data.sql<br/>⏳ 待处理&#34;] end

这种增量迁移的机制带来了几个重要好处:

  1. 幂等性:支持多次、重复执行迁移而不会造成数据丢失或重复执行
  2. 并行开发:允许多个开发者并行工作,每个人的本地环境可能处于不同版本,但最终都能汇聚到一致的状态
  3. 可追溯 :每一次变更是哪一次部署引入的、对应哪个版本的代码,都能通过 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();
相关推荐
wespaul1 小时前
VeapAI 自然语言操作实战|第 04 篇:系统数据加工——把系统自身的元数据吃成知识
java·大数据·运维·数据库·人工智能
幼儿园蛋小黑1 小时前
SPI 子系统:从原理到实践
java·前端·数据库
安妮细水长流1 小时前
【无标题】
数据库·mongodb
KKKlucifer1 小时前
从规则到大模型:数据分类分级平台的能力分野与场景适配
大数据·数据库·分类
架构师伯子明2 小时前
QT+Opengl (1):渲染一个三角形
数据库
A-刘晨阳2 小时前
金仓时序数据库:以高效压缩、快速写入与时序优化,破解工业海量数据管理难题
数据库·时序数据库
2603_965148112 小时前
终极清单:构建自动化选品比价系统的所有组件与资源
大数据·运维·数据库·数据挖掘·自动化
打工仔折腾 AI2 小时前
SQL 编辑器不是高级记事本:KStudio 与 KSQL Developer 的建表与增删改查手法
数据库·人工智能·后端·python·sql·编辑器·ai agent 实战
CNSSIRD数据库2 小时前
中国省地县乡四级行政建制及区划代码-2025年版
大数据·数据库·数据分析·论文笔记