1. 概述
EntityManager
的 getReference()
方法自 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