一篇被 Cursor / Claude / Copilot 反复教育之后,我才搞明白的 AI Coding 时代数据库版本管理生存指南。

写在前面:AI 写 SQL,是真的爽,也是真的危险
去年这个时候,我还在手写每一个 Flyway 脚本------V1.0.7__add_user_status_column.sql,列名敲一遍、注释敲一遍、回滚预案再敲一遍。
今年我已经习惯了对着 AI 喊一句:
「帮我加个用户状态字段,0 启用 1 停用,写个 Flyway 脚本」
三秒钟,脚本就出来了。语法正确、命名规范、连 COMMENT 都贴心带上。爽吗?爽得不行。
直到某天上午 10 点,我盯着 IDEA 启动日志,看见那行红色的:
makefile
FlywayValidateException: Migration checksum mismatch for migration version 1.0.32
------AI 把已经在生产库执行过的脚本,又给我改了一遍内容。
那一刻我才意识到:Flyway 这套设计了十几年的「不可变迁移」哲学,正在被 AI Coding 的"自动化贴心服务"系统性地破坏。
这篇文章不教 Flyway 基础用法,而是把我和团队过去半年踩过的 5 个真实坑摊开来讲,每一个坑配上 AI 行为模式分析、根因、和我们最终沉淀下来的工程规范。
读完你会知道:在 AI 写 SQL 已经是日常的今天,Flyway 这套老工具应该怎么用,才能不被 AI 反向坑死。
案例一:AI 修改了已上线的 V1.0.32,应用集体起不来
现场
我让 AI 帮我「优化一下昨天那个建表脚本,字段顺序不太合理」。AI 很听话,一顿调整、commit、push。
第二天同事拉代码启动应用:
rust
Migration checksum mismatch for migration version 1.0.32
-> Applied to database : 1234567890
-> Resolved locally : 9876543210
应用直接起不来,团队 8 个人的本地环境集体瘫痪。
AI 为什么会这么干
因为你让它「优化脚本」,它根本不知道这个脚本已经在数据库里跑过了。
Flyway 把每个版本脚本视作不可变的历史事件 ------脚本一旦写入 flyway_schema_history,对应的 checksum 就被钉死。本地脚本内容只要变一个字符,下次启动校验就直接挂掉。
但 AI 不知道这件事。在它眼里,V1.0.32__xxx.sql 就是一个普通 .sql 文件,跟 UserService.java 没区别------你让它改,它就改。
正确姿势
铁律 1:进过任何数据库(哪怕只是同事本地库)的 Flyway 脚本,禁止任何形式的修改。
需要修正?新增下一个版本号:
bash
db/migration/
├── V1.0.32__create_ai_crud_config.sql # 已落库,禁止改
└── V1.0.33__fix_ai_crud_config_status_type.sql # 修正用,新增
给 AI 的护栏 :在你的 AGENTS.md / .cursorrules / CLAUDE.md 里明确写:
sql
## Flyway 脚本规范
- db/migration/V*.sql 为已执行历史,禁止修改任何已存在的脚本内容。
- 修正历史变更必须新增更高版本号,例如 V1.0.33__fix_xxx.sql。
- 同一版本号只能有一个脚本,禁止复用。
- 脚本必须可重复执行:建表用 CREATE TABLE IF NOT EXISTS,
插入用 INSERT ... SELECT ... WHERE NOT EXISTS。
亲测:把这段规则贴进 AGENTS.md,Claude 和 Cursor 主动改老脚本的概率从 8 成降到几乎为 0,AI 会自动建议新增版本。
案例二:版本号撞车,AI 默认从 V1.0.1 开始数
现场
两个同事并行开发:A 在 feature 分支让 AI 生成了 V1.0.40__add_message_template.sql;B 在另一个 feature 分支同样让 AI 生成了 V1.0.40__add_workflow_listener.sql。
合并到 main 那一刻,谁先合谁开心。后合并的同学:
sql
Found more than one migration with version 1.0.40
AI 为什么会这么干
AI 不知道你团队当前最大的 Flyway 版本号是多少。它会根据 prompt 上下文「猜」一个看起来合理的版本,最常见的猜法是:
- 看 prompt 里你提了什么版本,就 +1
- 没提的话,从
V1.0.1起步 - 偶尔从
V2025_01_15__xxx.sql这种时间戳格式起步(如果它见过 Liquibase 风格的项目)
更狠的是分支并行场景------AI 完全没有跨分支视野,两个分支各自看到的"最新版本"可能完全一样。
正确姿势
铁律 2:让 AI 写脚本前,先告诉它当前版本号水位。
我团队的 Cursor / Claude Code 工作流是这样的:
bash
# 1. 让 AI 先看一眼当前最大版本号
ls db/migration | sort -V | tail -3
# 2. 在 prompt 里明确指定下一个版本
# "新增一个 Flyway 脚本,版本号 V1.0.45,
# 文件名 V1.0.45__add_audit_log_table.sql"
更进一步,在 AGENTS.md 里加一条:
bash
- 写新 Flyway 脚本前,必须先执行
ls db/migration | sort -V | tail -1 查看当前最大版本号。
- 新脚本版本号必须严格大于该值,命名格式
V<major>.<minor>.<patch>__<lower_snake_case>.sql。
进阶玩法 :对于多人并行项目,把版本号空间按团队/模块切分。例如系统模块用 V1.0.x、流程模块用 V1.1.x、AI 模块用 V1.2.x。撞车概率指数级下降。
案例三:AI 把 'ENABLED' 塞进了 char(1) 字段
现场
让 AI 给 ai_crud_config 表写一条种子数据:
sql
INSERT INTO ai_crud_config (config_key, name, status, ...)
VALUES ('crm_customer', 'CRM客户管理', 'ENABLED', ...);
启动报错:
sql
Data truncation: Data too long for column 'status' at row 1
AI 为什么会这么干
我项目里同时存在两种 status 设计:
sql
-- ai_lowcode_domain.status 是 varchar(16)
status varchar(16) NOT NULL DEFAULT 'ENABLED' COMMENT '状态'
-- ai_crud_config.status 是 char(1)
status char(1) NOT NULL DEFAULT '0' COMMENT '状态(0启用 1停用)'
AI 没区分。一个项目里两种 status 风格混用,AI 大概率挑那个语义更清晰的 'ENABLED' 给你写进去。
而 ai_crud_config.status 是 char(1),最多 1 个字符。'ENABLED' 7 个字符,直接截断报错。
正确姿势
铁律 3:AI 写 INSERT 之前,必须先读建表语句。
在 AGENTS.md 里加:
sql
## SQL INSERT 编写规则
- 写 INSERT 之前必须先 grep 表的 CREATE TABLE 语句确认字段类型。
- char(1) 状态字段统一用 '0'(启用)/ '1'(停用)。
- varchar(N) 枚举状态字段统一用 'ENABLED' / 'DISABLED' / 'ARCHIVED'。
- 禁止依赖字段顺序,INSERT 必须显式列出所有列名。
更狠一点:让 AI 在写脚本时主动列出依据:
「我已确认 ai_crud_config.status 字段类型为 char(1),因此使用 '0' 表示启用。」
强制 AI 自证清白,比你事后排查省力得多。
修复流程
如果 Flyway 已经因为这个失败了,光改脚本不够,得手动清掉失败记录:
sql
-- 1. 检查失败记录
SELECT version, description, success
FROM flyway_schema_history
WHERE success = 0;
-- 2. 删除失败记录
DELETE FROM flyway_schema_history WHERE version = '1.0.32';
-- 3. 修正脚本后重启应用,Flyway 会自动重跑
这个流程值得贴在团队 wiki,因为类似场景一定还会再发生。
案例四:AI 写的种子数据 tenant_id = 0,所有租户都看不到
现场
让 AI 给字典表加几条数据:
sql
INSERT INTO sys_dict_data (dict_type, dict_label, dict_value, tenant_id, ...)
VALUES ('user_status', '正常', '0', 0, ...);
tenant_id 配的是 0,看起来"中性、合理、像默认值"。结果上线之后所有租户都看不到这条字典。
AI 为什么会这么干
tenant_id = 0 在 AI 的训练语料里是非常常见的「系统默认值 / 全局共享」语义。它真的以为这就是给所有租户共享的数据。
但很多多租户框架(包括我们项目用的 MyBatis-Plus + TenantLineInnerInterceptor)并不是这么实现的:
- 拦截器在每条 SELECT 后自动追加
WHERE tenant_id = 当前用户的 tenant_id - 如果当前用户
tenant_id = 1,就只能看到tenant_id = 1的数据 tenant_id = 0既不属于任何租户,也不被任何租户看到------对所有人不可见
这是租户隔离机制的实现细节 ,不是通用语义。AI 永远会按 SaaS 通用直觉猜一个看起来合理但其实错误的值。
正确姿势
铁律 4:项目特定的业务规则必须沉淀到 AGENTS.md,禁止依赖 AI 的"通用直觉"。
我的项目 AGENTS.md 里这一段专门约束 AI:
diff
## 多租户数据写入规范
- 业务内置数据(字典、配置、菜单等)的 tenant_id 必须设为 1(默认租户)。
- 禁止 tenant_id = 0------会导致数据对所有租户不可见。
- 不受租户拦截的表(如 sys_resource 菜单权限表),tenant_id 仍设为 1。
这种项目特定的语义陷阱,AI 永远不可能凭借"常识"绕过去。规则必须写死在项目根目录的 AI 上下文文件里,让每次 AI 调用都读到。
案例五:AI 写的 seed 数据没防重,重启就报主键冲突
现场
某个新模块需要内置一批默认配置,AI 给我写了:
sql
INSERT INTO sys_config (config_key, config_value, tenant_id)
VALUES
('mail.smtp.host', 'smtp.example.com', 1),
('mail.smtp.port', '465', 1);
第一次启动正常。但只要稍微动一下这个 seed 脚本(比如修个值),或者在某些场景下 Flyway 触发重跑------主键冲突:
rust
SQLIntegrityConstraintViolationException:
Duplicate entry 'mail.smtp.host' for key 'sys_config.uk_config_key'
AI 为什么会这么干
AI 的训练数据里有海量"教程式" SQL,那些教程为了简洁省略了防重判断。AI 默认认为「INSERT 就是 INSERT,第一次能跑就行」。
但 Flyway 脚本在真实工程中可能因为以下原因被重跑:
- 修复 checksum mismatch 后用
flyway repair+ 重跑 - 测试库重置后重跑全量 migration
- 不同环境(dev / staging / prod)独立执行
- 多分支合并产生的不规范操作
任何 INSERT 都必须假设它可能被执行多次。
正确姿势
铁律 5:所有 seed 数据 INSERT 必须自带防重保护。
sql
-- ❌ AI 默认写法
INSERT INTO sys_config (config_key, config_value, tenant_id)
VALUES ('mail.smtp.host', 'smtp.example.com', 1);
-- ✅ 防重写法 1:WHERE NOT EXISTS(最通用)
INSERT INTO sys_config (config_key, config_value, tenant_id)
SELECT 'mail.smtp.host', 'smtp.example.com', 1
WHERE NOT EXISTS (
SELECT 1 FROM sys_config WHERE config_key = 'mail.smtp.host' AND tenant_id = 1
);
-- ✅ 防重写法 2:ON DUPLICATE KEY UPDATE(MySQL 简洁版)
INSERT INTO sys_config (config_key, config_value, tenant_id)
VALUES ('mail.smtp.host', 'smtp.example.com', 1)
ON DUPLICATE KEY UPDATE config_value = VALUES(config_value);
-- ✅ 防重写法 3:INSERT IGNORE(不更新已有数据)
INSERT IGNORE INTO sys_config (config_key, config_value, tenant_id)
VALUES ('mail.smtp.host', 'smtp.example.com', 1);
AGENTS.md 里的对应规则:
sql
## SQL 可重复执行规则
- 所有 INSERT 必须自带防重保护(WHERE NOT EXISTS / ON DUPLICATE KEY UPDATE / INSERT IGNORE)。
- 所有 ALTER TABLE ADD COLUMN 必须先查 information_schema.columns 防止重复添加。
- 所有 CREATE INDEX 必须先查 information_schema.statistics 或用 IF NOT EXISTS。
- 删除/修改语义的脚本(DELETE / UPDATE / DROP)必须有明确的 WHERE 条件和影响行数预估。
AI Coding 时代 Flyway 的 4 条工程铁律
把上面 5 个案例抽象一下,就是这 4 条:
| 铁律 | 一句话总结 |
|---|---|
| 不可变性优先 | 已落库的脚本是历史,不是源代码。AI 只能新增版本,不能改历史。 |
| 防御式 SQL | 任何 SQL 都要假设会被重跑。建表加 IF NOT EXISTS,插入加防重,列加 information_schema 检查。 |
| 业务规则前置 | 项目特定的业务约定(tenant_id、字段类型、状态枚举)必须写进 AGENTS.md,不要寄希望于 AI 的"直觉"。 |
| 强制自证清白 | 让 AI 在生成脚本时主动声明它依据的字段类型、版本号、租户规则。错的会暴露,对的留痕。 |
如果你要把这 4 条落地到团队,最低成本的做法就是:
- 在项目根创建
AGENTS.md(Cursor / Claude Code / GitHub Copilot 都会自动读) - 把上述铁律的具体规则贴进去
- 在
db/migration/README.md里再写一份给人看的版本
这套护栏起码帮我们团队近半年减少了 70% 以上的 Flyway 翻车事故。
一个把这套实践做到位的开源项目:ForgeAdmin
写到这里得安利一下我们团队正在做的开源项目 ForgeAdmin------一个企业级中后台管理框架,恰好是把上面这些 Flyway × AI Coding 实践全部内化到工程结构里的实战样本。
如果你正在搭一个新项目,或者团队里 AI 写 SQL 的乱象需要被治一治,ForgeAdmin 的几个工程约定值得直接抄走:
1. 数据库脚本目录约定
bash
db/
├── migration/ # Flyway 版本化迁移:表结构、索引、字段、系统资源
│ ├── V1.0.0__baseline.sql
│ ├── V1.0.1__add_dashboard_version.sql
│ └── V1.0.32__create_ai_crud_config.sql
├── seed/
│ ├── required/ # 系统运行必需的初始化数据
│ ├── demo/ # 演示数据,默认不导入
│ └── optional/ # 可选模块数据
└── README.md # 给人和 AI 同时看的规范
关键设计:把"必须执行"的迁移和"可选执行"的种子数据物理隔离,AI 在写脚本时不会把演示数据混进 migration。
2. AGENTS.md 中的 Flyway 强约束
ForgeAdmin 的 AGENTS.md 里有一整章「数据库脚本维护规范」,强制约束所有 AI 助手:
- 已落库脚本禁止修改,必须新增版本
- INSERT 必须列名显式,必须 NOT EXISTS 防重
- 业务内置数据
tenant_id必须为 1,禁止为 0 - 涉及资金/状态/权限的 SQL 必须在 Spec 中说明影响范围
这套规则 AI 看得懂、人看得懂,新人入职第一天打开就能照着写。
3. Spec 驱动开发流程:No Spec No Code
ForgeAdmin 内置了一套渐进式开发命令:
bash
/propose <需求> # 创建变更提案:spec.md + tasks.md
/apply <变更名> # 按 Spec 执行编码(含数据库脚本)
/review <变更名> # 两阶段审查:Spec 合规 + 代码质量
/test <变更名> # 自动化测试
/archive <变更名> # 归档并沉淀知识
每一次涉及数据库变更的需求,必须先有 Spec 才能动手写脚本。 AI 在 spec 阶段就被强制说清楚:要改哪个表、字段类型是什么、影响哪些租户、回滚方案是什么。等 AI 写到 SQL 阶段,已经没有空间瞎猜了。
4. 微内核 + 插件化架构
后端基于 Spring Boot 3 + MyBatis-Plus + Sa-Token + Flowable,模块化拆分到位:
forge-plugin-system系统管理(用户/角色/菜单/部门/租户/字典)forge-plugin-flowFlowable 流程引擎forge-plugin-aiAI 供应商管理 + 代码生成forge-starter-tenant多租户隔离forge-starter-ormORM + 动态数据源 + 分页
每个插件独立维护自己的 Flyway 脚本目录,互不干扰。AI 在这种结构下写脚本,作用域天然被约束在当前插件内。
5. 前端零代码 CRUD:AiCrudPage
配套的前端 AiCrudPage 组件支持配置式 CRUD:你只要定义好 api-config 和 schema,列表、详情、新增、编辑、删除全自动。AI 在这种约定下生成的页面代码,几乎不会跑偏。
复盘:AI 不是替你写代码,是替你执行你定义好的规则
回到开头那个 checksum mismatch 的清晨。
我后来花了一下午时间,把整个团队的 AGENTS.md 重写了一遍:Flyway 规范、SQL 防重规则、租户语义、字段类型约定,能写多细写多细。
之后的三个月,团队再没出过同类问题。不是 AI 变聪明了,是规则被前置了。
这是 AI Coding 时代最大的认知翻转:
- 过去:你写规范是为了让队友写代码不出错
- 现在:你写规范是为了让 AI 写代码不出错------而 AI 比任何新人都更需要明确的边界
Flyway 这种「不可变历史」属性的工具,恰好是这次认知翻转的最佳试金石。它把 AI 的"贴心修改"放大成 checksum mismatch 的醒目报错,让你不得不直面这件事。
写在最后
把这篇文章总结成一句话:
在 AI 写 SQL 已经常态化的今天,Flyway 不能再被当成一个简单的迁移工具,它是你团队工程约束最显眼的边界。守住它,等于守住了数据库版本管理的下限。
如果这篇文章对你有帮助,欢迎点赞、收藏、转发给那个还在被 AI 改老脚本的队友。
项目信息
- ForgeAdmin 开源中后台管理框架
- 技术栈:Spring Boot 3 + Vue 3 + MyBatis-Plus + Sa-Token + Flowable
- 核心特性:RBAC 权限、多租户、AI 代码生成、Flowable 工作流、消息中心、AI 数据大屏
如果你正在搭建一个长期演进的企业级中后台系统,欢迎来 Star ⭐:
- Gitee:gitee.com/ForgeLab/fo...
- GitHub:github.com/yaomindong1...
如果你正在做企业级中后台,欢迎来 GitHub 给颗 Star。也欢迎在评论区聊聊你被 AI 改 SQL 坑过的瞬间------评论区翻车合集,我们交换故事。