在 JDBC 操作之前,刷新 Hibernate Session (包含 TSE 範例程式碼)

工程 | Alef Arendsen | 2008年01月04日 | ...

在同一個交易中混合使用物件關聯對應器 (Object-Relational Mapper) 的程式碼和不使用物件關聯對應器的程式碼,可能會導致資料在應該可用的時候在底層資料庫中卻不可用的問題。由於這種情況我偶爾會遇到,因此我想如果我寫下這個問題的解決方案,對大家都會有所幫助。

簡而言之:我將在本篇文章的其餘部分介紹的是一個觸發底層持久化機制 (JPA、Hibernate、TopLink) 將任何髒資料傳送到資料庫的面向。

順帶一提,我在去年十二月於The Spring Experience 的其中一場會議中介紹過這個面向,並且這篇文章也包含了那些正在等待原始碼的人的原始碼。

混合使用 ORM 引擎與直接 JDBC 的需求

在許多企業應用程式中,物件關聯對應引擎用於管理(有時複雜的)網域模型的儲存和檢索。我認為我不需要爭論在需要持久化高度互連的網域模型的情況下,ORM 工具可能會提高生產力,更不用說比直接 JDBC 更有效率。

然而,這並不表示可以完全捨棄在應用程式中編寫明確的 SQL。在許多情況下,仍然需要編寫偶爾的 SQL 查詢,以滿足應用程式中的特定需求。我通常看到人們仍然手動編寫 SQL 查詢並在 Java 程式碼中執行它們的幾個原因,例如

  • 測試程式碼:使用 ORM 工具的程式碼仍然需要經過測試。為了絕對確定一段資料存取程式碼(使用 ORM 工具)正在資料庫中正確插入記錄,需要驗證資料庫本身... 使用直接 SQL 查詢。我認為一個非常好的做法是,例如,首先使用 ORM 工具插入一個物件,然後驗證列數是否增加
  • 預存程序:最好使用 JDBC 呼叫而不是透過笨拙的 API 來呼叫預存程序。我真的不想捲入關於預存程序是否的辯論。如果您對此感興趣,只需閱讀其中一些文章。重點是:我經常遇到使用預存程序並希望將執行此操作的程式碼與使用 ORM 引擎的程式碼混合的專案。例如,首先插入幾個新物件,然後需要對新插入的記錄和已有的記錄執行聚合。
  • 涉及大量相似物件的操作。例如,當您需要將一百萬個訂單的取消標誌從true 設為 false 時,可能會發生這種情況。我個人可能不想為此使用 ORM 引擎(有時即使 ORM 引擎有乾淨的 DML 來為我處理髒活)。

混合 ORM 操作與直接 SQL 的問題

在您的應用程式中混合使用 ORM 引擎執行的操作與使用直接 SQL 的操作存在一個大問題。要理解這一點,首先看一下以下虛擬碼(假設資料庫為空)
start transaction

create part with name Bolt
associate with ORM engine (i.e. save using entity manager)

update part set stock = 15 where name='Bolt'

end transaction

這裡的 update 語句將會失敗,儘管我們確實將零件與實體管理器關聯(換句話說:要求實體管理器為我們持久化它)。然而,實體管理器不會因為您將其與實體管理器關聯而立即將記錄插入資料庫。這稱為延遲寫入——幾乎所有 ORM 引擎都實作的東西。實體管理器中的髒狀態(例如我們新建立的零件實例)不會立即發送到資料庫(使用 SQL 語句),而(通常)只會在交易結束時發送。

正如您現在可能已經弄清楚的一般規則,當在某些時間點,您期望資料在資料庫中可用,但實際上並非如此(尚未)時,這種延遲寫入的概念可能會導致嚴重的問題!

以正確的方式解決問題

這個問題有幾種解決方案。一種(非常無知的)解決方案是簡單地說:讓我們稍微修改一下虛擬碼,以包含兩個交易
start transaction
create part with name Bolt
associate with ORM engine (i.e. save using entity manager)
end transaction

start transaction
update part set stock = 15 where name='Bolt'
end transaction

出於顯而易見的原因,這不是正確的解決方案。以這種方式解決問題將導致兩個獨立的交易。如果目的是讓這兩個動作在一個原子操作中執行,那麼情況就不再是這樣了。

這裡正確的解決方案是讓 ORM 引擎在 SQL 查詢執行之前將其變更儲存到資料庫。幸運的是,JPA 以及 Hibernate 例如都提供了執行此操作的方法。強制 ORM 引擎將其變更儲存到資料庫稱為刷新。考慮到這一點,我們可以修改虛擬碼使其運作

start transaction

create part with name Bolt
associate with ORM engine (i.e. save using entity manager)

*** flush

update part set stock = 15 where name='Bolt'

end transaction

在正確的地方解決問題

現在我們已經解決了問題,讓我們將此程式碼放入情境中。我之前使用過 CarPlant 範例來說明某些事情,現在我將再次這樣做。以下循序圖顯示 CarPartsInventory 首先使用 Hibernate Session 插入零件,然後使用 Spring JdbcTemplate(在底層使用直接 JDBC 連線)更新庫存。所有這些都在一個交易中執行。 hib-flush1.png

