領先一步
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
這裡的 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
如果我們將虛擬碼直接翻譯成 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 上方法的全部呼叫。這對您的口味來說可能太過了。可以輕鬆修改切入點,以將此行為應用於由特定註釋註釋的方法(想想:execution(@JdbcOperation *(..)))。
其次,您可能會想知道如果沒有可用的 Hibernate Session 會發生什麼情況。SessionFactory.getCurrentSession() 始終在 Spring 管理的環境中建立新的 Session。如果您希望即使根本沒有 SessionFactory,或者尚未建立 Session(並且您不希望建立 Session)時,此面向也能運作,則應修改此面向以使用 Spring 中的 SessionFactoryUtils 類別。此類別具有允許您請求 Session,如果沒有可用的 Session,則不會返回 Session 的方法。
HibernateCarPartsInventoryTests 測試案例說明了此行為。當啟用此面向時,testAddPart() 方法會成功。當停用此面向時(例如,透過將其從建置路徑中排除,或透過註解 before() advice),測試將會失敗,因為計數語句每次執行時的記錄數都相同(換句話說,在查詢執行時,零件不存在於資料庫中)。
在目前的設定中,before advice 已被註解掉,因此測試將會失敗。請注意,此專案的 pom.xml 檔案包含 Maven AspectJ 外掛程式。可能會有一些關於版本衝突的警告(由外掛程式使用與專案本身不同的 AspectJ 版本引起),但儘管存在這些警告,它仍然應該可以運作。
原始碼:carplant.zip