Ohhnews

分类导航

$ cd ..
Baeldung原文

Hibernate 中 @NamedEntityGraph 使用指南

#hibernate#java#实体图#数据持久化#性能优化

[LOADING...]

1. 概述

JPA 提供了实体图(Entity Graphs),让我们能够在运行时控制实体的获取计划。然而,随着关联层级的加深,定义这些图会变得非常冗长。

Hibernate 7 引入了一个增强的、Hibernate 特有的 @NamedEntityGraph 注解(org.hibernate.annotations.NamedEntityGraph),它允许使用基于文本的图语言来定义实体图。我们不再需要编写嵌套的注解树,而是通过字符串来定义实体图。

在本教程中,我们将探讨 @NamedEntityGraph 注解,并通过一些示例展示其工作原理。

2. 设置

在深入使用之前,我们需要准备好相应的环境。

2.1. Hibernate ORM

要使用该新注解,我们需要 Hibernate ORM 依赖(版本 7.0 或更高):

$ xml
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>7.3.1.Final</version>
    <scope>compile</scope>
</dependency>

接下来,我们设置数据模型。

2.2. 数据模型

此处我们使用 JPA 教程中的博客领域模型。

首先是 User 实体:

$ java
@Entity
@Table(name = "app_user")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
    // 标准 getter 和 setter
}

User 实体作为单表继承体系中的基类。

接下来定义 Author,代表撰写文章的 User

$ java
@Entity
public class Author extends User {
    private String bio;
    // 标准 getter 和 setter
}

同样,我们定义 Moderator,代表管理站点的 User

$ java
@Entity
public class Moderator extends User {
    private String department;
    // 标准 getter 和 setter
}

接着定义 Post 实体,它与 User 存在 [@ManyToOne](https://www.baeldung.com/jpa-hibernate-associations#2-many-to-one-relationship) 关联,与 Comment 存在 [@OneToMany](https://www.baeldung.com/jpa-hibernate-associations#1-one-to-many-relationship) 关联:

$ java
@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;
    // 标准 getter 和 setter
}

最后,Comment 实体同时引用了它所属的 Post 和撰写它的 User

$ java
@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;
    // 标准 getter 和 setter
}

现在,我们准备好实践 @NamedEntityGraph 了。

3. 基于文本的图语言

Hibernate 通过解析逗号分隔的属性列表和子图规范的文本表示来创建实体图。

其语法非常直观,可总结为三点:

  • 简单属性以逗号分隔
  • 子图包含在括号内
  • 支持嵌套子图

我们使用这种语法来更简洁地定义命名图。

4. 理解 @NamedEntityGraph

Hibernate 提供了 @NamedEntityGraph 注解作为 JPA 原生注解的替代方案,它接受一个包含获取计划文本表示的 graph 属性。

4.1. 简单图

要进行获取,我们列出所需的属性,并使用括号表示子图:

$ java
@NamedEntityGraph(graph = "subject, user, comments(user)")
@Entity
public class Post {
    // ....
}

与 JPA 的方式相比,这是一种定义实体图的清晰简洁的方法。它会获取 subjectusercomments,并且对于每个 comment,也会获取其 user。在底层,Hibernate 会解析图定义,并构建出与 JPA 注解树相同的实体图结构

4.2. 定义多个图

默认情况下,Hibernate 使用实体名称注册图。但是,我们可以使用 name 属性为图命名,以便与其他图区分开

$ java
@NamedEntityGraph(name = "post-with-comment-users", graph = "subject, user, comments(user)")
@Entity
public class Post {
    // ...
}

此外,我们可以在同一个实体上使用 @NamedEntityGraph 注解定义多个实体图:

$ java
@NamedEntityGraph(name = "post-basic", graph = "subject, user, comments")
@NamedEntityGraph(name = "post-with-comment-users", graph = "subject, user, comments(user)")
@NamedEntityGraph(name = "post-with-typed-users", graph = "subject, user(name), user(Author: bio), user(Moderator: department)")
@Entity
public class Post {
    // ...
}

基于上述定义,我们现在可以使用 post-basic 来仅获取评论(不包含其用户),而在详情页面可以使用 post-with-comment-users 来加载完整的评论树。

