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 或自动执行
3.3 迁移脚本的原子性与幂等性
无论使用哪种工具,迁移脚本的质量直接决定数据库变更的可靠性。两个核心属性——原子性与幂等性——是衡量迁移脚本成熟度的关键指标。
3.3.1 原子性(Atomicity)
原子性要求一个迁移脚本要么全部执行成功,要么完全不执行,不允许出现“部分执行”的中间状态。
- 单语句原子性: 数据库原生保证单条 DDL/DML 的原子性
- 多语句原子性: 需要显式使用事务包裹(
BEGIN TRANSACTION/COMMIT) - DDL 事务支持差异: PostgreSQL、SQL Server 支持 DDL 事务;MySQL、MariaDB 在执行 DDL 时会隐式提交,需额外注意
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;
3.4 传统工具的局限性
尽管 Flyway 和 Liquibase 解决了“版本化执行”的基本问题,但在领域驱动设计的语境下,它们存在以下不足:
- 语义缺失: 迁移脚本仅描述“如何改表结构”,不表达“为什么改”以及“对应哪个领域概念”
- 契约断裂: 数据库 Schema 变更后,API 接口、领域事件、应用服务之间的契约无法自动联动
- 回滚脆弱: 回滚脚本需要手动维护,且容易与正向迁移产生语义偏差
- 领域知识散落: 字段含义、约束规则、关联关系等业务语义仅存在于 DBA 或开发者的头脑中
这些局限性驱动了“领域建模驱动接口契约与版本控制”这一新范式的诞生——从第 4 章开始,我们将逐步构建这一体系。