数据库迁移

数据库迁移工具将 Schema 变更(建表、加列、加索引)纳入版本控制,与代码一起发布,解决手动执行 SQL 脚本混乱、环境不一致的问题。

Flyway vs Liquibase

对比FlywayLiquibase
配置格式SQL 文件(主)/ JavaYAML / XML / JSON / SQL
学习成本低,约定优于配置中,需理解 changeset 概念
回滚支持付费版支持(开源版无)原生支持 rollback
版本管理顺序版本号(V1, V2…)changeSet id + author
适合场景简单、纯 SQL 迁移需要回滚、多格式、复杂变更

Flyway

引入依赖

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.flywaydb:flyway-core'
// MySQL 8+
implementation 'org.flywaydb:flyway-mysql'

配置

spring:
  flyway:
    enabled: true
    locations: classpath:db/migration     # 脚本目录
    baseline-on-migrate: true             # 对已有数据库首次使用时建基线
    baseline-version: 0                   # 基线版本号
    validate-on-migrate: true             # 迁移前校验已执行脚本的 checksum
    out-of-order: false                   # 禁止乱序执行(如 V3 已执行,不允许再跑 V2)
    table: flyway_schema_history          # 历史记录表名
    placeholders:                         # 脚本中的占位符替换
      schema: mydb
      env: prod

命名规则

db/migration/
├── V1__Create_users_table.sql       ← 版本迁移(不可修改)
├── V2__Add_email_to_users.sql
├── V3__Create_orders_table.sql
├── V3_1__Add_orders_index.sql       ← 子版本
├── R__Refresh_user_stats_view.sql   ← 可重复迁移(内容变化即重新执行)
└── U3__Undo_orders_table.sql        ← 撤销迁移(Flyway Teams)

命名格式:{前缀}{版本}__{描述}.sql

  • V:版本迁移(只执行一次,执行后不能修改)
  • R:可重复迁移(视图、存储过程,checksum 变化则重新执行)
  • 双下划线 __ 分隔版本号和描述,描述中空格用 _ 代替

SQL 脚本示例

