《Spring Boot Flyway 实战手册》第2招 Migration 版本管理——用 V1、V2、V3 管住数据库演进
## 01、痛点 ``` 手动执行 V1__init.sql │ ▼ 手动执行 V2__add_user_email.sql │ ▼ 有人改了 V2,忘了执行 V3 │ ▼ 测试库、生产库字段不一致 ``` 我是老李,一个跟数据库结构打了十多年交道的工程师。最近团队在发版时又出了一次让所有人后背发凉的事故:开发环境跑得好好的,测试环境却报“users 表不存在 email 列”。查了半天,有人为了方便,直接在测试库手工补了列,但没同步到脚本;还有人把已经执行过的 V2 脚本当草稿改了,导致下一次发布时,生产库和新环境之间出现了谁也说不清的结构漂移。 类似的问题不是个例。数据库脚本缺少版本管理时,发布过程中的变更就像散落在聊天记录里的补丁:谁在什么时候对哪个库做了什么,通常只能靠记忆。上线前大家彼此问一句“脚本都跑了吗”,上线后祈祷环境不会说谎。现在是时候把数据库变更从“手工执行”升级为“版本演进”了。原因很简单:数据库结构已经是业务连续性的底座,它的每一步变化都必须可追溯、可校验、可重放。 ## 02、是什么 Flyway 是一个面向数据库的结构演进工具,核心思路是把数据库变更脚本当作代码一样管理。脚本文件放进固定目录,按文件名约定版本和描述,由工具自动按顺序执行,并在数据库里维护一张历史表记录每次变更。 先看最常见的文件名: ``` V2__add_user_email.sql │ │ │ │ │ └─ 描述:新增用户邮箱列 │ └─ 双下划线分隔符 └─ 版本号:2 ``` 其中各个部分的意义如下:V 前缀表示 Versioned Migration,也就是版本化迁移,一旦执行过,内容不允许再修改;2 是版本号,可以用整数或点分数字,例如 1.2.3;双下划线是固定分隔符;后面的 add_user_email 是描述,只影响阅读和排序,不影响执行逻辑;.sql 是脚本后缀,表示这是一段 SQL 迁移。 除了版本化迁移,Flyway 还支持另一种类型:R__xxx.sql,也就是 Repeatable Migration。它没有版本号,例如 R__refresh_view.sql。每次文件内容变化后,Flyway 会重新执行它;如果内容没变,就跳过。因此它适合管理视图、函数、存储过程这类需要保持最新定义的对象。 Flyway 的迁移来源主要分三类。SQL Migration 是最常见的形式,写在 .sql 文件里,直接交给工具解析执行。Java Migration 是写代码实现的迁移,适合需要跨数据库适配或包含复杂程序逻辑的场景。Baseline Migration 用于已经存在的数据库,给现有结构打一个基线,让 Flyway 可以只从某个版本之后开始接管变更。 ## 03、为什么 为什么我们需要把数据库变更纳入版本化?因为数据库不像应用代码,它的变更直接作用在数据上,错误影响更持久;而应用代码可以回滚部署,但数据库结构一旦改坏,恢复往往意味着停机、导数据、甚至追责。 手工执行脚本的典型问题至少有三个:一是不可追溯,你无法回答“生产库上有没有跑过这个脚本”;二是不可重复,同一个脚本在不同时间不同环境执行,结果可能不同;三是不可校验,脚本文件被人改过,执行过的库却浑然不觉。Flyway 用元数据表记录版本、描述、校验和、执行时间、是否成功,相当于给每次数据库变更都开了收据。 版本化迁移还有一个更根本的价值:它把数据库结构演进变成了线性历史。所有环境从同一个起点开始,依次执行 V1、V2、V3,最终到达一致的结构。新增变更就新增 V4,老环境补跑即可。这样的演化方式简单、稳定,也容易在团队中形成“只追加、不重写”的纪律。 ## 04、架构 从内部看,Flyway 的架构并不复杂。它主要做四件事:扫描、排序、校验、执行。围绕这四件事,有几个核心模块和一个关键的元数据表。 ``` ┌─────────────────────────┐ db/migration 目录 │ flyway_schema_history │ ┌──────────────────────┐ │ │ │ V1__init.sql │ │ installed_rank │ │ V2__add_user_email.sql│ │ version │ │ V3__create_order.sql │ │ description │ │ R__refresh_view.sql │ │ type │ └──────────────────────┘ │ script │ │ │ checksum │ ▼ │ installed_by │ 扫描 / 排序 / 校验 │ execution_time │ │ │ success │ ▼ └─────────────────────────┘ 执行 SQL / Java 迁移 │ ▼ 更新元数据表 ``` 元数据表 flyway_schema_history 是整个版本管理的基础。每执行一个迁移,Flyway 会插入一条记录,包括版本号、描述、迁移类型、脚本名、校验和、执行人、执行耗时和成功标记。之后每次运行 migrate 或 validate,都会读取这张表并与本地脚本比对。 工作流程大致如下:扫描迁移目录,识别所有 V 版本迁移和 R 可重复迁移;排除已经执行过且校验和未变化的脚本;对新增的版本迁移按版本号升序排列;对已执行过的脚本做校验,发现 checksum 不一致就中止;然后依次执行新增迁移,执行成功后写入历史表;最后处理可重复迁移,只有内容变化时才重新执行。 版本比较规则需要特别留意。Flyway 把版本号当作点分的数字段逐段比较,而不是直接做字符串比较。所以 1.10 大于 1.9,1.2.3 大于 1.2。这个规则避免了字典序带来的“1.10 排在 1.9 前”的错误直觉。 ## 05、实战 下面用一个最小场景把 V1 到 V3 的自动升级完整走一遍。假设已经存在一个 V1__init.sql,内容是建立 users 表。现在要新增一个用户邮箱列,同时创建订单表,并维护一个活跃用户视图。 先创建 V2__add_user_email.sql: ```sql ALTER TABLE users ADD COLUMN email VARCHAR(255); ``` 再创建 V3__create_order.sql: ```sql CREATE TABLE orders ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); ``` 执行迁移命令: ```bash flyway -url=jdbc:h2:mem:test -user=sa migrate ``` 观察命令行输出,Flyway 会依次报出 Migrating to version 2 - add user email、Migrating to version 3 - create order,然后提示成功。此时 V1 已经执行过,不会重复执行;V2、V3 是新增,按版本号自动升级。整个过程不需要人工判断先后顺序。 接下来创建 R__refresh_view.sql: ```sql CREATE OR REPLACE VIEW active_users AS SELECT id, username, email FROM users WHERE status = 1; ``` 再次执行 migrate,会发现可重复迁移被识别并执行。然后修改 R__refresh_view.sql,例如把 status = 1 改成 status = 0,再执行 migrate,Flyway 会检测到校验和变化并重新执行这个可重复迁移,因为它维护的是“最新定义”,而不是“一次性历史”。 执行 info 可以查看当前所有迁移的状态: ```text +---------+----------+----------------+------+---------------------+---------+ | Version | Category | Description | Type | Installed On | State | +---------+----------+----------------+------+---------------------+---------+ | 1 | version | init schema | SQL | 2025-01-01 10:00:01 | Success | | 2 | version | add user email | SQL | 2025-01-01 10:00:02 | Success | | 3 | version | create order | SQL | 2025-01-01 10:00:03 | Success | | | repeat | refresh view | SQL | 2025-01-01 10:00:04 | Success | +---------+----------+----------------+------+---------------------+---------+ ``` 执行 validate,它会比较本地脚本与历史表记录的校验和。正常情况下输出成功信息。现在做一个有意的破坏:打开已经执行过的 V2__add_user_email.sql,把内容改成 `ALTER TABLE users ADD COLUMN email VARCHAR(320);`。再次执行 validate 或 migrate,会看到类似下面的报错: ```text Migration checksum mismatch for migration version 2 -> Applied to database : 1735689600 -> Resolved locally : 1735699999 ``` 这表示数据库里记录的是旧内容校验和,本地文件已经变成了新内容。Flyway 阻止继续执行,避免本地修改把环境推入不一致状态。恢复脚本后再次 validate,校验通过。 从这个过程可以看出 checksum 的价值:它不关注文件名变没变,只关注内容是否和当初执行时完全一致。只要内容有一字节变化,就会触发校验失败。生产环境中,程序员不可以改历史;如果确实需要调整,应当新增 V4__adjust_email_length.sql 来完成 `ALTER TABLE users ALTER COLUMN email VARCHAR(320);`,这样所有环境都能照着同一个版本序列升级。 此外,baseline、clean、repair 也是日常会用到的操作。baseline 用于给一个已有但没有历史表的库打基线,比如执行 `flyway baseline -baselineVersion=2` 后,Flyway 会认为 V1、V2 已经是既定事实,从 V3 开始接管。clean 会清空目标模式并删除历史表,只适合本地开发。repair 可以删除失败记录、修复历史表中的 checksum,但生产环境使用必须极其谨慎,尤其不能用它来合理化对历史脚本的篡改。 ## 06、洞见 checksum 机制本质上是给“历史执行”做了不可抵赖记录。Flyway 在迁移脚本执行前计算内容校验和,执行成功后写入元数据表。每次后续的 validate 或 migrate 都会重新计算当前文件的校验和,并与历史记录比较。任何对已执行版本迁移的改动都会被立即发现。 这个设计带来一个关键纪律:版本化迁移一旦发布到生产环境,就只能新增,不能修改。原因至少有四点。第一,修改已执行脚本等于重写历史,从老环境升级上来的库和从零搭建的库会走向不同结构,环境一致性被破坏。第二,校验和失配会让自动化发布中断,阻断本来可以顺利完成的版本升级。第三,审计和备份依赖“版本线性演进”这个假设,如果某个版本内容漂移,就无法知道当前库到底执行过什么。第四,团队协作中,公共脚本就是契约,随意改动等于单方面撕毁契约。 可重复迁移则是另一套规则。它没有版本号,也不记录“一次性历史”,而是始终表示当前应当存在的定义。因此 R 脚本修改后,Flyway 会重新执行,而不是报错。这适合视图、函数、触发器等需要持续保持最新结构的对象。但可重复迁移不适合做数据修补或结构变更,因为只要文件内容变化,它会在每个环境中反复执行,风险反而更高。 还有版本比较和执行顺序的细节。版本迁移按版本号升序先执行一遍;可重复迁移在版本迁移之后按描述排序执行。所有新增的版本迁移都在已有历史之后追加。若出现失败,Flyway 会标记失败记录,后续修好脚本需要用 repair 或重新执行前排查元数据表。repair 的语义不是“把本地乱改变成合法”,而是修正元数据表里的校验和、失败标记,适合在确认本地脚本正确、历史记录因环境残留不一致时使用。生产环境使用 repair 应当极其谨慎。 ## 07、行动 要把这套机制落到日常,可以从七个动作开始。 第一,统一目录和命名。新建迁移放入固定目录,版本号用清晰数字,描述写清楚做了什么。命名范例:V4__alter_orders_add_index.sql、R__active_users_view.sql。 第二,代码评审时把迁移脚本纳入审查范围。检查点包括:是否有修改已合并的历史脚本;是否有版本号冲突;是否把一次性数据变更写进了可重复迁移。 第三,发布流水线里加一行 validate。每次构建或发布前执行 `flyway validate`,校验本地脚本与目标库记录是否一致;失败就阻断发布,并通知提交人。 第四,生产发布只允许新增。发布包里的迁移脚本只包含新版本,不包含对旧版本的修改。如果线上结构需要调整,新写一个版本迁移,而不是重写旧文件。 第五,应急变更也要补脚本。如果生产因紧急情况手工改过表结构,第二天必须补一个版本化迁移,并在测试库上重新验证完整升级链路。否则下一次发版一定会被 checksum 或结构差异教训。 第六,定期查看元数据表。`SELECT version, description, type, checksum, success FROM flyway_schema_history ORDER BY installed_rank;` 能快速发现重复执行、失败记录和异常描述。 第七,把 clean 和 repair 关进开发的笼子。clean 会清空 schema 和历史表,只适合本地开发;repair 应只在确认本地脚本正确的场景下使用,生产禁止随意 repair,尤其不要用 repair 掩盖对历史脚本的非法修改。 用一张检查清单收束: ``` [ ] 只新增版本迁移,不改历史脚本 [ ] R 脚本只用于视图/函数/存储过程 [ ] 发布前执行 migrate 预览或 validate [ ] 所有环境都从同一迁移目录升级 [ ] 失败后先查 flyway_schema_history 再重试 ``` ## 08、小结 数据库脚本不是草稿,是历史。历史只能追加,不能重写。版本化迁移让每一次结构变更都有版本、有描述、有校验,让数据库发布从“凭记忆执行”变成“可复现工程”。从 V1 到 V3,再到 R 可重复迁移,Flyway 给数据库变更立下了简单的规矩:先新增,再校验,最后执行。记住这一条,就能避开大半发布事故。 ## 09、链接与概念说明 先做一个概念澄清。SQL Migration 是用 .sql 文件写的迁移,适合大多数结构变更和简单数据操作。Java Migration 由 Java 类实现,类名按版本约束,适合复杂计算、跨库转换或需要读取外部数据的场景。Baseline Migration 用于已有库,设置一个基线版本,让 Flyway 忽略基线以下的历史;比如老库已经有 V1、V2,但从未用 Flyway 管理,可以 baseline 到 2,再从 V3 开始接管。Repeatable Migration 没有版本,内容变化才重跑,适合保持最新定义的数据库对象。 关于校验和,Flyway 默认使用 CRC32 对文件内容计算,记录在 flyway_schema_history 表的 checksum 字段。不同文件换行符、不同编码都可能导致校验和变化,所以团队应统一文件编码和行尾风格。若有特殊需要,可以通过配置调整校验算法。 延伸阅读建议优先看 Flyway 官方文档中关于 migrations、callbacks、schema history 的章节,官方文档最权威。如果想理解版本化数据库设计的思想,可以读 Martin Fowler 等作者关于 Evolutionary Database Design 的文章。如果团队正处于从手工脚本迁移到工具管理的阶段,可以先从已有库 baseline 开始,再逐步把新变更纳入版本迁移目录。