3. 迁移脚本编写规范:可重复迁移与不可重复迁移

数据库迁移脚本是 CI/CD 流水线的核心交付物。编写不规范会导致版本混乱、数据丢失或部署失败。本章围绕迁移脚本的两种类型、命名规则、原子性与幂等性要求展开,确保脚本在流水线中可安全、可重复执行。

3.1 可重复迁移与不可重复迁移

根据脚本执行次数与效果,迁移脚本分为两类:可重复迁移(Repeatable Migration) 和 不可重复迁移(Versioned Migration)。两者在流水线中的角色截然不同。

特性 不可重复迁移(Versioned) 可重复迁移(Repeatable)
执行次数 每个版本仅执行一次 每次检测到内容变化时执行
典型场景 DDL(CREATE TABLE、ALTER TABLE)、初始数据插入 视图、函数、存储过程、物化视图、权限脚本
版本标识 文件名包含版本号(如 V1.0.1__add_user.sql) 文件名包含描述,不包含版本号(如 R__user_view.sql)
幂等性要求 必须幂等(IF NOT EXISTS / 条件判断) 必须幂等(CREATE OR REPLACE / 先 DROP 再 CREATE)
回滚策略 通常提供回滚脚本(U 前缀) 不提供回滚,直接覆盖
流水线行为 按版本顺序执行,已执行的不再重复 每次部署时检查 checksum,变化则重新执行
实践建议: 将 DDL 和静态参考数据放入不可重复迁移;将视图、函数等逻辑对象放入可重复迁移。这样既能保证结构变更的版本可追溯,又能让逻辑更新随代码自动生效。

3.2 迁移脚本的命名规则

统一的命名规则是流水线自动排序、去重和追溯的基础。推荐采用 前缀 + 版本号 + 分隔符 + 描述 的格式。

3.2.1 不可重复迁移命名

V{版本号}__{描述}.sql
示例:
V1.0.0__create_user_table.sql
V1.0.1__add_email_column.sql
V1.1.0__insert_initial_roles.sql
U1.0.0__drop_user_table.sql   (回滚脚本,可选)
  • 前缀:V 表示版本迁移,U 表示回滚迁移。
  • 版本号:使用语义化版本(主版本.次版本.补丁),支持数字、点、下划线。
  • 分隔符:双下划线 __ 将版本号与描述清晰分离。
  • 描述:简短英文或拼音,用下划线代替空格,体现脚本意图。

3.2.2 可重复迁移命名

R__{描述}.sql
示例:
R__user_view.sql
R__order_summary_function.sql
R__grant_readonly_permissions.sql
  • 前缀:R 表示可重复迁移。
  • 无版本号:可重复迁移不参与版本排序,仅按描述字母顺序执行。
  • 描述:清晰表达对象类型和名称,便于流水线识别变更。
避坑提醒: 不要使用日期或时间戳作为版本号(如 20250401__xxx.sql),这会导致分支合并时版本冲突。始终使用语义化版本,并在 CI 流水线中自动递增。

3.3 原子性与幂等性要求

迁移脚本在流水线中必须满足原子性和幂等性,这是保障数据库状态一致性的底线。

3.3.1 原子性

每个迁移脚本要么全部执行成功,要么全部回滚,不允许部分执行。实现方式:

  • 事务包裹:DDL 和 DML 放在同一个事务中执行。
  • 单脚本单变更:一个脚本只做一件事(如只创建一个表、只添加一个字段)。
  • 避免混合 DDL 与 DML:某些数据库(如 MySQL)隐式提交 DDL,导致事务中断。
-- 正确示例:单脚本单变更,事务包裹
BEGIN;
CREATE TABLE IF NOT EXISTS user_role (
    user_id INT NOT NULL,
    role_id INT NOT NULL,
    PRIMARY KEY (user_id, role_id)
);
COMMIT;

