JPA 的脏检查:一次“没 save() 却更新了”的排查记录

第 11 篇 · 共100篇|用代码丈量成长 ------ 坚持写下去,就是最好的成长。

Hibernate:你不 save,我也要帮你 UPDATE

那天在排查一个挺诡异的事:

某个分表系统(用的是 ShardingSphere),日志里出现了奇怪的更新。明明业务逻辑只改了一条数据,最后路由日志却显示更新了好几张分表。

最开始我们都以为是 SQL 写错了,或者 ShardingSphere 的分片配置有问题。结果一通跟踪下来,发现问题根本不在 SQL,而在 JPA 自己干的"好事"


"我没 save(),它自己就 UPDATE 了?"

当时业务代码大概长这样:

scss 复制代码
@Transactional
public void updateOrder() {
    Order order = orderRepo.findById(9527L).orElseThrow();
    order.setStatus("PAID");
    order.setUpdateTime(new Date());
}

没调用 save(),也没写 update 语句,但提交事务时,数据库那边真的多了一条 UPDATE。

一开始我还以为是同事哪里调用错了,后来把日志和 SQL 都跟了一遍,才想起来------这是 JPA 的"脏检查"(Dirty Checking)。

JPA 查出来的实体在事务里是托管状态,Hibernate 会在事务提交前比对对象的快照,如果字段被改了,就自动发 UPDATE。

这机制平时挺贴心的,但在分表系统里就变成了个坑。


分表的锅:一个小 UPDATE,变成多表广播

JPA 发的 UPDATE 语句是:

bash 复制代码
update orders set status=?, update_time=? where id=?

如果分片键不是 id,或者 WHERE 条件没带上分片字段,ShardingSphere 就懵了------它不知道该发到哪张分表去,只能"全表广播更新"。

日志里就能看到:

sql 复制代码
Logic SQL: update orders set status=?, update_time=? where id=?
Actual SQL: update orders_1001_2025 ...
Actual SQL: update orders_1002_2025 ...

一条逻辑 SQL,打到了两张甚至多张分表上。

看着这些 UPDATE 日志,心里直犯嘀咕:这谁背得起啊。


对照实验:同样的逻辑,不同的状态

为了确认是不是 JPA 自动干的,我们做了几个小实验。

情况一:有事务,自动 UPDATE

typescript 复制代码
@Transactional
public void caseA() {
    Order o = repo.findById(9527L).orElseThrow();
    o.setStatus("PAID");
    o.setUpdateTime(new Date());
}

提交事务时 Hibernate 会自动发 UPDATE。

情况二:detach 后再改,不会 UPDATE

scss 复制代码
@Transactional
public void caseB() {
    Order o = repo.findById(9527L).orElseThrow();
    entityManager.detach(o);
    o.setStatus("CANCELLED");
}

因为被 detach 掉了,不再是托管对象,Hibernate 就不会再跟踪。

只是这种写法容易破坏工作单元的一致性,后续再 merge 回去挺麻烦,不太推荐。

情况三:只读事务,最干净的做法

ini 复制代码
@Transactional(readOnly = true)
public UserDTO view(Long id) {
    User u = userRepo.findById(id).orElseThrow();
    return toDto(u);
}

只读事务 Hibernate 默认不 flush,用在纯查询场景下特别合适。

这种写法读起来就安全,不用担心哪天有人无意中改了个字段把数据库带跑偏。


最后的方案其实挺简单:

查询完直接转 DTO,再改 DTO。

这样实体就不会处在托管状态,也不会触发自动更新。

从那之后,凡是看到有事务的地方改实体对象,大家都会多问一句:"这个改动是要落库的,还是只是展示?"

有时候小小一个约定,能省下很多奇怪的线上故障。


一点后话

其实 JPA 的脏检查机制挺聪明的,它帮我们省掉了很多 save() 的麻烦。只是当系统复杂起来,比如有分表、有中间件、有异步逻辑时, "自动"反而成了风险

那次排查让我意识到,ORM 的便利是有边界的。它能帮你管理状态,但你得清楚自己在哪个状态里动了手。

有时候写代码不是在防 bug,而是在防"好心办坏事"。

你的阅读与同行,让路途更有意义

愿我们一路向前,成为更好的自己

相关推荐
Neptune141 分钟前
深入浅出:理解js的‘万物皆对象’与原型链
前端·javascript
王霸天1 小时前
扒一扒 Vue3 大屏适配插件 vfit 的源码:原来这么简单?
前端
王霸天1 小时前
拒绝 rem 计算!Vue3 大屏适配,我是这样做的 (vfit 使用体验)
前端
xinyu_Jina1 小时前
ikTok Watermark Remover:客户端指纹、行为建模与自动化逆向工程
前端·人工智能·程序人生·信息可视化
盗德1 小时前
最全音频处理WaveSurfer.js配置文档
前端·javascript
Heo1 小时前
关于Gulp,你学这些就够了
前端·javascript·面试
Irene19911 小时前
web前端开发岗位就业前景和未来变化分析(附:AI技术如何进一步影响前端工作)
前端
码途进化论1 小时前
基于 Node.js 和 SSH2 的 Docker 自动化部署实践
前端·自动化运维
溪饱鱼1 小时前
NextJs + Cloudflare Worker 是出海最佳实践
前端·后端