領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多在同一個事務中混合使用物件關係對映器(Object-Relational Mapper)的程式碼和不使用它的程式碼,可能會導致資料在底層資料庫中未能及時可用的問題。由於這種情況我時常遇到,我認為如果我寫下解決此問題的方法,對大家都會有所幫助。
簡而言之:我將在本文的其餘部分中介紹一個方面,它觸發底層持久化機制(JPA、Hibernate、TopLink)將任何髒資料傳送到資料庫。
在上個十二月,我參加 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
這裡的更新語句將失敗,儘管我們_確實_將部件與實體管理器關聯起來(換句話說:要求實體管理器為我們持久化它)。然而,實體管理器不會因為您將其與實體管理器關聯而立即將記錄插入資料庫。這被稱為“寫回”——幾乎所有 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

如果我們將虛擬碼直接轉換為 Java 程式碼,我們必須新增 flush() 呼叫,這時就出現了一個棘手的問題:我們將 flush() 呼叫放在哪裡:是將其作為 addPart() 呼叫的一部分(在我們將部件與 Session 關聯後),還是將其作為 updateStock() 呼叫的一部分(在發出 UPDATE 語句之前)?
無論你怎麼看,兩者都是不好的
結論是我們有三個需求(新增部件、更新部件和重新整理會話),但只有兩個地方可以新增程式碼來解決需求。這就是面向切面程式設計發揮作用的地方。面向切面程式設計技術本質上提供了一個額外的、可以新增程式碼來解決這個需求的地方。換句話說,它允許我們在各自獨立的模組中解決每個需求。
插入新部件
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 會話是髒的,請重新整理它_。這句話中有兩個重要元素。後半部分指明瞭我們_想_做什麼。前半部分回答了我們_在_哪裡以及_何時_執行重新整理行為的問題。
如果瞭解 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。
HibernateCarPartsInventoryTests 測試用例演示了該行為。當切面啟用時,testAddPart() 方法成功。當切面停用時(例如,透過將其從構建路徑中排除,或註釋掉 before() 建議),測試將失敗,因為每次執行時計數語句的記錄數量都相同(換句話說,在查詢執行時,部件不在資料庫中)。
在當前設定中,before 建議被註釋掉了,所以測試將**失敗**。請注意,此專案的 pom.xml 檔案包含 Maven AspectJ 外掛。可能會有一些關於版本衝突的警告(由外掛使用與專案本身不同的 AspectJ 版本引起),但儘管有這些警告,它仍然應該有效。
原始碼:carplant.zip