如果我們將虛擬碼直接翻譯成 Java 程式碼,我們必須新增 flush() 呼叫,這時會出現一個棘手的問題:我們將 flush() 呼叫放在哪裡:我們是否將其作為 addPart() 呼叫的一部分(在我們將零件與 Session 關聯之後立即)還是我們將其作為 updateStock() 呼叫的一部分(在發出 UPDATE 語句之前)。

無論您怎麼看,兩者都是不好的

  • 將其作為 addPart() 呼叫的一部分基本上破壞了延遲寫入的整個概念。在插入零件後寫入,我們立即強制 Hibernate 刷新 session,因此,如果需要在同一個交易中插入多個零件,它就無法再進行最佳化
  • 從先前的論點來看,將其作為 updateStock() 呼叫的一部分會更好,但是如果還有其他需要執行的 SQL 語句,我們是否也需要在那裡新增 flush() 呼叫?
hib-flush2.png

總之,我們有三個需求(新增零件、更新零件和刷新 session)並且只有兩個我們可以新增程式碼來解決需求的地方。這就是面向切面編程 (aspect-orientation) 的用武之地。面向切面編程技術基本上為我們提供了一個額外的地方,我們可以在其中新增程式碼來解決這個需求。換句話說,它允許我們在各自獨立的模組中解決每個需求。

在三個不同的模組中實作三個需求

讓我們在一個獨立的模組中解決每個需求。幸運的是,前兩個需求非常簡單明瞭

插入新零件


private SessionFactory sessionFactory;

public void insertPart(Part p) {
	sessionFactory.getCurrentSession().save(p);
}

使用 Hibernate SessionFactory,我們取得一個 session。session 用於儲存新零件。

更新零件的庫存


private SimpleJdbcTemplate jdbcTemplate;

public void updateStock(Part p, int stock) {
	jdbcTemplate.update("update stock set stock = stock + ? where number=?", 
		stock, p.getNumber());
}

同步 session 作為一般規則,我們可以說每當 JDBC 操作即將發生時,如果 session 是髒的,則首先刷新 session。我們可以將其改寫為在呼叫 JDBC 操作之前,如果 Hibernate session 是髒的,則刷新它。這個短語中有兩個重要元素。最後一部分指定了我們想要做什麼。第一部分回答了我們想要在哪裡何時執行刷新行為的問題。

  • 何時:之前
  • 哪裡:呼叫 JDBC 操作
  • 做什麼:刷新髒的 Hibernate session

如果有人了解 AspectJ 語言,則很容易將其翻譯成 AspectJ。即使您不想使用 AspectJ,也可以透過使用 Spring AOP 來實現這種行為。


public aspect HibernateStateSynchronizer {

	private SessionFactory sessionFactory;
	
	public void setSessionFactory(SessionFactory sessionFactory() {
		this.sessionFactory = sessionFactory;
	}

	pointcut jdbcOperation() : 
		call(* org.springframework.jdbc.core.simple.SimpleJdbcTemplate.*(..));
		
	before() jdbcOperation() {
		Session session = sessionFactory.getCurrentSession();
		if (session.isDirty()) {
			session.flush();
		}
	}
}

這個面向將實作所需的行為;每當 JDBC 操作即將發生時,它都會刷新 Hibernate session。

變化

在審查這個面向時,需要記住一些事項。

首先,您希望應用此行為的地方可能會有所不同。上面的範例將此行為應用於 SimpleJdbcTemplate 上方法的全部呼叫。這對您的口味來說可能太過了。可以輕鬆修改切入點,以將此行為應用於由特定註釋註釋的方法(想想:execution(@JdbcOperation *(..)))。

其次,您可能會想知道如果沒有可用的 Hibernate Session 會發生什麼情況。SessionFactory.getCurrentSession() 始終在 Spring 管理的環境中建立新的 Session。如果您希望即使根本沒有 SessionFactory,或者尚未建立 Session(並且您不希望建立 Session)時,此面向也能運作,則應修改此面向以使用 Spring 中的 SessionFactoryUtils 類別。此類別具有允許您請求 Session,如果沒有可用的 Session,則不會返回 Session 的方法。

原始碼

此條目的隨附原始碼使用 AspectJ 實作了 HibernateStateSynchronizer 面向。但是,修改此面向以使其與 Spring AOP 一起運作非常簡單明瞭。

HibernateCarPartsInventoryTests 測試案例說明了此行為。當啟用此面向時,testAddPart() 方法會成功。當停用此面向時(例如,透過將其從建置路徑中排除,或透過註解 before() advice),測試將會失敗,因為計數語句每次執行時的記錄數都相同(換句話說,在查詢執行時,零件不存在於資料庫中)。

在目前的設定中,before advice 已被註解掉,因此測試將會失敗。請注意,此專案的 pom.xml 檔案包含 Maven AspectJ 外掛程式。可能會有一些關於版本衝突的警告(由外掛程式使用與專案本身不同的 AspectJ 版本引起),但儘管存在這些警告,它仍然應該可以運作。

原始碼:carplant.zip

取得 Spring 電子報

隨時掌握 Spring 電子報的最新資訊

訂閱

領先一步

VMware 提供訓練和認證,以加速您的進展。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

了解更多

即將到來的活動

查看 Spring 社群中所有即將到來的活動。

查看全部