Java 程序员的 AI 进化论 | AI 帮我做 MySQL 表结构升级,三天活变半天

Java 程序员的 AI 进化论 | AI 帮我做 MySQL 表结构升级,三天活变半天

上周业务方甩过来一张改表清单:用户表加 4 个字段、订单表拆两个字段到独立子表、地址表把 phone 列从 varchar(20) 收紧到 varchar(11)、再加 3 张关联表的外键索引。一共 11 张表,37 处改动。按我以前的节奏,写 ALTER、做数据迁移脚本、配回滚 SQL、跑回归验证,三天起步。这次我拉了 AI 一起来做,从 schema diff 到 Flyway 落地,半天搞定。这篇文章记录下整个流程,包括几个坑。

一、为什么这种活最容易翻车

数据库迁移这种事,写代码的老手也不见得能避雷。改业务代码出 bug 大不了回滚发布,改表结构出事是真要命------数据写进去就回不来了。我自己踩过两次大坑:

第一次是把 nullable 字段改成 NOT NULL,没塞默认值,生产环境老数据几万行直接报错,凌晨两点被叫起来热修复。

第二次是给一张 8000 万行的表加索引,没走 ALGORITHM=INPLACE,跑了 40 分钟把从库连接池打满。

这两次教训让我得出一个判断:表结构升级不是写 SQL 的事,是风险控制的事。schema diff 要全、迁移脚本要可回滚、上线要灰度。AI 在这种「步骤明确但细节繁琐」的活上,能帮你把繁琐压下来,但风险判断得你自己兜。

二、整体方案:AI + Flyway 的协作分工

我先定了个分工:人负责风险判断和 review,AI 负责 diff、SQL 草稿、回滚脚本、变更说明。Flyway 负责版本化和落地。流程是:

  1. 用 mysqldump 把新旧 schema 导出,做 diff
  2. AI 读 diff,生成 ALTER + 数据迁移 SQL + 回滚 SQL
  3. 人 review,重点看锁表、默认值、外键、索引算法
  4. 写成 Flyway Migration 文件,进 git
  5. 灰度环境跑通,再切生产

依赖配置我贴在下面这张表,不用翻 pom.xml。

依赖 groupId artifactId 版本 作用
Flyway Core org.flywaydb flyway-core 9.22.3 数据库版本化迁移
Flyway MySQL org.flywaydb flyway-mysql 9.22.3 MySQL 方言支持
MyBatis-Plus com.baomidou mybatis-plus-boot-starter 3.5.5 ORM 层
Spring Boot Web org.springframework.boot spring-boot-starter-web 3.2.x Web 框架

整体流程我画了张图,对着图看会更清楚:

三、Schema Diff:让 AI 先看清差异

旧 schema 我用 mysqldump --no-data 导出一份,新 schema 用设计稿(DDL 文本)。两份都丢给 AI 做 diff。这里我用了三个不同模型对比了一遍,结果挺有意思:

模型 diff 召回率 漏检字段数 SQL 可执行率 平均生成耗时
DeepSeek V3 91% 3 87% 4.2s
通义千问 Max 86% 5 79% 5.8s
Claude 3.5 Sonnet 96% 1 94% 6.5s

数据来源是我自己造的 50 张测试表的对比,样本量不大但够看趋势。Claude 漏检最少但慢,DeepSeek 快但漏字段偏多。我末了选了 Claude 做主 diff,DeepSeek 做交叉校验,两边都报的改动才认。

Prompt 模板我固化成了这样:

java 复制代码
// MigrationPromptBuilder.java - com.example.migration
public class MigrationPromptBuilder {

    public String buildDiffPrompt(String oldSchema, String newSchema) {
        return """
            你是 MySQL DBA。下面是同一组表的新旧 DDL。
            请输出结构化 JSON,按表名分组,每张表列出:
            - added_columns: 新增字段(含类型、是否 NULL、默认值、注释)
            - dropped_columns: 删除字段
            - modified_columns: 类型/约束变更(含旧值、新值)
            - added_indexes / dropped_indexes
            - foreign_key_changes
            只输出 JSON,不要解释。表结构如下:
            ## OLD_SCHEMA
            %s
            ## NEW_SCHEMA
            %s
            """.formatted(oldSchema, newSchema);
    }
}

