1. 概述

EntityManagergetReference() 方法自 JPA 1.0 起就已存在,但不少开发者对其行为感到困惑——它的实际表现依赖于底层持久化框架的实现。尤其在使用 Hibernate 时,这个方法的行为和直觉可能不一致。

本文将深入剖析 getReference()Hibernate EntityManager 中的实际行为,帮你避开常见坑点,提升数据库操作效率。

2. EntityManager 的查询方式

EntityManager 提供了两种无需写 JPQL 即可根据主键加载实体的方法。它们看似相似,实则大有不同。

2.1. find()

这是最常用的加载方式:

Game game = entityManager.find(Game.class, 1L);

行为特点

  • 立即执行 SELECT 查询,返回一个已初始化的实体对象
  • 若数据库中无对应记录,返回 null

⚠️ 注意:即使你只关心主键,find() 也会把整个实体字段都查出来。

2.2. getReference()

另一种加载方式:

Game game = entityManager.getReference(Game.class, 1L);

行为特点

  • 返回一个仅主键字段初始化的代理对象(proxy)
  • 其他字段是懒加载的,只有在真正访问时才会触发 SELECT 查询
  • 从语义上讲,它表示“我只需要一个引用,不关心具体数据”

❌ 误区澄清:

“用了 getReference() 就一定不会查数据库” —— 错!

是否触发 SELECT,取决于你后续怎么用这个代理对象。

3. 示例场景

我们用 Game(游戏)和 Player(玩家)两个实体来演示,关系为:一个游戏可有多个玩家。

3.1. 实体定义

Game 实体:

@Entity
public class Game {

    @Id
    private Long id;

    private String name;

    // 构造函数、getter、setter 省略
}

Player 实体:

@Entity
public class Player {

    @Id
    private Long id;

    private String name;

    // 构造函数、getter、setter 省略
}

3.2. 建立关联

Player 中添加对 Game 的引用,形成 @ManyToOne 关系:

@ManyToOne
private Game game;

这样,每个玩家可以属于一个游戏。

4. 实战测试用例

测试前先准备数据:

entityManager.getTransaction().begin();

entityManager.persist(new Game(1L, "Game 1"));
entityManager.persist(new Game(2L, "Game 2"));
entityManager.persist(new Player(1L, "Player 1"));
entityManager.persist(new Player(2L, "Player 2"));
entityManager.persist(new Player(3L, "Player 3"));

entityManager.getTransaction().commit();

同时,开启 Hibernate SQL 日志,方便观察:

<property name="hibernate.show_sql" value="true"/>

4.1. 更新字段:find() vs getReference()

目标:更新 Game 的名称。

使用 find()

Game game1 = entityManager.find(Game.class, 1L);
game1.setName("Game Updated 1");
entityManager.persist(game1);

生成的 SQL:

Hibernate: select g1_0.id,g1_0.name from Game g1_0 where g1_0.id=?
Hibernate: update Game set name=? where id=?

✅ 没问题,但 SELECT 是必须的,因为要先加载实体。

现在换成 getReference()

Game game1 = entityManager.getReference(Game.class, 1L);
game1.setName("Game Updated 2");
entityManager.persist(game1);

你以为只会有 UPDATE?错!实际 SQL:

Hibernate: select g1_0.id,g1_0.name from Game g1_0 where g1_0.id=?
Hibernate: update Game set name=? where id=?

⚠️ 关键结论

只要你调用了代理对象的 setter,Hibernate 就会先触发 SELECT 加载完整实体,否则无法知道原始值(脏检查需要)。
所以,用 getReference() 更新字段,并不能避免多余的 SELECT

4.2. 删除实体:getReference() 真香

删除操作就不一样了。

使用 find() 删除:

Player player2 = entityManager.find(Player.class, 2L);
entityManager.remove(player2);

SQL:

Hibernate: select p1_0.id,g1_0.id,g1_0.name,p1_0.name 
           from Player p1_0 left join Game g1_0 on g1_0.id=p1_0.game_id where p1_0.id=?
Hibernate: delete from Player where id=?

