Hibernate的session.flush做了什麼呢?
an assertion failure occured (this may indicate a bug in Hibernate, but is more likely due to unsafe use of the session)
net.sf.hibernate.AssertionFailure: possible nonthreadsafe access to session
注:非possible non-threadsafe access to the session (那是另外的錯誤,類似但不一樣)
這個異常應該很多的朋友都遇到過,原因可能各不相同。但所有的異常都應該是在flush或者事務提交的過程中發生的。這一般由我們在事務開始至事務提交的過程中進行了不正確的操作導致,也會在多執行緒同時操作一個Session時發生,這裡我們僅僅討論單執行緒的情況,多執行緒除了執行緒同步外基本與此相同。
至於具體是什麼樣的錯誤操作那?我給大家看一個例子(假設Hibernate配置正確,為保持程式碼簡潔,不引入包及處理任何異常)
SessionFactory sf = new Configuration().configure().buildSessionFactory() ;
Session s = sf.openSession();
Cat cat = new Cat();
Transaction tran = s.beginTransaction(); (1)
s.save(cat); (2)(此處同樣可以為update delete)
s.evict(cat); (3)
tran.commit(); (4)
s.close();(5)
這就是引起此異常的典型錯誤。我當時就遇到了這個異常,檢查程式碼時根本沒感覺到這段程式碼出了問題,想當然的認為在Session上開始一個事務,通過Session將物件存入資料庫,再將這個物件從Session上拆離,提交事務,這是一個很正常的流程。如果這裡正常的話,那問題一定在別處。
問題恰恰就在這裡,我的想法也許沒有錯,但是一個錯誤的論據所引出的觀點永遠都不可能是正確的。因為我一直以為直接在對資料庫進行操作,忘記了在我與資料庫之間隔了一個Hibernate,Hibernate在為我們提供持久化服務的同時,也改變了我們對資料庫的操作方式,這種方式與我們直接的資料庫操作有著很多的不同,正因為我們對這種方式沒有一個大致的瞭解造成了我們的應用並未得到預先設想的結果。
那Hibernate的持久化機制到底有什麼不同那?簡單的說,Hibernate在資料庫層之上實現了一個快取區,當應用save或者update一個物件時,Hibernate並未將這個物件實際的寫入資料庫中,而僅僅是在快取中根據應用的行為做了登記,在真正需要將快取中的資料flush入資料庫時才執行先前登記的所有行為。
在實際執行的過程中,每個Session是通過幾個對映和集合來維護所有與該Session建立了關聯的物件以及應用對這些物件所進行的操作的,與我們這次討論有關的有entityEntries(與Session相關聯的物件的對映)、insertions(所有的插入操作集合)、deletions(刪除操作集合)、updates(更新操作集合)。下面我就開始解釋在最開始的例子中,Hibernate到底是怎樣運作的。
(1)生成一個事務的物件,並標記當前的Session處於事務狀態(注:此時並未啟動資料庫級事務)。
(2)應用使用s.save儲存cat物件,這個時候Session將cat這個物件放入entityEntries,用來標記cat已經和當前的會話建立了關聯,由於應用對cat做了儲存的操作,Session還要在insertions中登記應用的這個插入行為(行為包括:物件引用、物件id、Session、持久化處理類)。
(3)s.evict(cat)將cat物件從s會話中拆離,這時s會從entityEntries中將cat這個物件移出。
(4)事務提交,需要將所有快取flush入資料庫,Session啟動一個事務,並按照insert,update,……,delete的順序提交所有之前登記的操作(注意:所有insert執行完畢後才會執行update,這裡的特殊處理也可能會將你的程式搞得一團糟,如需要控制操作的執行順序,要善於使用flush),現在cat不在entityEntries中,但在執行insert的行為時只需要訪問insertions就足夠了,所以此時不會有任何的異常。異常出現在插入後通知Session該物件已經插入完畢這個步驟上,這個步驟中需要將entityEntries中cat的existsInDatabase標誌置為true,由於cat並不存在於entityEntries中,此時Hibernate就認為insertions和entityEntries可能因為執行緒安全的問題產生了不同步(也不知道Hibernate的開發者是否考慮到例子中的處理方式,如果沒有的話,這也許算是一個bug吧),於是一個net.sf.hibernate.AssertionFailure就被丟擲,程式終止。
我想現在大家應該明白例子中的程式到底哪裡有問題了吧,我們的錯誤的認為s.save會立即的執行,而將cat物件過早的與Session拆離,造成了Session的insertions和entityEntries中內容的不同步。所以我們在做此類操作時一定要清楚Hibernate什麼時候會將資料flush入資料庫,在未flush之前不要將已進行操作的物件從Session上拆離。
對於這個錯誤的解決方法是,我們可以在(2)和(3)之間插入一個s.flush()強制Session將快取中的資料flush入資料庫(此時Hibernate會提前啟動事務,將(2)中的save登記的insert語句登記在資料庫事務中,並將所有操作集合清空),這樣在(4)事務提交時insertions集合就已經是空的了,即使我們拆離了cat也不會有任何的異常了。
前面簡單的介紹了一下Hibernate的flush機制和對我們程式可能帶來的影響以及相應的解決方法,Hibernate的快取機制還會在其他的方面給我們的程式帶來一些意想不到的影響。看下面的例子:
(name為cat表的主鍵)
Cat cat = new Cat();
cat.setName(“tom”);
s.save(cat);
cat.setName(“mary”);
s.update(cat);(6)
Cat littleCat = new Cat();
littleCat.setName(“tom”);
s.save(littleCat);
s.flush();
這個例子看起來有什麼問題?估計不瞭解Hibernate快取機制的人多半會說沒有問題,但它也一樣不能按照我們的思路正常執行,在flush過程中會產生主鍵衝突,可能你想問:“在save(littleCat)之前不是已經更改cat.name並已經更新了麼?為什麼還會發生主鍵衝突那?”這裡的原因就是我在解釋第一個例子時所提到的快取flush順序的問題,Hibernate按照insert,update,……,delete的順序提交所有登記的操作,所以你的s.update(cat)雖然在程式中出現在s.save(littleCat)之前,但是在flush的過程中,所有的save都將在update之前執行,這就造成了主鍵衝突的發生。
這個例子中的更改方法一樣是在(6)之後加入s.flush()強制Session在儲存littleCat之前更新cat的name。這樣在第二次flush時就只會執行s.save(littleCat)這次登記的動作,這樣就不會出現主鍵衝突的狀況。
再看一個例子(很奇怪的例子,但是能夠說明問題)
Cat cat = new Cat();
cat.setName(“tom”);
s.save(cat); (7)
s.delete(cat);(8)
cat.id=null;(9)
s.save(cat);(10)
s.flush();
這個例子在執行時會產生異常net.sf.hibernate.HibernateException: identifier of an instance of Cat altered from 8b818e920a86f038010a86f03a9d0001 to null
這裡例子也是有關於快取的問題,但是原因稍有不同:
(7)和(2)的處理相同。
(8)Session會在deletions中登記這個刪除動作,同時更新entityEntries中該物件的登記狀態為DELETED。
(9)Cat類的識別符號欄位為id,將其置為null便於重新分配id並儲存進資料庫。
(10)此時Session會首先在entityEntries查詢cat物件是否曾經與Session做過關聯,因為cat只改變了屬性值,引用並未改變,所以會取得狀態為DELETED的那個登記物件。由於第二次儲存的物件已經在當前Session中刪除,save會強制Session將快取flush才會繼續,flush的過程中首先要執行最開始的save動作,在這個save中檢查了cat這個物件的id是否與原來執行動作時的id相同。不幸的是,此時cat的id被賦為null,異常被丟擲,程式終止(此處要注意,我們在以後的開發過程儘量不要在flush之前改變已經進行了操作的物件的id)。
這個例子中的錯誤也是由於快取的延時更新造成的(當然,與不正規的使用Hibernate也有關係),處理方法有兩種:
1、在(8)之後flush,這樣就可以保證(10)處save將cat作為一個全新的物件進行儲存。
2、刪除(9),這樣第二次save所引起的強制flush可以正常的執行,在資料庫中插入cat物件後將其刪除,然後繼續第二次save重新插入cat物件,此時cat的id仍與從前一致。
這兩種方法可以根據不同的需要來使用,呵呵,總覺得好像是很不正規的方式來解決問題,但是問題本身也不夠正規,只希望能夠在應用開發中給大家一些幫助,不對的地方也希望各位給與指正。
總的來說,由於Hibernate的flush處理機制,我們在一些複雜的物件更新和儲存的過程中就要考慮資料庫操作順序的改變以及延時flush是否對程式的結果有影響。如果確實存在著影響,那就可以在需要保持這種操作順序的位置加入flush強制Hibernate將快取中記錄的操作flush入資料庫,這樣看起來也許不太美觀,但很有效。