-- 错误示例:混合 DDL 与 DML,事务被隐式提交
BEGIN;
ALTER TABLE user ADD COLUMN status VARCHAR(20);  -- 隐式提交
INSERT INTO user_role VALUES (1, 2);              -- 已不在事务中
COMMIT;  -- 仅提交 INSERT,ALTER 已提前提交

3.3.2 幂等性

同一脚本在相同状态下执行多次,结果与执行一次相同。实现方式:

操作类型 幂等实现方式 示例
CREATE TABLE IF NOT EXISTS CREATE TABLE IF NOT EXISTS ...
ALTER TABLE ADD COLUMN 先检查列是否存在 IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS ...)
INSERT 静态数据 使用 MERGE / ON CONFLICT / INSERT IGNORE INSERT INTO role (id, name) VALUES (1, 'admin') ON CONFLICT (id) DO NOTHING;
CREATE VIEW / FUNCTION CREATE OR REPLACE CREATE OR REPLACE VIEW user_view AS ...
GRANT 权限 先 REVOKE 再 GRANT,或使用 IF NOT EXISTS 语法 REVOKE ALL ON schema FROM user; GRANT SELECT ON ... TO user;
关键原则: 不可重复迁移的幂等性通过“条件判断”实现;可重复迁移的幂等性通过“覆盖式重建”实现。两者都必须在脚本编写阶段就内建,不能依赖流水线跳过。

3.4 迁移脚本分类与执行流程

下图展示了迁移脚本在 CI/CD 流水线中的分类与执行顺序:

迁移脚本分类与执行流程 不可重复迁移(Versioned) V1.0.0__create_user_table.sql V1.0.1__add_email_column.sql V1.1.0__insert_initial_roles.sql 按版本号升序执行,每个脚本仅执行一次 可重复迁移(Repeatable) R__user_view.sql R__order_summary_function.sql R__grant_readonly_permissions.sql 每次部署检测 checksum,变化则重新执行 先执行 数据库目标状态(幂等、原子、版本对齐)

3.5 原子性与幂等性检查清单

在代码审查和流水线门禁中,建议对每个迁移脚本进行以下检查:

  1. 是否包含事务包裹? 对于不可重复迁移,DDL 和 DML 是否在同一个 BEGIN/COMMIT 内?
  2. 是否处理了重复执行? CREATE 是否加了 IF NOT EXISTS?ALTER 是否先检查列/索引是否存在?
  3. 是否避免隐式提交? 是否在事务中混合了会导致隐式提交的 DDL?
  4. 是否单脚本单变更? 一个脚本是否只做一件事?
  5. 可重复迁移是否使用 CREATE OR REPLACE? 视图、函数、存储过程是否支持覆盖?
  6. 回滚脚本是否与正向脚本完全对称? 回滚后能否恢复到迁移前的状态?
流水线集成建议: 在 CI 阶段对迁移脚本进行静态检查(如使用 flyway-validate 或 liquibase diff),自动检测命名规范、事务完整性、幂等性缺陷。不符合规范的脚本直接阻断流水线,避免进入部署阶段。

3.6 常见反模式与修正

反模式 问题 修正方案
使用日期作为版本号 分支合并时版本冲突,无法排序 使用语义化版本,CI 自动递增
可重复迁移中包含 DDL DDL 无法使用 CREATE OR REPLACE,导致重复执行报错 DDL 放入不可重复迁移,可重复迁移只放逻辑对象
脚本中硬编码数据库名 迁移无法在不同环境复用 使用占位符或环境变量替换
一个脚本包含多个 ALTER TABLE 中间失败导致部分执行,难以回滚 拆分为多个独立脚本,每个脚本一个变更
INSERT 语句不带幂等处理 重复执行导致主键冲突或重复数据 使用 MERGE / ON CONFLICT / INSERT IGNORE

遵循上述规范,迁移脚本将成为 CI/CD 流水线中可靠、可追溯、可自动化的核心资产,为数据库版本对齐提供坚实基础。