1. 概述
JPA 2.1 引入了 Entity Graph 特性,作为一种更灵活、更高效的方式来处理实体关联数据的加载问题。
它允许我们通过定义一个“图”模板,指定哪些关联字段需要被一起加载,并且可以在运行时动态选择使用哪种图(Graph)策略。
在本文中,我们将深入讲解如何创建和使用 Entity Graph。
2. Entity Graph 解决了什么问题?
在 JPA 2.0 及之前版本中,我们通常使用 FetchType.LAZY
和 FetchType.EAGER
来控制关联字段的加载行为:
- ✅
LAZY
:懒加载,访问时才从数据库加载 - ✅
EAGER
:立即加载,获取主实体时就一并加载
但问题是:这些策略是静态配置的,无法在运行时切换。比如你可能希望在某个场景下懒加载,在另一个场景下立即加载,但注解不支持这种灵活性。
这就是 Entity Graph 出现的原因:
Entity Graph 的核心目标是提升运行时性能,特别是在加载实体及其关联关系时减少不必要的 SQL 查询次数。
简单来说,JPA 提供商会一次性把图中定义的所有字段都查出来,避免后续再发 N+1 查询。
3. 实体模型定义
为了演示 Entity Graph,我们先定义几个实体类。
假设我们要做一个博客系统,用户可以发布文章、评论文章:
User 实体
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
//...
}
Post 实体
@Entity
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String subject;
@OneToMany(mappedBy = "post")
private List<Comment> comments = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
private User user;
//...
}
Comment 实体
@Entity
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String reply;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
private Post post;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
private User user;
//...
}
我们要加载的图结构大致如下:
Post -> user:User
-> comments:List<Comment>
comments[0]:Comment -> user:User
comments[1]:Comment -> user:User
4. 使用 FetchType 策略加载关联实体
JPA 中有两种 Fetch 策略:
- ✅
FetchType.EAGER
:立即加载,默认用于@Basic
,@ManyToOne
,@OneToOne
- ✅
FetchType.LAZY
:懒加载,默认用于@OneToMany
,@ManyToMany
,@ElementCollection
举个例子:
@OneToMany(mappedBy = "post", fetch = FetchType.EAGER)
private List<Comment> comments = new ArrayList<>();
上面这段代码会让每次加载 Post
时自动加载其所有评论。
而如果使用 LAZY
:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private Post post;
则表示在访问 post
字段时才会触发查询。
⚠️ 注意:虽然标记为 LAZY
,但具体实现可能仍然会提前加载(例如 Hibernate 的 batch fetching),所以不能完全依赖这个注解来保证延迟加载。
最关键的是:这些策略是写死在注解里的,无法动态切换。
这正是 Entity Graph 要解决的问题。
5. 定义 Entity Graph
我们可以用两种方式定义 Entity Graph:
5.1. 使用注解方式定义
使用 @NamedEntityGraph
注解可以指定我们要加载的属性:
@NamedEntityGraph(
name = "post-entity-graph",
attributeNodes = {
@NamedAttributeNode("subject"),
@NamedAttributeNode("user"),
@NamedAttributeNode("comments"),
}
)
@Entity
public class Post {
@OneToMany(mappedBy = "post")
private List<Comment> comments = new ArrayList<>();
//...
}
如果我们还想进一步加载 Comment
关联的 User
,可以使用子图(subgraph):
@NamedEntityGraph(
name = "post-entity-graph-with-comment-users",
attributeNodes = {
@NamedAttributeNode("subject"),
@NamedAttributeNode("user"),
@NamedAttributeNode(value = "comments", subgraph = "comments-subgraph"),
},
subgraphs = {
@NamedSubgraph(
name = "comments-subgraph",
attributeNodes = {
@NamedAttributeNode("user")
}
)
}
)
@Entity
public class Post {
@OneToMany(mappedBy = "post")
private List<Comment> comments = new ArrayList<>();
//...
}
此外,也可以通过 orm.xml
配置文件定义:
<entity-mappings>
<entity class="com.baeldung.jpa.entitygraph.Post" name="Post">
...
<named-entity-graph name="post-entity-graph">
<named-attribute-node name="comments" />
</named-entity-graph>
</entity>
...
</entity-mappings>
5.2. 使用 JPA API 动态定义
我们也可以在运行时通过 EntityManager
创建 Entity Graph:
EntityGraph<Post> entityGraph = entityManager.createEntityGraph(Post.class);
entityGraph.addAttributeNodes("subject");
entityGraph.addAttributeNodes("user");
entityGraph.addSubgraph("comments").addAttributeNodes("user");
这种方式更加灵活,适用于需要根据条件动态构建图的场景。
6. 使用 Entity Graph
6.1. Entity Graph 类型
JPA 提供了两个 hint 来控制 Entity Graph 的加载方式:
- ✅
jakarta.persistence.fetchgraph
:只加载图中明确指定的属性 - ✅
jakarta.persistence.loadgraph
:除了图中指定的属性外,还会加载默认标记为EAGER
的字段
⚠️ 注意:主键和版本号(如果有)始终会被加载。
6.2. 加载 Entity Graph
使用 EntityManager.find()
EntityGraph entityGraph = entityManager.getEntityGraph("post-entity-graph");
Map<String, Object> properties = new HashMap<>();
properties.put("jakarta.persistence.fetchgraph", entityGraph);
Post post = entityManager.find(Post.class, id, properties);
Hibernate 生成的 SQL 会一次性加载所有相关数据:
select
post0_.id as id1_1_0_,
post0_.subject as subject2_1_0_,
post0_.user_id as user_id3_1_0_,
comments1_.post_id as post_id3_0_1_,
comments1_.id as id1_0_1_,
comments1_.id as id1_0_2_,
comments1_.post_id as post_id3_0_2_,
comments1_.reply as reply2_0_2_,
comments1_.user_id as user_id4_0_2_,
user2_.id as id1_2_3_,
user2_.email as email2_2_3_,
user2_.name as name3_2_3_
from
Post post0_
left outer join
Comment comments1_
on post0_.id=comments1_.post_id
left outer join
User user2_
on post0_.user_id=user2_.id
where
post0_.id=?
使用 JPQL 查询
EntityGraph entityGraph = entityManager.getEntityGraph("post-entity-graph-with-comment-users");
Post post = entityManager.createQuery("select p from Post p where p.id = :id", Post.class)
.setParameter("id", id)
.setHint("jakarta.persistence.fetchgraph", entityGraph)
.getSingleResult();
使用 Criteria API
EntityGraph entityGraph = entityManager.getEntityGraph("post-entity-graph-with-comment-users");
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Post> criteriaQuery = criteriaBuilder.createQuery(Post.class);
Root<Post> root = criteriaQuery.from(Post.class);
criteriaQuery.where(criteriaBuilder.equal(root.<Long>get("id"), id));
TypedQuery<Post> typedQuery = entityManager.createQuery(criteriaQuery);
typedQuery.setHint("jakarta.persistence.loadgraph", entityGraph);
Post post = typedQuery.getSingleResult();
无论哪种方式,都要通过 setHint()
或 Map
设置 Entity Graph 类型。
7. 总结
本文介绍了 JPA Entity Graph 的基本概念和使用方法,它提供了一种在运行时动态决定加载哪些关联字段的机制,从而有效避免 N+1 查询问题。
建议在设计实体时尽可能使用 FetchType.LAZY
,并在需要批量加载时借助 Entity Graph 来优化性能。
✅ 推荐做法:
- 默认使用懒加载
- 有性能瓶颈时使用 Entity Graph 显式控制加载范围
相关代码可参考 GitHub。