别再手动写 updated_at = NOW () 了,这坑我踩过
做后端写业务表,updated_at 几乎是标配。谁都知道它用来记数据最后修改时间,排查问题、对账、做增量同步都靠它。
但绝大多数人写代码的习惯,其实从一开始就埋了雷:每次 UPDATE 都手动塞一句 updated_at = NOW(),或者在 Go 里写 user.UpdatedAt = time.Now()。
我早年也这么干,直到线上出过好几次「数据明明改了,更新时间却没动」的破事,排查到最后发现要么是同事写脚本漏了,要么是运维直接跑 SQL 改的数据,业务代码根本兜不住。
这篇不说废话,就讲一件事:把 updated_at 彻底扔给 MySQL 自己维护,省代码、少踩坑、还靠谱。
先看最常见的写法,问题藏在哪
拿 Go + GORM 举例,几乎每个项目都能看到这种代码:
go
// 修改用户信息
user.Name = "新名字"
user.Avatar = "/imgs/new.png"
user.UpdatedAt = time.Now() // 每次都要记得加
db.Model(&user).Updates(user)
单看这一段没毛病。但项目一迭代、人一多,问题全出来了。
第一个坑:总有地方会忘写
更新逻辑散得到处都是:接口里改、定时任务批量刷、MQ 消费回调改、导入脚本批量更新。只要有一个人漏写一次赋值,就会出现「数据变了,时间没变」。后面查变更记录、做数据审计,直接抓瞎。
第二个坑:很多场景代码根本管不到
手动赋值只能覆盖业务代码主动更新的情况。运维线上直接执行 UPDATE、数据库触发器联动修改、数据同步工具从别的库同步过来、甚至有人直接在客户端改表...... 这些场景完全不走业务代码,updated_at 永远躺在那不动。
最后表里面时间有的准有的不准,字段等于白建。
第三个坑:纯纯的重复垃圾代码
一个项目几十上百个更新方法,每处都要重复写一遍赋值。这行代码不产生任何业务价值,既不实现功能,也不优化逻辑,就是纯粹的模板代码。写得多,维护成本就高,毫无意义。
根治方案:MySQL 原生自带,一行配置搞定
MySQL 本身就支持字段自动更新时间,不需要触发器、不需要框架钩子,建表时字段上加个属性就行。
标准写法长这样,生产环境直接抄:
sql
`updated_at` DATETIME NOT NULL
DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP
COMMENT '最后更新时间'
就两个规则,跑一遍就记住了:
DEFAULT CURRENT_TIMESTAMP:INSERT 的时候如果你没传这个字段,数据库自动填当前时间ON UPDATE CURRENT_TIMESTAMP:这一行任何字段的值发生变化,数据库自动把这个字段刷成最新时间
说白了就是:时间维护这件事,从业务代码手里接过来,下沉到数据库层去做。底层机制永远比人工兜底靠谱。
跑一下看实际效果
拿一张用户表演示,先建表:
sql
CREATE TABLE t_user (
user_id INT PRIMARY KEY AUTO_INCREMENT,
user_name VARCHAR(32) NOT NULL,
status VARCHAR(20) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
插一条数据,不指定时间:
sql
INSERT INTO t_user (user_name, status) VALUES ('alice', 'active');
查一下:
plaintext
+---------+-----------+--------+---------------------+---------------------+
| user_id | user_name | status | created_at | updated_at |
+---------+-----------+--------+---------------------+---------------------+
| 1 | alice | active | 2026-07-05 10:00:00 | 2026-07-05 10:00:00 |
+---------+-----------+--------+---------------------+---------------------+
两个时间都自动填上了,不用你写任何代码。
再更新一下用户名:
sql
UPDATE t_user SET user_name = 'alice_new' WHERE user_id = 1;
再查:
plaintext
+---------+-----------+--------+---------------------+---------------------+
| user_id | user_name | status | created_at | updated_at |
+---------+-----------+--------+---------------------+---------------------+
| 1 | alice_new | active | 2026-07-05 10:00:00 | 2026-07-05 10:01:00 |
+---------+-----------+--------+---------------------+---------------------+
updated_at 自己变了。UPDATE 语句里完全没提这个字段,数据库帮你做了。
不管你是代码执行、脚本跑、手动敲 SQL,只要行数据改了,时间一定会刷新。这才是真正的万无一失。
Go + GORM 怎么适配,别踩冲突的坑
很多人顾虑:GORM 不是会自动维护 UpdatedAt 吗,会不会和数据库冲突?
结论先说:结构体不用动,更新时主动忽略这个字段就行。
模型结构体保留不动
gorm.Model 自带的 UpdatedAt 字段直接留着,查询、序列化、返回前端都正常用,啥也不用改:
go
type User struct {
gorm.Model
Name string `gorm:"size:32;comment:用户名"`
Status string `gorm:"size:20;comment:状态"`
}
关键一步:更新时 Omit 掉
部分 GORM 版本会默认把 UpdatedAt 拼进 UPDATE 语句里,这样就会覆盖数据库的自动赋值,等于白配置。
最稳妥的写法,所有更新统一加 Omit("updated_at"),彻底把这个字段交给数据库管:
go
db.Model(&user).Omit("updated_at").Updates(map[string]any{
"name": "alice_new",
"status": "disabled",
})
统一成三条规范,团队直接执行
- 新增数据:不用手动设 UpdatedAt,数据库默认填
- 更新数据:代码里绝对不出现
UpdatedAt = time.Now() - 所有更新语句统一 Omit ("updated_at")
几个容易踩的细节,提前说清楚
1. 用 DATETIME,别用 TIMESTAMP
TIMESTAMP 有 2038 年上限,时区处理也一堆坑。
DATETIME 完全支持 DEFAULT 和 ON UPDATE 特性,生产环境直接选 DATETIME。
2. 别加 autoUpdateTime 标签
如果结构体字段上写了 gorm:"autoUpdateTime",GORM 会强制在代码层更新时间,直接盖掉数据库规则。
想用数据库自动维护,这个标签必须删掉。
3. 字段值没变化时,不会更新
ON UPDATE 触发的前提是「行数据真的发生了改变」。
如果你 UPDATE 的值和原值一模一样,数据库会认为数据没变化,updated_at 不会刷新。
这不是 bug,是正常行为。如果业务上强制要求刷新,手动在 SQL 里带上 updated_at = NOW() 即可。
最后说两句
手动维护 updated_at 这种事,本质就是业务代码在替底层兜底。吃力不讨好,还容易出问题。
MySQL 这个特性成熟得不能再成熟了,零成本接入,一劳永逸解决时间不准的问题。
做后端开发有个很实用的原则:能下沉到数据库、框架层做的事,就别往业务代码里塞。代码越少、越专注业务,坑就越少,维护起来也越省心。