这一步关键是让 AI 输出结构化 JSON,而不是直接给 SQL。直接给 SQL 你没法 review,结构化之后你能按字段一条条对。

四、生成迁移 SQL:AI 出草稿,人定风险等级

AI 拿到 diff 后,按改动类型生成 ALTER 语句,每条都标风险等级(低/中/高)。这里我又踩了个坑:

坑一:AI 给大表加索引没加 ALGORITHM/LOCK 选项

8000 万行的订单表加复合索引,AI 直接给的是 ALTER TABLE t_order ADD INDEX idx_user_status_create(user_id, status, create_time)。这条语句默认会走 LOCK=DEFAULT,在 MySQL 5.7 上等价于 LOCK=SHARED,会把表锁成只读。生产高峰期跑这条等于自杀。

正确的写法是显式声明:

java 复制代码
// HighRiskIndexMigration.java - com.example.migration
public class HighRiskIndexMigration {

    /**
     * 大表加索引,必须显式 INPLACE + NONE 锁
     * 5.6+ 支持 online DDL,但默认不一定走 INPLACE
     */
    public String buildSafeAddIndex(String table, String indexName, String columns) {
        return """
            ALTER TABLE %s
            ADD INDEX %s (%s),
            ALGORITHM=INPLACE, LOCK=NONE;
            """.formatted(table, indexName, columns);
    }

    /**
     * 如果 INPLACE 不支持(如修改列类型),用 pt-osc 或 gh-ost
     * 这里只生成检查 SQL,实际走 DBA 工具
     */
    public String buildFallbackCheck(String table) {
        return """
            -- 检查当前 MySQL 版本是否支持该 DDL 的 INPLACE
            SELECT ALGORITHM, LOCK, COMMIT_TYPE FROM information_schema.ALGORITHM_DEPENDENCIES
            WHERE TABLE_NAME = '%s';
            """.formatted(table);
    }
}

这个坑让我意识到一个关键原则:AI 生成的 SQL 必须按风险等级分类,高危的不能直接进 Flyway。我加了一层风险拦截,高危 SQL 走 DBA 工具(pt-osc / gh-ost),只有低中危才进 Flyway 自动跑。

风险分级我定了个表:

风险等级 改动类型 执行方式
加列(带默认值) Flyway 自动
删索引 Flyway 自动
改列类型(兼容方向,如扩长度) Flyway 自动 + 灰度验证
加 NOT NULL 约束 Flyway 自动 + 默认值兜底
大表加索引(>1000 万行) DBA 工具,绕开 Flyway
改列类型(不兼容方向,如缩长度) DBA 工具 + 数据回填脚本
删列 双写过渡,不直接删

五、Flyway 落地:版本化 + 回滚脚本

低中危 SQL 进 Flyway,命名按 V{date}__{desc}.sql,回滚按 U{date}__{desc}.sql。Flyway 社区版不支持自动回滚,所以 U 文件是给人手动执行的。

java 复制代码
// FlywayMigrationRunner.java - com.example.migration
import org.flywaydb.core.Flyway;
import org.springframework.stereotype.Component;

@Component
public class FlywayMigrationRunner {

    private final Flyway flyway;

    public FlywayMigrationRunner(Flyway flyway) {
        this.flyway = flyway;
    }

    /**
     * 跑迁移前先 dry-run,输出待执行 SQL 到日志
     * Flyway 9.x 用 repair + validate 模拟
     */
    public MigrationResult dryRun() {
        MigrationResult result = new MigrationResult();
        try {
            flyway.validate();
            result.setOk(true);
            // 待执行脚本列表
            result.setPendingScripts(flyway.info().pending());
        } catch (Exception e) {
            result.setOk(false);
            result.setError(e.getMessage());
        }
        return result;
    }

    public MigrationResult execute() {
        MigrationResult result = new MigrationResult();
        try {
            int applied = flyway.migrate();
            result.setAppliedCount(applied);
            result.setOk(true);
        } catch (Exception e) {
            // 失败立即停,等人工介入执行 U 文件回滚
            result.setOk(false);
            result.setError(e.getMessage());
        }
        return result;
    }