-- V1__Create_users_table.sql
CREATE TABLE users (
    id         BIGINT       NOT NULL AUTO_INCREMENT,
    username   VARCHAR(64)  NOT NULL,
    email      VARCHAR(128) NOT NULL,
    password   VARCHAR(256) NOT NULL,
    status     TINYINT      NOT NULL DEFAULT 1,
    created_at DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    UNIQUE KEY uk_username (username),
    UNIQUE KEY uk_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- V2__Add_phone_to_users.sql
ALTER TABLE users
    ADD COLUMN phone VARCHAR(20) NULL AFTER email,
    ADD INDEX idx_phone (phone);
-- V3__Create_orders_table.sql
CREATE TABLE orders (
    id          BIGINT         NOT NULL AUTO_INCREMENT,
    order_no    VARCHAR(32)    NOT NULL,
    user_id     BIGINT         NOT NULL,
    amount      DECIMAL(12, 2) NOT NULL,
    status      VARCHAR(20)    NOT NULL DEFAULT 'PENDING',
    created_at  DATETIME       NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    UNIQUE KEY uk_order_no (order_no),
    INDEX idx_user_id (user_id),
    INDEX idx_status_created (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Java 迁移(复杂数据初始化)

// db/migration/V4__Migrate_user_data.java(需放在 db.migration 包下)
@Component
public class V4__Migrate_user_data implements JavaMigration {
 
    @Override
    public MigrationVersion getVersion() {
        return MigrationVersion.fromVersion("4");
    }
 
    @Override
    public String getDescription() { return "Migrate user data"; }
 
    @Override
    public Integer getChecksum() { return 42; }  // 手动指定,修改时记得更改
 
    @Override
    public boolean isUndo() { return false; }
 
    @Override
    public boolean canExecuteInTransaction() { return true; }
 
    @Override
    public void migrate(Context context) throws Exception {
        try (Statement stmt = context.getConnection().createStatement()) {
            // 复杂数据迁移逻辑
            stmt.execute(
                "UPDATE users SET display_name = username WHERE display_name IS NULL"
            );
        }
    }
}

常用 Flyway 命令

# Maven
mvn flyway:migrate    # 执行所有待执行的迁移
mvn flyway:validate   # 验证迁移脚本完整性(checksum 校验)
mvn flyway:info       # 显示所有迁移的状态
mvn flyway:repair     # 修复失败的迁移记录(failed 状态)
mvn flyway:baseline   # 对已有数据库建立基线(跳过历史变更)
mvn flyway:clean      # ⚠️ 危险:清空整个数据库(生产禁用)

Liquibase

引入依赖

implementation 'org.liquibase:liquibase-core'

配置

spring:
  liquibase:
    enabled: true
    change-log: classpath:db/changelog/db.changelog-master.yaml
    default-schema: mydb
    contexts: prod          # 只执行标记了 prod context 的 changeset

Changelog 结构

# db/changelog/db.changelog-master.yaml(主入口,引入各版本)
databaseChangeLog:
  - include:
      file: db/changelog/v1/create-tables.yaml
  - include:
      file: db/changelog/v2/add-columns.yaml
  - include:
      file: db/changelog/v3/seed-data.yaml
# db/changelog/v1/create-tables.yaml
databaseChangeLog:
  - changeSet:
      id: 1-create-users
      author: zhangsan
      context: prod, dev      # 在 prod 和 dev 环境执行
      changes:
        - createTable:
            tableName: users
            columns:
              - column:
                  name: id
                  type: BIGINT
                  autoIncrement: true
                  constraints:
                    primaryKey: true
              - column:
                  name: username
                  type: VARCHAR(64)
                  constraints:
                    nullable: false
                    unique: true
              - column:
                  name: email
                  type: VARCHAR(128)
                  constraints:
                    nullable: false
      rollback:
        - dropTable:
            tableName: users
 
  - changeSet:
      id: 2-add-phone
      author: lisi
      changes:
        - addColumn:
            tableName: users
            columns:
              - column:
                  name: phone
                  type: VARCHAR(20)
        - createIndex:
            tableName: users
            indexName: idx_phone
            columns:
              - column:
                  name: phone
      rollback:
        - dropIndex:
            tableName: users
            indexName: idx_phone
        - dropColumn:
            tableName: users
            columnName: phone

SQL 格式的 Changeset(复杂 SQL 直接写)

- changeSet:
    id: 5-complex-migration
    author: wangwu
    changes:
      - sql:
          sql: |
            UPDATE orders
            SET status = 'COMPLETED'
            WHERE status = 'PAID' AND updated_at < NOW() - INTERVAL 30 DAY;
    rollback:
      - sql:
          sql: |
            UPDATE orders
            SET status = 'PAID'
            WHERE status = 'COMPLETED' AND updated_at > NOW() - INTERVAL 30 DAY;

测试环境处理

# application-test.yml
spring:
  flyway:
    locations:
      - classpath:db/migration        # 正式迁移脚本
      - classpath:db/testdata         # 测试数据(仅测试环境)
    clean-on-validation-error: true   # 测试时可自动清理并重建(生产禁用)
// 测试中使用 H2 内存数据库 + Flyway
@SpringBootTest
@ActiveProfiles("test")
class OrderRepositoryTest {
    // Flyway 自动在测试启动时执行迁移
}

CI/CD 集成最佳实践

开发          → 编写迁移脚本 → 提交 Git
                                   │
CI 流水线  → 运行测试(H2 + Flyway)→ 通过
                                   │
CD 部署   → 应用启动前先执行 Flyway migrate
          → 迁移成功后启动应用
          → 迁移失败则部署中止(保护线上数据)

Spring Boot 默认在应用启动时自动执行 flyway:migrate,无需手动触发。

生产环境安全规则:

  • 禁用 flyway.clean-on-validation-error(默认已禁用)
  • 禁用 flyway.clean-disabled: false(默认已禁用)
  • 迁移脚本一旦提交执行,永远不要修改(会导致 checksum 校验失败)
  • 数据变更(UPDATE / DELETE)需在脚本中备份原始数据

相关链接

  • 数据访问 — JPA / MyBatis 与数据库 Schema 的关系
  • 事务管理 — 迁移脚本执行时的事务控制
  • 多数据源 — 多数据源场景下每个数据源独立配置 Flyway 实例
  • 环境与Profile — 测试环境与生产环境使用不同迁移脚本
  • Docker部署 — 容器启动时自动执行迁移的时序问题