3. 数据库迁移基础:数据库Schema演进的历史、传统迁移工具原理与脚本特性

数据库 Schema 的演进是软件系统持续交付中不可回避的挑战。本章从历史演进出发,剖析传统迁移工具(Flyway / Liquibase)的核心原理,并深入探讨迁移脚本的原子性与幂等性——这是构建可靠数据库变更管线的基石。

3.1 数据库 Schema 演进的历史

数据库 Schema 的变更管理经历了从“手动脚本 + 口头沟通”到“版本化、自动化、可审计”的四个阶段:

阶段 时期 典型做法 核心痛点
1. 手动脚本时代 2000 年前 DBA 编写 DDL 脚本,通过 SQL 客户端手动执行 无版本记录、易遗漏、环境间不一致
2. 文件命名约定时代 2000–2010 按日期/序号命名脚本(如 V1__create_user.sql),手动排序执行 依赖人工排序、回滚困难、无状态追踪
3. 工具驱动时代 2010–2018 Flyway / Liquibase 等工具接管版本追踪与执行顺序 工具绑定、脚本幂等性依赖开发者自觉
4. 领域驱动 + 契约化时代 2018 至今 迁移脚本与领域模型对齐,接口契约驱动变更生成 领域与基础设施的语义鸿沟
关键洞察: 前三个阶段的核心矛盾是“如何让数据库变更像应用代码一样被版本管理”;第四阶段则进一步追问“变更的语义是否与业务领域一致”。

3.2 传统迁移工具原理:Flyway 与 Liquibase

Flyway 和 Liquibase 是目前最广泛使用的两种数据库迁移工具。它们虽然实现细节不同,但都遵循“版本化迁移 + 状态追踪”的基本模式。

3.2.1 Flyway 原理

Flyway 采用“约定优于配置”策略,通过扫描指定路径下的 SQL 脚本,按版本号顺序执行,并将执行记录写入元数据表(默认 flyway_schema_history)。

  • 脚本命名规范: V<版本号>__<描述>.sql(如 V1__create_user_table.sql)
  • 执行流程: 校验 → 计算待执行迁移 → 加锁 → 执行 → 写入记录 → 释放锁
  • 回滚策略: 原生不支持自动回滚,需手动编写撤销脚本(U<版本号>__<描述>.sql)

3.2.2 Liquibase 原理

Liquibase 采用“声明式 + 变更集(ChangeSet)”模型,通过 XML / YAML / JSON 或 SQL 描述变更,利用 DATABASECHANGELOG 表追踪执行状态。

  • 变更集标识: 由 id + author + filePath 三元组唯一确定
  • 执行流程: 解析 changelog → 计算未执行变更集 → 按顺序执行 → 写入 DATABASECHANGELOG → 记录 MD5 校验和
  • 回滚策略: 原生支持回滚标签(rollback),可生成回滚 SQL 或自动执行
选型建议: 团队偏好 SQL 原生控制、追求轻量级 → 选 Flyway;需要复杂变更编排、多环境差异管理、声明式定义 → 选 Liquibase。
传统迁移工具执行流程对比 Flyway 流程 扫描脚本目录 校验版本 & 校验和 执行待迁移脚本 写入 flyway_schema_history Liquibase 流程 解析 changelog 文件 计算未执行变更集 按顺序执行变更集 写入 DATABASECHANGELOG 核心差异 Flyway:SQL 优先,版本号驱动,轻量级 Liquibase:声明式优先,变更集驱动,支持多格式

3.3 迁移脚本的原子性与幂等性

无论使用哪种工具,迁移脚本的质量直接决定数据库变更的可靠性。两个核心属性——原子性与幂等性——是衡量迁移脚本成熟度的关键指标。

3.3.1 原子性(Atomicity)

原子性要求一个迁移脚本要么全部执行成功,要么完全不执行,不允许出现“部分执行”的中间状态。

  • 单语句原子性: 数据库原生保证单条 DDL/DML 的原子性
  • 多语句原子性: 需要显式使用事务包裹(BEGIN TRANSACTION / COMMIT)
  • DDL 事务支持差异: PostgreSQL、SQL Server 支持 DDL 事务;MySQL、MariaDB 在执行 DDL 时会隐式提交,需额外注意
常见陷阱: 在 MySQL 中,ALTER TABLE 会触发隐式提交,导致事务内的前序 DML 被提前持久化。建议将 DDL 与 DML 分离到不同迁移脚本中。

3.3.2 幂等性(Idempotency)

幂等性要求同一个迁移脚本在相同状态下多次执行,产生的结果与执行一次完全相同。这是应对“重试执行”、“回滚后重放”、“多环境重复部署”等场景的关键保障。

幂等类型 说明 示例
CREATE IF NOT EXISTS 创建对象前检查是否存在 CREATE TABLE IF NOT EXISTS users (...)
条件 DDL 通过查询元数据决定是否执行 DDL IF NOT EXISTS (SELECT * FROM sys.columns ...) ALTER TABLE ...
幂等 DML 使用 MERGE / UPSERT 替代 INSERT INSERT ... ON CONFLICT DO NOTHING
全量替换 先删除再创建(适用于视图、函数) DROP VIEW IF EXISTS ...; CREATE VIEW ...

3.3.3 原子性与幂等性的协同

原子性保证“要么全做,要么全不做”;幂等性保证“做多次等于做一次”。两者结合,才能构建出可安全重试、可回滚、可重复部署的迁移管线。

-- 符合原子性与幂等性的迁移脚本示例(PostgreSQL)
BEGIN;

-- 幂等创建表
CREATE TABLE IF NOT EXISTS order_events (
    id          BIGSERIAL PRIMARY KEY,
    order_id    BIGINT NOT NULL,
    event_type  VARCHAR(50) NOT NULL,
    payload     JSONB,
    created_at  TIMESTAMPTZ DEFAULT NOW()
);

-- 幂等添加索引(通过元数据检查)
DO $$
BEGIN
    IF NOT EXISTS (
        SELECT 1 FROM pg_indexes
        WHERE tablename = 'order_events'
          AND indexname = 'idx_order_events_order_id'
    ) THEN
        CREATE INDEX idx_order_events_order_id ON order_events(order_id);
    END IF;
END $$;

COMMIT;
最佳实践: 每个迁移脚本都应设计为“可安全重试”。在 CI/CD 管线中,若迁移执行超时或网络中断,重试机制应能自动恢复,而不产生重复数据或结构冲突。

3.4 传统工具的局限性

尽管 Flyway 和 Liquibase 解决了“版本化执行”的基本问题,但在领域驱动设计的语境下,它们存在以下不足:

  • 语义缺失: 迁移脚本仅描述“如何改表结构”,不表达“为什么改”以及“对应哪个领域概念”
  • 契约断裂: 数据库 Schema 变更后,API 接口、领域事件、应用服务之间的契约无法自动联动
  • 回滚脆弱: 回滚脚本需要手动维护,且容易与正向迁移产生语义偏差
  • 领域知识散落: 字段含义、约束规则、关联关系等业务语义仅存在于 DBA 或开发者的头脑中

这些局限性驱动了“领域建模驱动接口契约与版本控制”这一新范式的诞生——从第 4 章开始,我们将逐步构建这一体系。