Hibernate 中 @NamedEntityGraph 指南
1. 概述
JPA 提供實體圖,使我們能夠在運行時控制實體獲取計劃。然而,隨著關聯層次結構的加深,定義這些圖表很快就會變得冗長繁瑣。
Hibernate 7 引入了一個增強的 Hibernate 特有的@NamedEntityGraph註解 ( org.hibernate.annotations.NamedEntityGraph ),它允許使用基於文字的圖語言定義實體圖。我們不再使用嵌套的註解樹,而是使用字串來定義實體圖。
在本教學中,我們將探討@NamedEntityGraph註解,並舉例說明它的運作方式。
2. 設定
在深入了解使用方法之前,讓我們先準備好合適的環境。
2.1. Hibernate ORM
要使用此新註解,我們需要Hibernate ORM依賴項(版本 7.0 或更高版本):
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>7.3.1.Final</version>
<scope>compile</scope>
</dependency>
接下來,我們建立資料模型。
2.2 資料模型
這裡,我們使用 JPA 教程中的部落格域模型。
我們先從User實體開始:
@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;
// standard getters and setters
}
User實體在單表繼承層次結構中充當基底類別。
接下來,我們定義一個Author ,它代表撰寫貼文的User :
@Entity
public class Author extends User {
private String bio;
// standard getters and setters
}
類似地,我們定義了Moderator ,它代表管理網站的User :
@Entity
public class Moderator extends User {
private String department;
// standard getters and setters
}
接下來,我們定義Post實體,它與User具有@ManyToOne關聯,與Comment具有@OneToMany關聯:
@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;
// standard getters and setters
}
最後, Comment實體會同時引用它所屬的Post和撰寫該留言的User :
@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;
// standard getters and setters
}
現在,我們已經準備好在實踐中探索@NamedEntityGraph 。
3. 以文字為主的圖語言
Hibernate 能夠透過解析以逗號分隔的屬性清單和子圖規範的文字表示形式來建立實體圖。
文法相當簡單明了,可以歸納為三點:
- 簡單屬性之間用逗號分隔
- 子圖用括號括起來。
- 支援嵌套子圖
在這裡,我們使用這種語法來更簡潔地定義命名圖。
4. 理解@NamedEntityGraph
Hibernate 提供了@NamedEntityGraph註解作為 JPA 註解的替代方案,並接受一個包含取得計劃文字表示形式的 graph 屬性。
4.1 簡單圖
為了取得數據,我們用括號列出所需的屬性(用於表示子圖):
@NamedEntityGraph(graph = "subject, user, comments(user)")
@Entity
public class Post {
// ....
}
與 JPA 方法相比,這種方法能夠更清晰簡潔地定義實體圖,它能夠獲取主題、使用者和評論,並且對於每個評論,還能獲取其對應的使用者。在底層, Hibernate 會解析圖定義,並建構與 JPA 註解樹相同的實體圖結構。
4.2 定義多個圖
Hibernate 預設使用實體名稱註冊圖。但是,我們也可以使用name屬性為圖命名,以便將其與其他圖區分開來:
@NamedEntityGraph(name = "post-with-comment-users", graph = "subject, user, comments(user)")
@Entity
public class Post {
// ...
}
此外,我們可以使用@NamedEntityGraph來註解在同一個實體上定義多個實體圖:
@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檔案在套件層級加入註解。但是,圖字串必須以實體名稱為前綴:
@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 會在運行時確定具體類型。
為此,我們可以透過在屬性前加上子類型名稱來針對每個子類型進行定位:
@NamedEntityGraph(name = "post-with-typed-user", graph = "subject, user(name), user(Author: bio), user(Moderator: department)")
@Entity
public class Post {
// ...
}
此圖定義告訴 Hibernate,在所有情況下都要獲取User名,如果是Author ,則獲取其個人簡介;如果是Moderator ,則獲取其部門。
4.4. 地圖關鍵子圖
如果屬性是一個Map ,而該 Map 的鍵本身就是一個託管實體,我們可以透過在屬性名稱後面附加.key來為該鍵定義一個子圖。
例如,如果 Post 實體有一個名為commentsByUser Map<User, Comment>屬性,我們可以取得使用者名稱以及映射條目:
@org.hibernate.annotations.NamedEntityGraph(graph = "commentsByUser.key(name)")
如果沒有.key ,子圖將套用於映射值類型,在本例中為Comment 。
5. 使用 GraphParser
Hibernate 也公開了GraphParser ,它支援在運行時建立圖。 parse parse()方法接受一個實體類別、圖字串和EntityManager :
final EntityGraph<Post> graph = GraphParser
.parse(Post.class, "subject, user, comments(user)", entityManager);
我們也可以使用parseInto()方法在運行時豐富現有的圖或子圖:
EntityGraph<Post> graph = GraphParser.createEntityGraph(Post.class);
GraphParser.parseInto(graph, "subject, user", entityManger);
GraphParser.parseInto(graph, "comments(user)", entityManger);
因此,我們能夠確保正確輸入。
6. 合併圖表
圖的不同方面可以在應用程式的多個部分進行定義。 Hibernate允許我們使用merge()方法將多個實體圖合併成一個圖,該圖是所有圖的並集:
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 方法使用提示,其中我們傳遞一個Map ,以jakarta.persistence.fetchgraph或jakarta.persistence.loadgraph作為鍵, EntityGraph物件作為值:
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);
提示鍵決定行為;使用fetchgraph或loadgraph決定要載入什麼。
重要的是,我們可以將相同的提示套用至EntityManager.find()或 JPQL 查詢。
7.2. 使用 Spring Data JPA
關鍵在於,在使用 Spring Data JPA 時,我們可以跳過一些提示。只要我們在倉庫方法中透過名稱引用圖, @EntityGraph註解就會在內部處理連線。此外,我們還可以明確地設定類型:
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. 測試
讓我們透過幾個場景來驗證這些實體圖在實踐中是如何運作的。
8.1 使用命名圖
讓我們從一個簡單的命名實體圖post-with-comment-users開始,並使用fetchgraph應用它:
@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 find()
另一方面,我們也可以使用EntityManager.find()來套用相同名稱的圖:
@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 提示來應用。
8.3. 比較多個圖表
讓我們來看看在實體上定義的多個命名實體圖是如何運作的。
在這種情況下,我們使用post-basic命名實體圖,並將其與post-with-comments-user實體圖進行比較:
@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. 子類型特定子圖
接下來,我們驗證如何取得特定於子類型的子圖。
具體來說,我們定義了post-with-typed-user圖:
@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取得其個人簡介,如果是Moderator則取得所屬部門。
8.5. 運行時解析圖
Hibernate 還允許我們在運行時建立圖。
使用parse()方法時,我們可以建立一個新圖:
@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();
assertNotNull(parsedPost);
assertEquals("First Post", parsedPost.getSubject());
assertTrue(Hibernate.isInitialized(parsedPost.getUser()));
assertTrue(Hibernate.isInitialized(parsedPost.getComments()));
assertEquals(2, parsedPost.getComments().size());
assertTrue(Hibernate.isInitialized(parsedPost.getComments().get(0).getUser()));
assertTrue(Hibernate.isInitialized(parsedPost.getComments().get(1).getUser()));
assertNotNull(parsedIntoPost);
assertEquals("First Post", parsedIntoPost.getSubject());
assertTrue(Hibernate.isInitialized(parsedIntoPost.getUser()));
assertTrue(Hibernate.isInitialized(parsedIntoPost.getComments()));
assertEquals(2, parsedIntoPost.getComments().size());
assertTrue(Hibernate.isInitialized(parsedIntoPost.getComments().get(0).getUser()));
assertTrue(Hibernate.isInitialized(parsedIntoPost.getComments().get(1).getUser()));
}
值得注意的是,我們可以使用parseInto()方法來豐富現有的圖。
8.6. 合併圖
最後,我們可以將多個圖合併成一個圖:
@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 上找到。