Hibernate 中 @NamedEntityGraph 使用指南
[LOADING...]
1. 概述
JPA 提供了实体图(Entity Graphs),让我们能够在运行时控制实体的获取计划。然而,随着关联层级的加深,定义这些图会变得非常冗长。
Hibernate 7 引入了一个增强的、Hibernate 特有的 @NamedEntityGraph 注解(org.hibernate.annotations.NamedEntityGraph),它允许使用基于文本的图语言来定义实体图。我们不再需要编写嵌套的注解树,而是通过字符串来定义实体图。
在本教程中,我们将探讨 @NamedEntityGraph 注解,并通过一些示例展示其工作原理。
2. 设置
在深入使用之前,我们需要准备好相应的环境。
2.1. Hibernate ORM
要使用该新注解,我们需要 Hibernate ORM 依赖(版本 7.0 或更高):
接下来,我们设置数据模型。
2.2. 数据模型
此处我们使用 JPA 教程中的博客领域模型。
首先是 User 实体:
User 实体作为单表继承体系中的基类。
接下来定义 Author,代表撰写文章的 User:
同样,我们定义 Moderator,代表管理站点的 User:
接着定义 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) 关联:
最后,Comment 实体同时引用了它所属的 Post 和撰写它的 User:
现在,我们准备好实践 @NamedEntityGraph 了。
3. 基于文本的图语言
Hibernate 通过解析逗号分隔的属性列表和子图规范的文本表示来创建实体图。
其语法非常直观,可总结为三点:
- 简单属性以逗号分隔
- 子图包含在括号内
- 支持嵌套子图
我们使用这种语法来更简洁地定义命名图。
4. 理解 @NamedEntityGraph
Hibernate 提供了 @NamedEntityGraph 注解作为 JPA 原生注解的替代方案,它接受一个包含获取计划文本表示的 graph 属性。
4.1. 简单图
要进行获取,我们列出所需的属性,并使用括号表示子图:
与 JPA 的方式相比,这是一种定义实体图的清晰简洁的方法。它会获取 subject、user 和 comments,并且对于每个 comment,也会获取其 user。在底层,Hibernate 会解析图定义,并构建出与 JPA 注解树相同的实体图结构。
4.2. 定义多个图
默认情况下,Hibernate 使用实体名称注册图。但是,我们可以使用 name 属性为图命名,以便与其他图区分开:
此外,我们可以在同一个实体上使用 @NamedEntityGraph 注解定义多个实体图:
基于上述定义,我们现在可以使用 post-basic 来仅获取评论(不包含其用户),而在详情页面可以使用 post-with-comment-users 来加载完整的评论树。
当然,我们也可以使用 [package-info.java](https://www.baeldung.com/java-package-info) 文件在包级别放置该注解。但是,图字符串必须以实体名称作为前缀:
通过这种方式,Hibernate 可以推断出该图属于哪个实体。
4.3. 子类型特定的子图
文本语法还支持在继承体系中针对特定的子类型。Post 引用了一个 User,Hibernate 会在运行时确定具体类型。
为此,我们可以通过在属性前加上子类型名称来针对每个子类型进行设置:
这个图定义告诉 Hibernate:在所有情况下都要获取 User 的 name;如果它是 Author,则获取 bio;如果它是 Moderator,则获取 department。
4.4. Map 键子图
如果属性是 Map 且 Map 的键本身是一个托管实体,我们可以通过在属性名称后添加 .key 来为键定义子图。
例如,如果 Post 实体有一个名为 commentsByUser 的 Map<User, Comment> 属性,我们可以获取用户名以及 Map 条目:
如果不加 .key,子图将应用于 Map 的值类型,在本例中即为 Comment。
5. 使用 GraphParser
Hibernate 还公开了 GraphParser,它支持在运行时创建图。parse() 方法接受一个实体类、图字符串和 EntityManager:
我们还可以使用 parseInto() 方法在运行时丰富现有的图或子图:
这样,我们确保了正确的类型匹配。
6. 合并图
图的不同方面可能在应用程序的多个部分定义。Hibernate 允许我们使用 merge() 方法将多个实体图组合成一个单一的图,即所有图的并集:
合并后的图会将两个图中的所有内容获取到一个查询中;我们也可以使用图字符串方法达到同样的效果。
7. 使用图
一旦定义了实体图(无论是在运行时还是通过注解),就需要将其应用于实际查询。有两种可能的方法:一种是通过标准的 JPA EntityManager(正如我们在 JPA 教程中看到的那样),另一种是使用 Spring Data JPA。
7.1. 使用 EntityManager
标准的 JPA 方法使用提示(Hints),我们将一个 Map 传递进去,其中 key 为 jakarta.persistence.fetchgraph 或 jakarta.persistence.loadgraph,value 为 EntityGraph 对象:
提示 key 决定了行为;使用 fetchgraph 或 loadgraph 决定了加载的内容。
重要的是,我们可以将相同的提示应用于 EntityManager.find() 或 JPQL 查询。
7.2. 使用 Spring Data JPA
关键在于,使用 Spring Data JPA 时,我们可以跳过这些提示。一旦我们在存储库方法上按名称引用了图,@EntityGraph 注解就会在内部处理连接。此外,我们还可以显式设置类型:
否则,Hibernate 默认将这些图视为 fetchgraph。## 8. 测试
让我们探讨一些场景,以验证这些实体图(Entity Graph)在实践中是如何工作的。
8.1. 使用命名图(Named Graph)
首先,我们从一个简单的命名实体图 post-with-comment-users 开始,并通过 fetchgraph 来应用它:
在这里,我们验证了命名实体图是否成功抓取了 Post 及其所有关联对象,包括发表评论的 User。
8.2. 使用 EntityManager.find()
另一方面,我们也可以使用 EntityManager.find() 来应用同一个命名图:
这表明,除了 JPQL 查询外,我们也可以通过 JPA 提示(hints)来应用相同的图。
8.3. 比较多个图
让我们看看定义在实体上的多个命名实体图是如何工作的。
在本例中,我们使用 post-basic 命名实体图并将其与 post-with-comments-user 进行对比:
正如我们所见,Post 及其所有关联加载方式与前一个示例类似;然而,Comments 中的 User 仍然是延迟加载的。
8.4. 子类型特定的子图(Subtype-Specific Subgraphs)
接下来,我们验证如何获取特定于子类型的子图。
具体来说,我们定义了 post-with-typed-user 图:
这段代码应该会抓取所有 User 类型的名称,如果它是 Author 实例,则抓取 bio;如果是 Moderator 实例,则抓取 department。
8.5. 在运行时解析图
Hibernate 还允许我们在运行时构建图。
使用 parse() 方法,我们可以创建一个新的图:
值得注意的是,我们可以使用 parseInto() 方法来丰富现有的图。
8.6. 合并图
最后,我们可以将多个图合并为一个图:
由此,我们得到了所有合并图的属性并集。
9. 结论
在本文中,我们探讨了 Hibernate 的 @NamedEntityGraph 注解以及其背后的基于文本的图语言。
首先,我们从简单的属性列表开始,探讨了嵌套子图和特定于子类型的子图。接下来,我们研究了允许在运行时解析图的 GraphParser,以及合并多个图的 EntityGraph 合并功能。最后,我们对比了 Hibernate 版本相对于 JPA 版本在简洁性和易用性上的优势。
一如既往,代码可以在 GitHub 上找到。