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,而是在防"好心办坏事"。

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

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

相关推荐
NeilCarmack15 分钟前
Deepseek-harness增加桌面版端序列:第 2 讲 · spawn Electron:当前进程如何“交棒“
前端·javascript·electron
上海魁鲸科技有限公司1 小时前
APS高级排产系统到底有什么用?一文讲清功能、选型与落地建议
前端·microsoft·excel
陈随易1 小时前
Bun v1.4 更新总结:把浏览器、图片、定时任务和工程工具都装进一个运行时
前端·后端·程序员
东风破_2 小时前
TypeScript 高级类型进阶:keyof、Exclude、Record 与类型组合思想
前端·后端·typescript
DS随心转插件3 小时前
Grok生成的html怎么导出——AI导出鸭:大模型结构化输出的“最后一公里”工程化解构
前端·人工智能·ai·html·豆包·deepseek·ai导出鸭
এ慕ོ冬℘゜3 小时前
使用 jQuery 动态渲染表格与状态切换
前端·javascript·jquery
人间凡尔赛4 小时前
2026 前端必修:4 个让 CSS 脱胎换骨的现代新特性(附实战代码)
前端·css
用户2181697049304 小时前
Flutter (二十) 轮播图
前端
前端snow4 小时前
ai agent - RAG 汇总
前端
计算机魔术师4 小时前
AI为拿高分不惜入侵网站:22个模型作弊审计,真相让人背脊发凉
前端