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

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

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

相关推荐
于慨15 小时前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz15 小时前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
从前慢丶15 小时前
前端交互规范(Web 端)
前端
CHU72903515 小时前
便捷约玩,沉浸推理:线上剧本杀APP功能版块设计详解
前端·小程序
GISer_Jing16 小时前
Page-agent MCP结构
前端·人工智能
王霸天16 小时前
💥别再抄网上的Scale缩放代码了!50行源码教你写一个永不翻车的大屏适配
前端·vue.js·数据可视化
小领航16 小时前
用 Three.js + Vue 3 打造炫酷的 3D 行政地图可视化组件
前端·github
@大迁世界16 小时前
2026年React大洗牌:React Hooks 将迎来重大升级
前端·javascript·react.js·前端框架·ecmascript
PieroPc16 小时前
一个功能强大的 Web 端标签设计和打印工具,支持服务器端直接打印到局域网打印机。Fastapi + html
前端·html·fastapi
悟空瞎说16 小时前
深入 Vue3 响应式:为什么有的要加.value,有的不用?从设计到源码彻底讲透
前端·vue.js