使用 getReference() 删除:

Player player3 = entityManager.getReference(Player.class, 3L);
entityManager.remove(player3);

SQL:

Hibernate: delete from Player where id=?

完美!
删除只需要主键,getReference() 返回的代理对象完全够用,避免了无意义的 SELECT 查询

4.3. 更新关联关系:getReference() 的最佳实践

这是 getReference() 最典型的使用场景。

假设我们要让 Player 1 参加 Game 2

错误做法(多一次 SELECT):

Game game1 = entityManager.find(Game.class, 1L); // 多余查询
Player player1 = entityManager.find(Player.class, 1L);
player1.setGame(game1);
entityManager.persist(player1);

SQL:

Hibernate: select g1_0.id,g1_0.name from Game g1_0 where g1_0.id=?
Hibernate: select p1_0.id,g1_0.id,g1_0.name,p1_0.name from Player p1_0 ... where p1_0.id=?
Hibernate: update Player set game_id=?,name=? where id=?

✅ 正确做法(零额外查询):

Game game2 = entityManager.getReference(Game.class, 2L); // 只要一个引用
Player player1 = entityManager.find(Player.class, 1L);
player1.setGame(game2);
entityManager.persist(player1);

SQL:

Hibernate: select p1_0.id,g1_0.id,g1_0.name,p1_0.name from Player p1_0 ... where p1_0.id=?
Hibernate: update Player set game_id=?,name=? where id=?

踩坑总结

当你只需要设置外键(比如 player.setGame(gameRef)),getReference() 获取目标实体,能有效避免不必要的 SELECT,提升性能。

5. 一级缓存的影响:find()getReference() 可能都没查询

Hibernate 的一级缓存(Persistence Context)会让事情变得更复杂。

看这个例子:

entityManager.getTransaction().begin();
Game game = new Game(1L, "Game 1");
Player player = new Player(1L, "Player 1");
entityManager.persist(game);
entityManager.persist(player);
entityManager.getTransaction().commit();

// 新事务
entityManager.getTransaction().begin();
Game gameRef = entityManager.getReference(Game.class, 1L); // 缓存命中
Player player1 = entityManager.find(Player.class, 1L);
player1.setGame(gameRef);
entityManager.persist(player1);
entityManager.getTransaction().commit();

SQL 输出:

Hibernate: update Player set game_id=?,name=? where id=?

⚠️ 注意
此时无论是 find() 还是 getReference(),**都不会触发 SELECT**,因为实体已经在当前 EntityManager 的缓存中。

结论

一级缓存存在时,find()getReference() 行为一致,都直接返回缓存对象,不查库。

6. 不同 JPA 实现的行为差异

JPA 规范允许 getReference() 在实体不存在时抛出 EntityNotFoundException,但 Hibernate 默认不这么做

Hibernate 的设计哲学是:能少查一次就少查一次。所以默认情况下:

  • getReference() 返回代理,即使数据库里没有这条记录(延迟验证)
  • 只有在真正访问非主键字段时,才会触发 SELECT 并可能抛出异常

如果你希望严格遵循 JPA 规范,可以开启:

<property name="hibernate.jpa.compliance.proxy" value="true"/>

开启后,getReference() 会立即执行 SELECT,并可能抛出 EntityNotFoundException,行为更“安全”但性能更差。

7. 总结

场景 推荐方法 原因
✅ 更新关联关系(如外键) getReference() 避免无意义的 SELECT,简单粗暴提升性能
✅ 删除实体 getReference() 只需主键,无需加载完整对象
❌ 更新实体字段 find() getReference() 仍会触发 SELECT,无收益
⚠️ 实体可能不存在 注意配置 hibernate.jpa.compliance.proxy 控制是否立即验证存在性

📌 核心口诀

getReference() 不是性能银弹,但在设置关联或删除时,它是最佳选择。理解其代理机制和一级缓存的影响,才能避免踩坑。

所有示例代码已托管至 GitHub:https://github.com/your-repo/hibernate-jpa-examples


原始标题:Quick Guide to EntityManager#getReference()