AI 自动化编写 SQL 脚本,更要守住 Flyway 版本管理的防线

一篇被 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.statuschar(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 条落地到团队,最低成本的做法就是:

  1. 在项目根创建 AGENTS.md(Cursor / Claude Code / GitHub Copilot 都会自动读)
  2. 把上述铁律的具体规则贴进去
  3. 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-flow Flowable 流程引擎
  • forge-plugin-ai AI 供应商管理 + 代码生成
  • forge-starter-tenant 多租户隔离
  • forge-starter-orm ORM + 动态数据源 + 分页

每个插件独立维护自己的 Flyway 脚本目录,互不干扰。AI 在这种结构下写脚本,作用域天然被约束在当前插件内。

5. 前端零代码 CRUD:AiCrudPage

配套的前端 AiCrudPage 组件支持配置式 CRUD:你只要定义好 api-configschema,列表、详情、新增、编辑、删除全自动。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 ⭐:

如果你正在做企业级中后台,欢迎来 GitHub 给颗 Star。也欢迎在评论区聊聊你被 AI 改 SQL 坑过的瞬间------评论区翻车合集,我们交换故事。

相关推荐
卷无止境1 小时前
Python的ABC库探索:能不能在系统设计之初就定义好所有抽象类?
后端
卷无止境1 小时前
Python collections 库深度解析:那些被低估的数据结构利器
后端
老金带你玩AI1 小时前
GLM-5.2来了,Claude Code百万上下文怎么配?
人工智能
AI_yangxi1 小时前
短视频矩阵系统供应商
大数据·人工智能·矩阵
Asize1 小时前
Prompt 驱动 NLP:从 ES6 模块化到文本推理实战
javascript·人工智能·机器学习
harykali1 小时前
Datawhale Hello-ROCm学习:初探Gemma4 #AMDev #Datawhale
人工智能·llm
XovH1 小时前
Redis 从入门到精通:分布式锁 —— 从 SETNX 到 Redlock
后端
用户329901675051 小时前
用 Web Speech API 给 AI 回答加"朗读"功能,边读边高亮 🔊
后端