bigywater.tech
Java · 415

JPA Persistence Context:实体为什么会被自动跟踪

JPA 里“改了对象就自动保存”不是魔法,而是持久化上下文的脏检查。这篇用真实的 SQL 日志与计时量出:它什么时候发 UPDATE、什么时候不发,一级缓存的身份映射,游离对象与 merge,flush 的时机,以及托管实体变多之后的代价。

0 条 / 1 条
托管实体改成相同的值或改了又改回原值 commit 时的 UPDATE 数 / 真正改了值的 UPDATE 数
0.08 → 3.70 ms
没有任何修改时 flush 的耗时:1000 个托管实体 vs 50000 个
0.005 ms
50000 个实体加载后 em.clear() 再 flush 的耗时

脏检查

实测 · 本机执行输出
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 where p1_0.id=?
  find 之后:SQL 语句数 +1,em.contains(p) = true,age = 30
  p.age = 31(没有调用任何持久化方法);在 commit 之前 SQL 语句数 +1
      SQL> update P set age=?,name=? where id=?
  commit 之后新增 SQL 语句 1 条(更新语句,见上面的 Hibernate: update …)
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 where p1_0.id=?
  把 age 赋成相同的值再 commit:新增 UPDATE 0 条(只比较值,不看是否调用过 setter)
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 where p1_0.id=?
  改成 99 又改回原值再 commit:新增 UPDATE 0 条(与加载时的快照比较,中间状态不重要)
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 where p1_0.id=?
  批量更新绕过持久化上下文:内存里 bob 的 age = 40
      SQL> update P p1_0 set age=70 where p1_0.id=2
  JPQL update 影响 1 行;内存里的 p.age 仍是 40,数据库里已经是 70
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 where p1_0.id=?
  em.refresh(p) 之后 p.age = 70

一级缓存与身份映射

实测 · 本机执行输出
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 where p1_0.id=?
  同一个 EntityManager 里 find 两次:x1 == x2 为 true,SQL 语句数 +1
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 where p1_0.id=1
  再用 JPQL 查同一行:返回的对象 == x1 为 true,SQL 语句数 +2(查询仍然发出了 SQL,但返回的是已托管的那个对象)
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 where p1_0.id=?
  另一个 EntityManager 里 find 同一行:y == x1 为 false,y.equals(x1) 为 false(没有重写 equals,所以是不同对象)
  getReference(2):新增 SQL 语句 0 条(没有查询),对象类 = P(Hibernate 生成的代理子类),Hibernate.isInitialized = false
  直接读代理对象的字段 ref.name = null(代理自己的字段是 null,没有触发加载),Hibernate.isInitialized = false
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 where p1_0.id=?
  调用 ref.getName() = bob:新增 SQL 语句 1 条,Hibernate.isInitialized = true

托管、游离与 merge

实测 · 本机执行输出
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 where p1_0.id=?
  detach 之后改 age = 500,commit:新增 UPDATE 0 条,contains = false
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 where p1_0.id=?
  把游离对象 d 交给 merge:返回对象 merged 与 d 是否同一个 = false,merged 是否托管 = true,d 是否托管 = false,merged.age = 500
      SQL> update P set age=?,name=? where id=?
  commit:新增 UPDATE 1 条;之后再改 d.age = 1(d 仍然游离)不会有任何效果
  新建的 P:persist 前 id = null,contains = false
      SQL> insert into P (age,name) values (?,?)
  persist 之后 id = 3(IDENTITY 策略要立刻 INSERT 才能得到 id),contains = true,SQL 语句数是否已增加:是

flush 时机

实测 · 本机执行输出
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 where p1_0.id=?
      SQL> update P set age=?,name=? where id=?
      SQL> select count(p1_0.id) from P p1_0 where p1_0.age=71
  FlushMode = AUTO:改了 bob 的 age = 71 之后立刻 JPQL 查询 count(age = 71) = 1,查询时新增 SQL 语句 2 条(先 UPDATE 再 SELECT)
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 where p1_0.id=?
      SQL> select count(p1_0.id) from P p1_0 where p1_0.age=71
  FlushMode = COMMIT:改了 bob 的 age = 71 之后立刻 JPQL 查询 count(age = 71) = 0,查询时新增 SQL 语句 1 条(只有 SELECT)

托管实体的代价

实测 · 本机执行输出
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 order by p1_0.id fetch first ? rows only
      SQL> update P set age=?,name=? where id=?
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 order by p1_0.id fetch first ? rows only
      SQL> update P set age=?,name=? where id=?
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 order by p1_0.id fetch first ? rows only
      SQL> update P set age=?,name=? where id=?
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 order by p1_0.id fetch first ? rows only
      SQL> update P set age=?,name=? where id=?
  1000 个托管实体:没有任何修改时 flush 约 0.08 ms(31 次中位数,第二轮,JIT 已预热);改其中 1 个后 flush 1.19 ms;em.clear() 之后 flush 0.003 ms
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 order by p1_0.id fetch first ? rows only
      SQL> update P set age=?,name=? where id=?
  10000 个托管实体:没有任何修改时 flush 约 0.71 ms(31 次中位数,第二轮,JIT 已预热);改其中 1 个后 flush 1.13 ms;em.clear() 之后 flush 0.002 ms
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 order by p1_0.id fetch first ? rows only
      SQL> update P set age=?,name=? where id=?
  50000 个托管实体:没有任何修改时 flush 约 3.70 ms(31 次中位数,第二轮,JIT 已预热);改其中 1 个后 flush 4.82 ms;em.clear() 之后 flush 0.005 ms
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 order by p1_0.id fetch first ? rows only
      SQL> select p1_0.id,p1_0.age,p1_0.name from P p1_0 order by p1_0.id fetch first ? rows only
  50000 个以只读方式加载的实体(hint org.hibernate.readOnly = true):flush 约 2.87 ms(31 次中位数)
  只读加载的实体被修改后 flush:新增 SQL 语句 0 条(只读实体的修改不会被写回)

分步演示

持久化上下文
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。Hibernate ORM 6.6.4.Final、PostgreSQL JDBC 42.7.4、本机 Homebrew PostgreSQL 16.15(端口 9501),主键策略 IDENTITY,hbm2ddl = create-drop;SQL 来自 hibernate.show_sql,语句计数来自 Hibernate Statistics
    • 没有验证:Spring Data JPA / @Transactional 的事务边界与 OpenEntityManagerInView、@DynamicUpdate、字节码增强(enhancement)后的脏检查、StatelessSession、乐观锁 / 二级缓存对 flush 的影响、其它 JPA 实现(EclipseLink)
    京ICP备2026067502号-1京公网安备11010502063487号