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

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

在同一個事務中混合使用物件關係對映器(ORM)的程式碼與不使用ORM的程式碼,可能會導致在底層資料庫中應該存在的資料卻不可用的問題。由於這種情況我偶爾會遇到,我認為如果我把這個問題的解決方案寫下來,對大家都會有幫助。

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

順便說一下,去年12月我在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

這裡的更新語句將會失敗,儘管我們確實將該部分與實體管理器關聯起來(換句話說:要求實體管理器為我們持久化它)。然而,由於你將它與實體管理器關聯,實體管理器並不會立即將記錄插入到資料庫中。這稱為寫後(write-behind)——幾乎所有ORM引擎都實現了這一點。實體管理器中的髒狀態(例如我們新建立的部分例項)不會立即傳送到資料庫(使用SQL語句),而通常只在事務結束時傳送。

正如你可能已經想到的,一般來說,寫後(write-behind)這個概念可能會在某些時候導致嚴重問題,當你期望資料在資料庫中可用時,它卻還沒有到位!

以正確的方式解決問題

這個問題有幾種解決方案。一種(非常無知)的解決方案是簡單地說:讓我們稍微修改一下虛擬碼,使其包含兩個事務。
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引擎將其更改儲存到資料庫稱為重新整理(flushing)。考慮到這一點,我們可以修改虛擬碼使其工作:

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()呼叫的一部分,實際上破壞了寫後(write-behind)的整個概念。在插入一個零件後立即強制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上的所有方法呼叫。這可能不適合你的偏好。可以輕鬆修改切入點(pointcut),將此行為應用於由特定註解標註的方法(例如:execution(@JdbcOperation *(..)))。

其次,你可能會想,如果沒有可用的Hibernate Session會發生什麼。在Spring管理的環境中,SessionFactory.getCurrentSession()總是建立一個新的Session。如果你希望這個切面即使在完全沒有SessionFactory的情況下,或者即使尚未建立Session(並且你也不希望建立一個)也能工作,你應該修改切面以使用Spring的SessionFactoryUtils類。這個類提供了方法,允許你請求一個Session,並且在沒有可用Session時不會返回。

原始碼

本文附帶的原始碼使用AspectJ實現了HibernateStateSynchronizer切面。然而,修改這個切面以使其與Spring AOP一起工作也相當直接。

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

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

原始碼:carplant.zip

獲取Spring新聞通訊

訂閱Spring新聞通訊保持聯絡

訂閱

獲得領先

VMware提供培訓和認證,助你快速前進。

瞭解更多

獲取支援

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

瞭解更多

即將舉行的活動

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

檢視全部