    public record MigrationResult(boolean ok, int appliedCount,
                                   String error, Object[] pendingScripts) {
        // 简化的结果记录对象
        public void setOk(boolean ok) {}
        public void setError(String error) {}
        public void setPendingScripts(Object[] scripts) {}
        public void setAppliedCount(int count) {}
    }
}

坑二:AI 生成的回滚 SQL 漏了数据维度

AI 给的回滚脚本只回滚 schema(ALTER TABLE t_user DROP COLUMN nickname),但生产环境已经用新字段跑了两小时,数据有了,回滚时这些数据就丢了。

正确的回滚策略应该是双写过渡:新字段加上之后,先让代码同时写新旧两个字段,跑一周确认稳定,再把老字段下掉。AI 一开始不理解这个节奏,得在 Prompt 里明确告诉它「删字段必须走双写,回滚脚本只针对加字段的场景」。

我把这条规则加进了 Prompt:

java 复制代码
// RollbackStrategySelector.java - com.example.migration
public class RollbackStrategySelector {

    public enum Strategy {
        DROP_COLUMN,    // 直接 DROP,回滚用 ALTER ADD(默认值兜底)
        DUAL_WRITE,     // 双写过渡,删字段延后 7 天
        NONE            // 不可回滚(数据迁移类,需备份)
    }

    public Strategy choose(String changeType, boolean hasDataMigration) {
        if (hasDataMigration) return Strategy.NONE;
        if ("DROP_COLUMN".equals(changeType)) return Strategy.DUAL_WRITE;
        return Strategy.DROP_COLUMN;
    }
}

六、实测数据:人工 vs AI 辅助

这次迁移我对比了去年的同规模项目(10 张表、30 处改动),耗时差异挺明显:

阶段 纯人工(去年) AI 辅助(本次) 节省
Schema diff 3 小时 12 分钟 93%
写 ALTER SQL 2 小时 25 分钟 79%
写回滚脚本 1.5 小时 10 分钟 89%
Code Review 1 小时 2.5 小时 -150%
灰度验证 4 小时 3 小时 25%
总计 11.5 小时 6.3 小时 45%

注意 review 时间反而涨了 150%------这正是我前面强调的,AI 出的 SQL 必须逐条核风险,这步偷不得懒。整体省的时间主要在写脚本这种重复劳动上。

另一个对比维度是 AI 漏检率随表数量的变化:

表数量 AI 漏检字段数 人工二次校验耗时
5 张 0 8 分钟
10 张 1 22 分钟
20 张 3 45 分钟
50 张 7 2 小时

表数越多,AI 漏检越多,人工校验成本非线性上涨。我的判断是:AI 适合 20 张表以内的迁移,超过 30 张就该拆批次

七、灰度阶段又冒出来三个问题

你以为 review 过了就稳了?灰度环境一跑又出事。这次灰度阶段我额外发现了三个 AI 没主动提示的问题,每个都得手动补。

问题一:外键约束的依赖顺序

AI 一次生成了 11 张表的迁移脚本,但没考虑外键依赖顺序。t_order_item 引用 t_order 的 id,如果先改 t_order_item 加外键,再改 t_order 主键类型,外键创建会失败。Flyway 默认按文件名顺序执行,得手动给文件命名加序号前缀。我把脚本命名规则从 V{date}__{desc}.sql 改成 V{date}_{seq:02d}__{desc}.sql,seq 是依赖序号。

问题二:触发器和存储过程引用了被改的列

老库有个触发器在 t_userphone 列上做格式校验,AI 把列改成 varchar(11) 没动触发器。灰度跑插入测试时触发器报长度溢出。这个 AI 真发现不了,因为它只看 DDL,不看触发器和存储过程定义。补了一个步骤:迁移前导出所有触发器/存储过程定义,grep 被改列名,看有没有引用。

问题三:binlog 格式对 ONLINE DDL 的影响

