1. 概述

JPA 2.1 引入了 Entity Graph 特性,作为一种更灵活、更高效的方式来处理实体关联数据的加载问题。

它允许我们通过定义一个“图”模板,指定哪些关联字段需要被一起加载,并且可以在运行时动态选择使用哪种图(Graph)策略。

在本文中,我们将深入讲解如何创建和使用 Entity Graph。

2. Entity Graph 解决了什么问题?

在 JPA 2.0 及之前版本中,我们通常使用 FetchType.LAZYFetchType.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


原始标题:JPA Entity Graph