当然,我们也可以使用 [package-info.java](https://www.baeldung.com/java-package-info) 文件在包级别放置该注解。但是,图字符串必须以实体名称作为前缀

$ java
@org.hibernate.annotations.NamedEntityGraph(name = "package-post-with-comment-users", graph = "Post: subject, user, comments(user)")
package com.baeldung.hibernate.entitygraph.model;

通过这种方式,Hibernate 可以推断出该图属于哪个实体。

4.3. 子类型特定的子图

文本语法还支持在继承体系中针对特定的子类型。Post 引用了一个 User,Hibernate 会在运行时确定具体类型。

为此,我们可以通过在属性前加上子类型名称来针对每个子类型进行设置:

$ java
@NamedEntityGraph(name = "post-with-typed-user", graph = "subject, user(name), user(Author: bio), user(Moderator: department)")
@Entity
public class Post {
    // ...
}

这个图定义告诉 Hibernate:在所有情况下都要获取 Username;如果它是 Author,则获取 bio;如果它是 Moderator,则获取 department

4.4. Map 键子图

如果属性是 Map 且 Map 的键本身是一个托管实体,我们可以通过在属性名称后添加 .key 来为键定义子图

例如,如果 Post 实体有一个名为 commentsByUserMap<User, Comment> 属性,我们可以获取用户名以及 Map 条目:

$ java
@org.hibernate.annotations.NamedEntityGraph(graph = "commentsByUser.key(name)")

如果不加 .key,子图将应用于 Map 的值类型,在本例中即为 Comment

5. 使用 GraphParser

Hibernate 还公开了 GraphParser,它支持在运行时创建图。parse() 方法接受一个实体类、图字符串和 EntityManager

$ java
final EntityGraph<Post> graph = GraphParser
    .parse(Post.class, "subject, user, comments(user)", entityManager);

我们还可以使用 parseInto() 方法在运行时丰富现有的图或子图:

$ java
EntityGraph<Post> graph = GraphParser.createEntityGraph(Post.class);
GraphParser.parseInto(graph, "subject, user", entityManager);
GraphParser.parseInto(graph, "comments(user)", entityManager);

这样,我们确保了正确的类型匹配。

6. 合并图

图的不同方面可能在应用程序的多个部分定义。Hibernate 允许我们使用 merge() 方法将多个实体图组合成一个单一的图,即所有图的并集

$ java
EntityGraph<Post> mergedGraph = EntityGraphs.merge(entityManager, Post.class,
    entityManager.getEntityGraph("post-basic"),
    entityManager.getEntityGraph("post-with-comment-users"));

合并后的图会将两个图中的所有内容获取到一个查询中;我们也可以使用图字符串方法达到同样的效果。

7. 使用图

一旦定义了实体图(无论是在运行时还是通过注解),就需要将其应用于实际查询。有两种可能的方法:一种是通过标准的 JPA EntityManager(正如我们在 JPA 教程中看到的那样),另一种是使用 Spring Data JPA。

7.1. 使用 EntityManager

标准的 JPA 方法使用提示(Hints),我们将一个 Map 传递进去,其中 key 为 jakarta.persistence.fetchgraphjakarta.persistence.loadgraph,value 为 EntityGraph 对象:

$ java
EntityGraph<Post> graph = entityManager.getEntityGraph("post-with-comment-users");
        
Map<String, Object> hints = Map.of("jakarta.persistence.fetchgraph", graph);
        
Post post = entityManager.find(Post.class, 1L, hints);

提示 key 决定了行为;使用 fetchgraphloadgraph 决定了加载的内容

重要的是,我们可以将相同的提示应用于 EntityManager.find() 或 JPQL 查询。

7.2. 使用 Spring Data JPA

关键在于,使用 Spring Data JPA 时,我们可以跳过这些提示。一旦我们在存储库方法上按名称引用了图,@EntityGraph 注解就会在内部处理连接。此外,我们还可以显式设置类型:

$ java
public interface PostRepository extends JpaRepository<Post, Long> {
    @EntityGraph(value = "post-basic", type = EntityGraph.EntityGraphType.LOAD)
    List<Post> findAll(String subject);
    
    @EntityGraph(value = "post-with-comment-users")
    Post findBySubject(String subject);
}

否则,Hibernate 默认将这些图视为 fetchgraph。## 8. 测试 让我们探讨一些场景,以验证这些实体图(Entity Graph)在实践中是如何工作的。

8.1. 使用命名图(Named Graph)

首先,我们从一个简单的命名实体图 post-with-comment-users 开始,并通过 fetchgraph 来应用它:

$ java
@Test
void whenFindWithFetchGraph_thenAssociationsAreLoaded() {
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    EntityGraph<Post> graph = (EntityGraph<Post>) entityManager.getEntityGraph("post-with-comment-users");
    Post post = entityManager.createQuery("Select p from Post p where p.subject = :subject", Post.class)
        .setParameter("subject", "First Post")
        .setHint("jakarta.persistence.fetchgraph", graph)
        .getSingleResult();
    entityManager.close();
    assertNotNull(post);
    assertEquals("First Post", post.getSubject());
    assertTrue(Hibernate.isInitialized(post.getUser()));
    assertTrue(Hibernate.isInitialized(post.getComments()));
    assertEquals(2, post.getComments().size());
    assertTrue(Hibernate.isInitialized(post.getComments().get(0).getUser()));
    assertTrue(Hibernate.isInitialized(post.getComments().get(1).getUser()));
}

在这里,我们验证了命名实体图是否成功抓取了 Post 及其所有关联对象,包括发表评论的 User

8.2. 使用 EntityManager.find()

另一方面,我们也可以使用 EntityManager.find() 来应用同一个命名图:

$ java
@Test
void whenFindingByIdWithEntityManagerHints_thenAssociationsAreLoaded() {
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    Long postId = entityManager.createQuery("Select p.id from Post p where p.subject = :subject", Long.class)
        .setParameter("subject", "First Post")
        .getSingleResult();
    EntityGraph<Post> graph = (EntityGraph<Post>) entityManager.getEntityGraph("post-with-comment-users");
    Post post = entityManager.find(Post.class, postId, Map.of("jakarta.persistence.fetchgraph", graph));
    entityManager.close();
    assertNotNull(post);
    assertEquals("First Post", post.getSubject());
    assertTrue(Hibernate.isInitialized(post.getUser()));
    assertTrue(Hibernate.isInitialized(post.getComments()));
    assertEquals(2, post.getComments().size());
    assertTrue(Hibernate.isInitialized(post.getComments().get(0).getUser()));
    assertTrue(Hibernate.isInitialized(post.getComments().get(1).getUser()));
}

这表明,除了 JPQL 查询外,我们也可以通过 JPA 提示(hints)来应用相同的图。

8.3. 比较多个图

让我们看看定义在实体上的多个命名实体图是如何工作的。

在本例中,我们使用 post-basic 命名实体图并将其与 post-with-comments-user 进行对比

$ java
@Test
void whenUsingPostBasicGraph_thenCommentUsersRemainLazy() {
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    EntityGraph<Post> graph = (EntityGraph<Post>) entityManager.getEntityGraph("post-basic");
    Post post = entityManager.createQuery("Select p from Post p where p.subject = :subject", Post.class)
        .setParameter("subject", "First Post")
        .setHint("jakarta.persistence.fetchgraph", graph)
        .getSingleResult();
    entityManager.close();
    assertNotNull(post);
    assertEquals("First Post", post.getSubject());
    assertTrue(Hibernate.isInitialized(post.getUser()));
    assertTrue(Hibernate.isInitialized(post.getComments()));
    assertFalse(Hibernate.isInitialized(post.getComments().get(0).getUser()));
}

正如我们所见,Post 及其所有关联加载方式与前一个示例类似;然而,Comments 中的 User 仍然是延迟加载的。

8.4. 子类型特定的子图(Subtype-Specific Subgraphs)

接下来,我们验证如何获取特定于子类型的子图。

具体来说,我们定义了 post-with-typed-user 图:

$ java
@Test
void whenUsingTypedUserGraph_thenSubtypeAttributesAreLoaded() {
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    EntityGraph<Post> graph = (EntityGraph<Post>) entityManager.getEntityGraph("post-with-typed-user");
    Post authorPost = entityManager.createQuery("Select p from Post p where p.subject = :subject", Post.class)
        .setParameter("subject", "First Post")
        .setHint("jakarta.persistence.fetchgraph", graph)
        .getSingleResult();
    Post moderatorPost = entityManager.createQuery("Select p from Post p where p.subject = :subject", Post.class)
        .setParameter("subject", "Second Post")
        .setHint("jakarta.persistence.fetchgraph", graph)
        .getSingleResult();
    entityManager.close();
    assertNotNull(authorPost);
    assertTrue(Hibernate.isInitialized(authorPost.getUser()));
    assertEquals("Author 1", authorPost.getUser().getName());
    assertInstanceOf(Author.class, authorPost.getUser());
    assertTrue(Hibernate.isPropertyInitialized(authorPost.getUser(), "bio"));
    assertEquals("A Baeldung Author", ((Author) authorPost.getUser()).getBio());
    assertNotNull(moderatorPost);
    assertTrue(Hibernate.isInitialized(moderatorPost.getUser()));
    assertEquals("Moderator 1", moderatorPost.getUser().getName());
    assertInstanceOf(Moderator.class, moderatorPost.getUser());
    assertTrue(Hibernate.isPropertyInitialized(moderatorPost.getUser(), "department"));
    assertEquals("A Baeldung Moderator", ((Moderator) moderatorPost.getUser()).getDepartment());
}

这段代码应该会抓取所有 User 类型的名称,如果它是 Author 实例,则抓取 bio;如果是 Moderator 实例,则抓取 department。

8.5. 在运行时解析图

Hibernate 还允许我们在运行时构建图。

使用 parse() 方法,我们可以创建一个新的图

$ java
@Test
void whenParsingGraphsAtRuntime_thenAssociationsAreLoaded() {
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    EntityGraph<Post> parsedGraph = GraphParser.parse(Post.class, "subject, user, comments(user)", entityManager);
    EntityGraph<Post> parsedIntoGraph = entityManager.createEntityGraph(Post.class);
    GraphParser.parseInto(parsedIntoGraph, "subject, user", entityManager);
    GraphParser.parseInto(parsedIntoGraph, "comments(user)", entityManager);
    Post parsedPost = entityManager.createQuery("Select p from Post p where p.subject = :subject", Post.class)
        .setParameter("subject", "First Post")
        .setHint("jakarta.persistence.fetchgraph", parsedGraph)
        .getSingleResult();
    Post parsedIntoPost = entityManager.createQuery("Select p from Post p where p.subject = :subject", Post.class)
        .setParameter("subject", "First Post")
        .setHint("jakarta.persistence.fetchgraph", parsedIntoGraph)
        .getSingleResult();
    entityManager.close();
    // ... 断言逻辑与前述示例一致
}

值得注意的是,我们可以使用 parseInto() 方法来丰富现有的图

8.6. 合并图

最后,我们可以将多个图合并为一个图:

$ java
@Test
void whenMergingGraphs_thenUnionOfAttributesIsLoaded() {
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    EntityGraph<Post> basicGraph = (EntityGraph<Post>) entityManager.getEntityGraph("post-basic");
    EntityGraph<Post> postWithCommentUsersGraph = (EntityGraph<Post>) entityManager.getEntityGraph("post-with-comment-users");
    EntityGraph<Post> mergedGraph = EntityGraphs.merge(entityManager, Post.class,
        basicGraph,
        postWithCommentUsersGraph);
    Post post = entityManager.createQuery("Select p from Post p where p.subject = :subject", Post.class)
        .setParameter("subject", "First Post")
        .setHint("jakarta.persistence.fetchgraph", mergedGraph)
        .getSingleResult();
    entityManager.close();
    assertNotNull(post);
    assertEquals("First Post", post.getSubject());
    assertTrue(Hibernate.isInitialized(post.getUser()));
    assertTrue(Hibernate.isInitialized(post.getComments()));
    assertEquals(2, post.getComments().size());
    assertTrue(Hibernate.isInitialized(post.getComments().get(0).getUser()));
    assertTrue(Hibernate.isInitialized(post.getComments().get(1).getUser()));
}

由此,我们得到了所有合并图的属性并集。

9. 结论

在本文中,我们探讨了 Hibernate 的 @NamedEntityGraph 注解以及其背后的基于文本的图语言。

首先,我们从简单的属性列表开始,探讨了嵌套子图和特定于子类型的子图。接下来,我们研究了允许在运行时解析图的 GraphParser,以及合并多个图的 EntityGraph 合并功能。最后,我们对比了 Hibernate 版本相对于 JPA 版本在简洁性和易用性上的优势。

一如既往,代码可以在 GitHub 上找到。