在 JDBC 操作之前,重新整理 Hibernate Session(包括 TSE 示例程式碼)

工程 | Alef Arendsen | 2008 年 1 月 4 日 | ...

在同一個事務中混合使用物件關係對映器(Object-Relational Mapper)的程式碼和不使用它的程式碼,可能會導致資料在底層資料庫中未能及時可用的問題。由於這種情況我時常遇到,我認為如果我寫下解決此問題的方法,對大家都會有所幫助。

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

在上個十二月,我參加 The Spring Experience 的其中一次會議時,順便介紹了這個方面的內容,這篇文章也包含了那些一直等待它的原始碼。

混合使用 ORM 引擎和純 JDBC 的必要性

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

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

  • 測試程式碼:使用 ORM 工具的程式碼仍然需要測試。為了絕對確定一段資料訪問程式碼(使用 ORM 工具)正確地將記錄插入到資料庫中,需要驗證資料庫本身……使用純 SQL 查詢。我個人認為,例如,首先使用 ORM 工具插入一個物件,然後驗證行數是否增加是一個非常好的實踐。
  • 儲存過程:最好使用 JDBC 呼叫而不是笨重的 API 呼叫儲存過程。我真的不想捲入關於儲存過程是否好的 爭論 (whether) (stored) (procedures)。如果你對此感興趣,只需閱讀其中一些文章。情況是:我經常遇到使用儲存過程的專案,並且希望將使用儲存過程的程式碼與使用 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

這裡的更新語句將失敗,儘管我們_確實_將部件與實體管理器關聯起來(換句話說:要求實體管理器為我們持久化它)。然而,實體管理器不會因為您將其與實體管理器關聯而立即將記錄插入資料庫。這被稱為“寫回”——幾乎所有 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 查詢執行之前將其更改儲存到資料庫中_。幸運的是,JPAHibernate 都提供了實現此功能的方法。強制 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 重新整理會話,因此在需要將多個部件插入同一事務的情況下,它無法再進行最佳化。
  • 從前一個論點來看,將其作為 updateStock() 呼叫的一部分會更好,但是如果需要執行額外的 SQL 語句,我們是否也需要在那裡新增 flush() 呼叫?
hib-flush2.png

結論是我們有三個需求(新增部件、更新部件和重新整理會話),但只有兩個地方可以新增程式碼來解決需求。這就是面向切面程式設計發揮作用的地方。面向切面程式設計技術本質上提供了一個額外的、可以新增程式碼來解決這個需求的地方。換句話說,它允許我們在各自獨立的模組中解決每個需求。

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

讓我們在單獨的模組中處理每個需求。幸運的是,前兩個需求相當直接:

插入新部件


private SessionFactory sessionFactory;

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

使用 Hibernate SessionFactory,我們獲得一個會話。該會話用於儲存新部件。

更新部件庫存


private SimpleJdbcTemplate jdbcTemplate;

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

同步會話 一般來說,我們可以說_每當 JDBC 操作即將發生時,如果會話是髒的,請先重新整理會話_。我們可以將其重新表述為_在呼叫 JDBC 操作之前,如果 Hibernate 會話是髒的,請重新整理它_。這句話中有兩個重要元素。後半部分指明瞭我們_想_做什麼。前半部分回答了我們_在_哪裡以及_何時_執行重新整理行為的問題。

  • 何時:之前
  • 何地:對 JDBC 操作的呼叫
  • 何事:重新整理一個髒的 Hibernate 會話

如果瞭解 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 會話。

變體

在審查這個切面時,需要牢記一些事情。

首先,您希望應用此行為的位置可能會有所不同。上面的示例將行為應用於 SimpleJdbcTemplate 上所有方法的呼叫。這可能對您來說太多了。可以輕鬆修改切入點,將行為應用於由特定註解註解的方法(例如:execution(@JdbcOperation *(..)))。

其次,您可能會想,如果沒有可用的 Hibernate Session,會發生什麼。在 Spring 管理的環境中,SessionFactory.getCurrentSession() 總是會建立一個新的 Session。如果您希望這個切面能夠工作,即使根本沒有 SessionFactory,或者還沒有建立 Session(並且您不希望建立一個),您應該修改切面以使用 Spring 的 SessionFactoryUtils 類。這個類有方法允許您請求一個 Session,並且如果沒有可用的 Session,則不會返回任何 Session。

原始碼

隨本文附帶的原始碼使用 AspectJ 實現了 HibernateStateSynchronizer 切面。但是,修改此切面使其與 Spring AOP 配合使用將非常簡單。

HibernateCarPartsInventoryTests 測試用例演示了該行為。當切面啟用時,testAddPart() 方法成功。當切面停用時(例如,透過將其從構建路徑中排除,或註釋掉 before() 建議),測試將失敗,因為每次執行時計數語句的記錄數量都相同(換句話說,在查詢執行時,部件不在資料庫中)。

在當前設定中,before 建議被註釋掉了,所以測試將**失敗**。請注意,此專案的 pom.xml 檔案包含 Maven AspectJ 外掛。可能會有一些關於版本衝突的警告(由外掛使用與專案本身不同的 AspectJ 版本引起),但儘管有這些警告,它仍然應該有效。

原始碼:carplant.zip

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視所有