背景
用 Cursor、Copilot 这类 AI 工具写代码时,有一个很实际的感受:AI 对业务上下文的理解,很大程度上取决于工作区里有什么。
代码写得再漂亮,如果 AI 不知道 orders 表和 order_items 是怎么关联的、字段含义是什么、索引约束有哪些,它生成的 SQL、Entity、Service 就容易「看起来对、跑起来错」。
这篇文章分享一条我在 AI 辅助开发中逐渐固化下来的实践:用 Migration 方案管理数据库结构的迭代,以 Java 生态为例,但思路适用于任何技术栈。
手动维护表结构的问题
很多团队的数据库演进路径是这样的:
- 开发者在 Navicat / DBeaver 里直接改表
- 改完本地能跑,口头或文档里记一笔
- 测试、生产环境靠 DBA 或同事「照着改一遍」
- 偶尔导出一份
schema.sql丢进文档目录,但很快又过期
这套流程在 AI 时代会放大几个问题:
- 文档与真实结构脱节:AI 读到的是过期的表结构说明,生成的代码自然对不上
- 环境不一致:本地有字段、测试没有,排查成本高
- 变更不可追溯:三个月前为什么加这个索引、谁改的,往往查不清
- 人工同步易出错:漏改一个字段、索引名写错、字符集不一致,都是常见事故
本质上,表结构也是代码的一部分,却长期被当作「数据库那边的事」单独维护,这和把业务逻辑写在 Word 里再手工抄进 IDE 没有区别。
Migration 方案是什么
Migration(数据库迁移)的核心思想很简单:
每一次表结构变更,都是一份可版本管理的、按顺序执行的脚本文件。
典型目录结构:
src/main/resources/db/migration/
├── V1__create_user_table.sql
├── V2__add_user_avatar.sql
├── V3__create_order_tables.sql
└── V4__add_order_status_index.sql应用启动时(或独立执行迁移命令),Migration 工具会:
- 读取已执行过的版本记录
- 按文件名顺序执行尚未应用的脚本
- 把执行结果写入版本表,避免重复执行
这样,数据库结构的演进历史就和 Git 提交历史一样可追溯。
Java 实践:以 Flyway 为例
Java 生态里 Flyway 和 Liquibase 都很成熟,Spring Boot 对 Flyway 的支持尤其顺滑。下面用 Flyway 举例。
1. 引入依赖
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>2. 配置
spring:
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true3. 编写迁移脚本
初始版本 V1__init_schema.sql:
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(64) NOT NULL UNIQUE,
email VARCHAR(128) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已取消',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status_created ON orders(status, created_at);后续需求变更,只新增文件,不修改已执行过的脚本,例如 V2__add_user_phone.sql:
ALTER TABLE users
ADD COLUMN phone VARCHAR(20) NULL AFTER email;
CREATE INDEX idx_users_phone ON users(phone);Flyway 会在启动时自动执行 V2,并在 flyway_schema_history 表中记录版本。
4. 团队约定
几条简单规则,长期收益很大:
| 规则 | 说明 |
|---|---|
| 只增不改 | 已上线的 migration 文件禁止修改,新变更用新文件 |
| 一条变更一个文件 | 便于 Code Review 和回滚定位 |
| 命名带语义 | V5__add_order_refund_fields.sql 比 V5__update.sql 清晰 |
| 与业务 PR 同提 | 改表结构和改代码在同一个 Pull Request 里 |
Liquibase 的思路类似,只是用 XML/YAML/SQL 描述变更集,适合需要跨多种数据库的场景。选型不是关键,「结构变更即代码」 才是。
为什么这对 AI 开发特别有用
把 Migration 文件放进工作区后,AI 能直接「看到」完整的数据模型,而不依赖你每次手动粘贴表结构。
1. 生成更准确的代码
当你让 AI 写「查询用户最近 30 天已支付订单」时,它能从 migration 里看到:
orders.status的含义(注释或命名)- 已有索引
idx_orders_status_created,可以给出更合理的查询条件 user_id外键关系,不会凭空造字段
2. 减少 Entity / Mapper 与表结构不一致
MyBatis、JPA 的实体类字段如果和真实表对不上,编译不一定报错,运行时才暴雷。Migration 文件相当于单一事实来源(Single Source of Truth),AI 生成或修改 Entity 时有据可依。
3. 新功能开发时上下文更完整
一个典型场景:你要加「订单退款」功能。工作区里有从 V1 到 V8 的完整 migration 历史,AI 能理解:
- 订单表当前有哪些字段
- 之前为什么加了某个索引
- 新表应该和哪些表建立外键
这比在对话里贴一张 Navicat 截图,或者口述「我们有个 orders 表」可靠得多。
4. Code Review 时 AI 也能帮上忙
Review 某个 PR 时,AI 可以同时看到 Java 代码变更和 V9__add_refund_table.sql,判断:
- 新增字段是否在 Entity 中同步
- 索引是否合理
- 是否遗漏了 NOT NULL 约束
和「导出整库 DDL」的区别
有人可能会问:我把整个库的 DDL 导出成一个 schema.sql 放进仓库,不也能给 AI 看吗?
可以,但不如 Migration 好,原因如下:
- DDL 快照是静态的,看不出变更意图和历史
- 容易和真实环境不同步,谁忘了更新快照,AI 就被误导
- 无法驱动环境自动升级,还得靠人工执行
Migration 兼顾了「当前完整结构」和「演进过程」:把所有 migration 文件按顺序读一遍,就是完整 schema;单独看某个文件,又能理解某次需求改了什么。
小结
AI 辅助开发不是魔法,它和你给它的上下文质量直接相关。数据库表结构是业务模型的重要载体,却常常是上下文里最缺失、最容易过期的一块。
用 Migration 管理数据库结构迭代,好处是双重的:
- 工程层面:版本可追溯、环境可复现、减少人工同步出错
- AI 协作层面:工作区里有完整、准确的表结构,AI 对业务的理解更深,生成的代码更靠谱
以 Java + Flyway 为例,落地成本很低——Spring Boot 几行配置即可。如果你已经在用 AI 写后端代码,把 migration 目录纳入日常开发流程,会是性价比很高的一步。