生产 MySQL 8.0 默认 binlog_format=ROW,部分 ONLINE DDL 操作会走 row-based 复制,从库应用 binlog 时是大事务。一张 5000 万行的表加索引,主库 12 分钟跑完,从库 binlog 应用拖了 35 分钟,延迟告警。这个坑也是 AI 没主动提的------它不知道你的从库拓扑。补救方案是把这种 DDL 拆成小批次,或者错峰执行。

补完这三个洞,灰度才算真正跑通。灰度耗时统计:

灰度阶段问题 AI 是否识别 人工补救耗时
外键依赖顺序 25 分钟重命名脚本
触发器引用变更列 40 分钟导出+grep
binlog 大事务 30 分钟拆批次
合计 0/3 1.6 小时

这张表挺说明问题:AI 在 DDL 本身写得不错,但跨对象依赖(触发器/存储过程/复制拓扑)几乎完全失明。这是它的能力边界,也是人 review 时最该盯的地方。

八、踩坑总结和检查清单

整体跑下来,AI 辅助数据库迁移能用,但必须有人兜底。前面提到的坑再强调一遍:

  1. AI 不会主动加大表 DDL 的 ALGORITHM/LOCK 参数,高危 DDL 必须走 DBA 工具,不进 Flyway
  2. AI 的回滚脚本只考虑 schema,不考虑数据,删字段必须走双写过渡
  3. AI 看不见跨对象依赖(触发器、存储过程、复制拓扑),灰度阶段必须人工补一遍

下面这张清单是我每次迁移前必走的流程:

检查项 建议
Schema diff 是否双模型交叉验证 用两个不同模型做 diff,两边都报的改动才认
大表(>1000 万行)DDL 是否标高危 高危走 pt-osc / gh-ost,不进 Flyway
ALTER 语句是否显式声明 ALGORITHM/LOCK 不声明就按默认走,可能锁表
新增 NOT NULL 字段是否带默认值 不带默认值老数据直接报错
删字段是否走双写过渡 直接删会丢生产数据,至少双写 7 天
回滚脚本是否覆盖数据维度 只回滚 schema 不够,数据也要能回
Flyway 文件命名是否含日期 V20260817_01__desc.sql,避免冲突
灰度环境是否完整跑过一遍 灰度不通不要上生产
是否备份了迁移前的表结构 mysqldump --no-data 留一份,出事能恢复
Code Review 是否逐条核风险等级 每条 SQL 都要标低/中/高,高危不进 Flyway

这套流程我跑了一个月,五次迁移零事故。AI 没让我变成甩手掌柜,反而让我把精力从「写 SQL」挪到了「判风险」上------后者才是数据库迁移真正的核心能力。如果你也打算把 AI 引入迁移流程,我建议先从低风险的纯加字段场景试起,跑顺了再扩展到改列、删字段这种高风险操作。别一上来就让它处理核心业务表,翻车成本太高。

相关推荐
三言老师1 小时前
K8s集群运行时异常趋势分析预警实操
java·开发语言·kubernetes
吃饱了得干活1 小时前
从经典的三层架构到DDD:一次对“业务逻辑层”的解剖与重构
java·后端·架构
SL_staff1 小时前
中小离散制造如何用工艺路线模板解决‘人走流程丢’——JVS-APS 实践解析
java·设计模式·全栈
我命由我123451 小时前
Android 开发问题:使用 AndroidTreeView 时,自定义视图无法撑满父容器
android·java·java-ee·kotlin·android studio·android-studio·android runtime
冷雨夜中漫步2 小时前
DeepSeek Harness:一切皆插件的 AI Agent 框架深度解析
java·人工智能·ai·开源·github
lhldsg2 小时前
社区健身系统开发实战:从需求分析到落地部署全流程指南
java·开发语言·小程序·需求分析
覆东流2 小时前
4.Java运算符与表达式
java·开发语言·后端
JacksonMx2 小时前
ContentCachingRequestWrapper 实战:解决请求体“只能读一次”的官方方案
java·spring boot·spring
微石科技3 小时前
医疗设备维保如何从被动维修转向预防管理?宁波微石科技让运行数据提前发信号
java·后端·struts