獲得領先
VMware提供培訓和認證,助你快速前進。
瞭解更多在同一個事務中混合使用物件關係對映器(ORM)的程式碼與不使用ORM的程式碼,可能會導致在底層資料庫中應該存在的資料卻不可用的問題。由於這種情況我偶爾會遇到,我認為如果我把這個問題的解決方案寫下來,對大家都會有幫助。
簡而言之:本文餘下部分將介紹的是一個方面,它會觸發底層的持久化機制(JPA、Hibernate、TopLink)將任何髒資料傳送到資料庫。
順便說一下,去年12月我在The Spring Experience的一次會議上介紹了這方面的內容,本文也包含了那些等待原始碼的人所需要的原始碼。
然而,這並不意味著在應用程式中編寫顯式SQL可以完全放棄。在許多情況下,為了滿足應用程式中的特定需求,仍然需要編寫偶爾的SQL查詢。我通常看到人們仍然手動編寫SQL查詢並在Java程式碼中執行的幾個原因,例如:
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查詢執行之前將更改儲存到資料庫。幸運的是,例如JPA和Hibernate都提供了這樣做的方法。強制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
如果我們將虛擬碼直接翻譯成Java程式碼,我們就必須新增flush()呼叫,這時一個棘手的問題出現了:我們應該把flush()呼叫放在哪裡?是讓它成為addPart()呼叫的一部分(在我們把零件與Session關聯後立即執行),還是讓它成為updateStock()呼叫的一部分(在發出UPDATE語句之前執行)?
無論你怎麼看,這兩種方式都不好
總結一下,我們有三個需求(新增零件、更新零件和重新整理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是髒的,就重新整理它。這句話中有兩個重要元素。後半部分說明了我們想做什麼。前半部分回答了我們想在哪裡和何時執行重新整理行為的問題。
如果瞭解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時不會返回。
HibernateCarPartsInventoryTests測試用例演示了此行為。當切面啟用時,testAddPart()方法成功。當切面停用時(例如,透過將其從構建路徑中排除,或透過註釋掉before()通知),測試將失敗,因為count語句每次執行時都返回相同數量的記錄(換句話說,在查詢執行時,資料庫中不存在該零件)。
在當前的設定中,before通知被註釋掉了,所以測試將會失敗。請注意,這個專案的pom.xml檔案包含了Maven AspectJ外掛。可能會有一些關於版本衝突的警告(由於外掛使用的AspectJ版本與專案本身不同),但儘管存在這些警告,它仍然應該能正常工作。
原始碼:carplant.zip