在 Hibernate 中使用 StatelessSession
1.概述
在使用 Hibernate 建立持久層時,我們經常使用Session
API 對領域實體執行 CRUD 操作。它自動為我們提供了諸如快取、髒檢查、延遲載入、級聯等功能。
雖然這些功能使大多數常見場景的開發變得更容易,但它們會帶來開銷,從而影響處理大型資料集或執行大量操作時的效能。
對於這些情況,Hibernate 提供了輕量級的StatelessSession
API 作為替代方案,它剝離了常規Session
的許多自動、有狀態的功能。
在本教程中,我們將探討如何使用 Hibernate 的StatelessSession
API 執行資料庫操作,並實際了解它與標準有狀態Session
介面有何不同。
2. 設定項目
在我們探討 Hibernate 中的無狀態會話概念之前,讓我們先建立一個將在本教程中使用的簡單應用程式。
2.1. 依賴項
讓我們先將Hibernate 依賴項新增到專案的pom.xml
檔案中:
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>7.1.0.Final</version>
</dependency>
這種依賴關係為我們提供了核心的 Hibernate ORM 功能,包括我們在本教程中討論的StatelessSession
類別。
或者,如果我們正在建立 Spring Boot 應用程序,我們可以使用Spring Data JPA starter ,它已經包含此依賴項。
2.2. 定義域實體
接下來,讓我們定義我們的領域實體,它由Author
和Article
實體組成。
首先,我們將建立一個可以有多篇articles
的Author
實體:
@Entity
@Table(name = "authors")
class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Article> articles;
// standard getters and setters
}
在這裡,我們為Author
定義一個標準實體。
它與Article
實體具有一對多關係,我們接下來將定義它:
@Entity
@Table(name = "articles")
class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id", nullable = false)
private Author author;
// standard getters and setters
}
我們在Article
實體中定義了與author
的多對一關係。這樣就完成了設置,在兩個實體之間建立了經典的父子關聯。
請注意,我們已在域實體中明確啟用了級聯,並將獲取類型設為LAZY
。在接下來的部分中,我們將看到這些設定在StatelessSession
中如何運作。
3.執行寫入操作
有了域實體之後,讓我們先來探索StatelessSession
如何處理寫入操作。
正如我們所討論的, StatelessSession
不支援自動級聯操作。讓我們看看當我們嘗試保存一篇Article
而不首先持久化其關聯的Author
時會發生什麼:
`SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
try (StatelessSession statelessSession = sessionFactory.openStatelessSession()) {
Author author = new Author();
author.setName(RandomString.make());
Article article = new Article();
article.setTitle(RandomString.make());
article.setAuthor(author);
Exception exception = assertThrows(TransientObjectException.class, () -> {
statelessSession.insert(article);
});
assertThat(exception.getMessage())
.contains("object references an unsaved transient instance - save the transient instance before flushing");
}`
在這裡,我們首先檢索StatelessSession
的實例,就像檢索標準Session
實例一樣。
然後,我們建立兩個實體並將它們關聯起來。然而,當我們只對article
物件呼叫insert()
時,Hibernate 就拋出了TransientObjectException
。
發生這種情況是因為關聯的author
物件尚未保存,並且StatelessSession
不會自動級聯插入操作,即使我們已經在實體映射中明確啟用了級聯。
為了解決這個問題,我們必須在插入article
之前明確呼叫author
物件上的insert()
。
此外, StatelessSession
不執行自動髒檢查。這意味著,除非我們明確告知 Hibernate 進行更新,否則在持久化實體後對其進行的任何更改都將被忽略。讓我們來分析一下這種行為:
Long authorId;
String originalName = RandomString.make();
try (StatelessSession statelessSession = sessionFactory.openStatelessSession()) {
statelessSession.getTransaction().begin();
Author author = new Author();
author.setName(originalName);
statelessSession.insert(author);
author.setName(RandomString.make());
statelessSession.getTransaction().commit();
authorId = author.getId();
}
try (StatelessSession statelessSession = sessionFactory.openStatelessSession()) {
Author author = statelessSession.get(Author.class, authorId);
assertThat(author.getName())
.isEqualTo(originalName);
}
這裡,我們插入一個帶有首字母的author
,然後立即更改它。在常規Session
中,Hibernate 會自動偵測到此變更並更新資料庫。
然而,使用StatelessSession
時,名稱變更會被忽略。資料庫仍然包含原始名稱,我們透過再次獲取author
資訊來驗證這一點。
為了保持這樣的變化,我們需要使用author
物件明確呼叫update()
方法。
4.執行讀取操作
接下來我們來探究StatelessSession
如何處理讀取操作。
主要區別在於缺乏對延遲加載的支援。即使我們已將關聯標記為FetchType.LAZY
,在初始載入後嘗試存取它仍會失敗:
Author author = statelessSession.get(Author.class, authorId);
assertThat(author)
.hasNoNullFieldsOrProperties();
assertThrows(LazyInitializationException.class, () -> {
author.getArticles().size();
});
這裡,我們成功使用get()
方法取得了Author
。然而,當我們嘗試存取關聯的articles
時,卻拋出了LazyInitializationException
異常。
發生這種情況是因為**StatelessSession
沒有附加持久上下文,因此無法傳回資料庫來初始化集合**。
為了正確載入實體及其關聯,我們必須在同一個查詢中急切地取得它們。
讓我們試著使用 JOIN FETCH 子句來做到這一點:
Author author = statelessSession
.createQuery(
"SELECT a FROM Author a LEFT JOIN FETCH a.articles WHERE a.id = :id",
Author.class)
.setParameter("id", authorId)
.getSingleResult();
assertThat(author)
.hasNoNullFieldsOrProperties();
assertThat(author.getArticles())
.isNotNull()
.isNotEmpty();
上面我們使用createQuery()
方法一次檢索了Author
及其articles
。然後,我們驗證了articles
集合是否可以正確存取。
或者,我們可以使用實體圖來實現相同的預先載入行為:
EntityGraph<Author> authorWithArticlesGraph = statelessSession.createEntityGraph(Author.class);
authorWithArticlesGraph.addAttributeNodes("articles");
Author author = statelessSession
.createQuery("SELECT a FROM Author a WHERE a.id = :id", Author.class)
.setParameter("id", authorId)
.setHint("jakarta.persistence.fetchgraph", authorWithArticlesGraph)
.getSingleResult();
這裡,我們為Author
實體建立一個EntityGraph
,並將articles
屬性配置為即時載入。然後,我們使用jakarta.persistence.fetchgraph
提示將其套用到查詢中。
最後,讓我們來看看StatelessSession
的另一個重要方面,那就是沒有一級快取:
Author author1 = statelessSession.get(Author.class, authorId);
Author author2 = statelessSession.get(Author.class, authorId);
assertThat(author1)
.isNotSameAs(author2);
當我們在同一個StatelessSession
實例中兩次取得同一個實體時,我們會得到兩個不同的物件。
這與常規Session
不同,常規 Session 從其一級快取中傳回相同的物件實例。但是,如果沒有此緩存,則會查詢資料庫兩次,從而導致創建一個新對象,即使它代表的是同一資料庫記錄。
我們在使用StatelessSession
時應該記住這種行為,並在程式碼中謹慎管理實體實例。
5. 結論
在本文中,我們探討如何使用 Hibernate 的StatelessSession
API 執行資料庫操作。
我們設定了我們的域實體,並實際觀察了它的行為與標準Session
API 有何不同。
雖然StatelessSession
需要對資料庫操作進行更明確的控制,但當我們處理大型資料集並且不需要 Hibernate 的自動功能時,它是一個很好的選擇。
與往常一樣,本文中使用的所有程式碼範例均可在 GitHub 上找到。