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

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

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

相关推荐
NEXT066 分钟前
CSS 选择器深度实战:从“个十百千”权重法到零 DOM 动画的降维打击
前端·css
Mapmost12 分钟前
防患未“燃”:掌握森林火灾仿真分析,精准把控火势蔓延趋势
前端
半世轮回半世寻14 分钟前
前端开发里最常用的5种本地存储
前端·javascript
OpenTiny社区16 分钟前
TinyPro v1.4.0 正式发布:支持 Spring Boot、移动端适配、新增卡片列表和高级表单页面
java·前端·spring boot·后端·开源·opentiny
爱上妖精的尾巴20 分钟前
7-9 WPS JS宏 对象使用实例6:按条件读取多表再拆成多表
前端·javascript·wps·jsa
有意义20 分钟前
现代 React 路由实践指南
前端·vue.js·react.js
三木檾21 分钟前
Cookie 原理详解:Domain / Path / SameSite 一步错,生产环境直接翻车
前端·浏览器
开始学java21 分钟前
踩坑实录:把 useRef 写进 JSX 后,我终于分清它和 useState 的核心差异
前端
二DUAN帝21 分钟前
像素流与UE通信
前端·javascript·css·ue5·html·ue4·html5
1024小神22 分钟前
cloudflare+hono框架实现jwtToken认证,并从token中拿到认证信息
前端