生产环境禁用 db.automigrate 改表,因其只补不删、不重命名、不降约束、不控锁;必须用 golang-migrate 版本化 sql 迁移,配合超时控制、可逆操作与无锁工具应对大表变更。

别用 db.AutoMigrate 上生产环境改表结构,它只补不删、不重命名、不降约束、不控锁,跑完你以为同步了,其实 schema 和代码早已脱节。
为什么 AutoMigrate 不能改已有字段名或删字段
db.AutoMigrate 的设计目标是“安全补全”,不是“结构演进”。它会检查 struct 字段是否存在对应列,但逻辑极其保守:
- 字段从 struct 中删除 → 数据库列原封不动保留
- 字段名改了(如
Email改成ContactEmail)→ 不执行RENAME COLUMN,旧列还在,新列不会加,也不报错 - 把
gorm:"not null"去掉 → NOT NULL 约束依然存在,它不碰已有约束 - 字段类型缩容(
VARCHAR(255)→VARCHAR(50))→ 直接跳过,怕丢数据 - 索引名变更或重复定义 → 静默忽略,不报错也不覆盖
结果就是:代码里删了字段,DB 里还留着;改了字段名,DB 里两个列并存;NOT NULL 没法去掉,迁移后业务写入直接报错。
生产环境改表必须用版本化迁移(migrate CLI)
真正可控的方案是用 golang-migrate 管理 SQL 文件,每步变更都显式声明、可追溯、可回滚:
- 已有线上库,先运行
migrate status查当前版本,再migrate force 1标记初始状态,避免重复建表 - 新增迁移:用
migrate create -ext sql -dir migrations -seq add_vip_level生成000002_add_vip_level.up.sql和.down.sql -
.up.sql里只写 DDL:比如ALTER TABLE users ADD COLUMN vip_level TINYINT DEFAULT 0 -
.down.sql必须能逆向:比如ALTER TABLE users DROP COLUMN vip_level(注意 MySQL 5.7+ 才支持 DROP COLUMN) - Go 程序中调
m.Up()时务必加超时:m.Up(context.WithTimeout(ctx, 30*time.Second), migrate.All),否则锁表卡住会 hang 死
MySQL 的 DDL 不在事务里,.up.sql 中任意语句失败,前面成功的不会自动回滚,得人工介入。别在 SQL 里写 USE database_name —— 连接 URL 已指定库,某些 MySQL 版本会因此报错。
大表加字段卡住?不是 bug,是 MySQL 在线 DDL 的真实限制
即使你用了正确的迁移工具,对千万级表执行 ADD COLUMN 仍可能长时间无响应,这不是 GORM 或 migrate 的问题:
- MySQL 5.6+ 虽支持“在线 DDL”,但
ALGORITHM=INPLACE并非万能:加NOT NULL字段且无默认值,或表有全文索引,仍会触发 copy table - 卡住时 SHOW PROCESSLIST 可能看到
Waiting for table metadata lock,说明被其他长事务阻塞 - 解决方案不是换 ORM,而是提前评估:用
pt-online-schema-change或gh-ost做无锁变更,或选业务低峰期操作
最常被忽略的一点:迁移文件的序号(如 000001_)由 migrate 自增生成,禁止手动修改。多实例部署时顺序错乱,会导致不同节点执行迁移顺序不一致,schema 彻底错乱。











