隨著Hibernate在Java開發(fā)中的廣泛應(yīng)用,我們?cè)谑褂肏ibernate進(jìn)行對(duì)象持久化操作中也遇到了各種各樣的問題。這些問題往往都是我們對(duì)Hibernate缺乏了解所致,這里我講個(gè)我從前遇到的問題及一些想法,希望能給大家一點(diǎn)借鑒。
?
???????這是在一次事務(wù)提交時(shí)遇到的異常。
???????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?(那是另外的錯(cuò)誤,類似但不一樣)
?
???????這個(gè)異常應(yīng)該很多的朋友都遇到過,原因可能各不相同。但所有的異常都應(yīng)該是在flush或者事務(wù)提交的過程中發(fā)生的。這一般由我們?cè)谑聞?wù)開始至事務(wù)提交的過程中進(jìn)行了不正確的操作導(dǎo)致,也會(huì)在多線程同時(shí)操作一個(gè)Session時(shí)發(fā)生,這里我們僅僅討論單線程的情況,多線程除了線程同步外基本與此相同。
?
???????至于具體是什么樣的錯(cuò)誤操作那?我給大家看一個(gè)例子(假設(shè)Hibernate配置正確,為保持代碼簡(jiǎn)潔,不引入包及處理任何異常)
??

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)


???????這就是引起此異常的典型錯(cuò)誤。我當(dāng)時(shí)就遇到了這個(gè)異常,檢查代碼時(shí)根本沒感覺到這段代碼出了問題,想當(dāng)然的認(rèn)為在Session上開始一個(gè)事務(wù),通過Session將對(duì)象存入數(shù)據(jù)庫(kù),再將這個(gè)對(duì)象從Session上拆離,提交事務(wù),這是一個(gè)很正常的流程。如果這里正常的話,那問題一定在別處。
?
????????問題恰恰就在這里,我的想法也許沒有錯(cuò),但是一個(gè)錯(cuò)誤的論據(jù)所引出的觀點(diǎn)永遠(yuǎn)都不可能是正確的。因?yàn)槲乙恢币詾橹苯釉趯?duì)數(shù)據(jù)庫(kù)進(jìn)行操作,忘記了在我與數(shù)據(jù)庫(kù)之間隔了一個(gè)Hibernate,Hibernate在為我們提供持久化服務(wù)的同時(shí),也改變了我們對(duì)數(shù)據(jù)庫(kù)的操作方式,這種方式與我們直接的數(shù)據(jù)庫(kù)操作有著很多的不同,正因?yàn)槲覀儗?duì)這種方式?jīng)]有一個(gè)大致的了解造成了我們的應(yīng)用并未得到預(yù)先設(shè)想的結(jié)果。
?
那Hibernate的持久化機(jī)制到底有什么不同那?簡(jiǎn)單的說,Hibernate在數(shù)據(jù)庫(kù)層之上實(shí)現(xiàn)了一個(gè)緩存區(qū),當(dāng)應(yīng)用save或者update一個(gè)對(duì)象時(shí),Hibernate并未將這個(gè)對(duì)象實(shí)際的寫入數(shù)據(jù)庫(kù)中,而僅僅是在緩存中根據(jù)應(yīng)用的行為做了登記,在真正需要將緩存中的數(shù)據(jù)flush入數(shù)據(jù)庫(kù)時(shí)才執(zhí)行先前登記的所有行為。
?
在實(shí)際執(zhí)行的過程中,每個(gè)Session是通過幾個(gè)映射和集合來維護(hù)所有與該Session建立了關(guān)聯(lián)的對(duì)象以及應(yīng)用對(duì)這些對(duì)象所進(jìn)行的操作的,與我們這次討論有關(guān)的有entityEntries(與Session相關(guān)聯(lián)的對(duì)象的映射)、insertions(所有的插入操作集合)、deletions(刪除操作集合)、updates(更新操作集合)。下面我就開始解釋在最開始的例子中,Hibernate到底是怎樣運(yùn)作的。
(1)生成一個(gè)事務(wù)的對(duì)象,并標(biāo)記當(dāng)前的Session處于事務(wù)狀態(tài)(注:此時(shí)并未啟動(dòng)數(shù)據(jù)庫(kù)級(jí)事務(wù))。
(2)應(yīng)用使用s.save保存cat對(duì)象,這個(gè)時(shí)候Session將cat這個(gè)對(duì)象放入entityEntries,用來標(biāo)記cat已經(jīng)和當(dāng)前的會(huì)話建立了關(guān)聯(lián),由于應(yīng)用對(duì)cat做了保存的操作,Session還要在insertions中登記應(yīng)用的這個(gè)插入行為(行為包括:對(duì)象引用、對(duì)象id、Session、持久化處理類)。
(3)s.evict(cat)將cat對(duì)象從s會(huì)話中拆離,這時(shí)s會(huì)從entityEntries中將cat這個(gè)對(duì)象移出。
(4)事務(wù)提交,需要將所有緩存flush入數(shù)據(jù)庫(kù),Session啟動(dòng)一個(gè)事務(wù),并按照insert,update,……,delete的順序提交所有之前登記的操作(注意:所有insert執(zhí)行完畢后才會(huì)執(zhí)行update,這里的特殊處理也可能會(huì)將你的程序搞得一團(tuán)糟,如需要控制操作的執(zhí)行順序,要善于使用flush),現(xiàn)在cat不在entityEntries中,但在執(zhí)行insert的行為時(shí)只需要訪問insertions就足夠了,所以此時(shí)不會(huì)有任何的異常。異常出現(xiàn)在插入后通知Session該對(duì)象已經(jīng)插入完畢這個(gè)步驟上,這個(gè)步驟中需要將entityEntries中cat的existsInDatabase標(biāo)志置為true,由于cat并不存在于entityEntries中,此時(shí)Hibernate就認(rèn)為insertions和entityEntries可能因?yàn)榫€程安全的問題產(chǎn)生了不同步(也不知道Hibernate的開發(fā)者是否考慮到例子中的處理方式,如果沒有的話,這也許算是一個(gè)bug吧),于是一個(gè)net.sf.hibernate.AssertionFailure就被拋出,程序終止。
?
我想現(xiàn)在大家應(yīng)該明白例子中的程序到底哪里有問題了吧,我們的錯(cuò)誤的認(rèn)為s.save會(huì)立即的執(zhí)行,而將cat對(duì)象過早的與Session拆離,造成了Session的insertions和entityEntries中內(nèi)容的不同步。所以我們?cè)谧龃祟惒僮鲿r(shí)一定要清楚Hibernate什么時(shí)候會(huì)將數(shù)據(jù)flush入數(shù)據(jù)庫(kù),在未flush之前不要將已進(jìn)行操作的對(duì)象從Session上拆離。
?
對(duì)于這個(gè)錯(cuò)誤的解決方法是,我們可以在(2)和(3)之間插入一個(gè)s.flush()強(qiáng)制Session將緩存中的數(shù)據(jù)flush入數(shù)據(jù)庫(kù)(此時(shí)Hibernate會(huì)提前啟動(dòng)事務(wù),將(2)中的save登記的insert語(yǔ)句登記在數(shù)據(jù)庫(kù)事務(wù)中,并將所有操作集合清空),這樣在(4)事務(wù)提交時(shí)insertions集合就已經(jīng)是空的了,即使我們拆離了cat也不會(huì)有任何的異常了。
前面簡(jiǎn)單的介紹了一下Hibernate的flush機(jī)制和對(duì)我們程序可能帶來的影響以及相應(yīng)的解決方法,Hibernate的緩存機(jī)制還會(huì)在其他的方面給我們的程序帶來一些意想不到的影響。看下面的例子:
??

(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();


這個(gè)例子看起來有什么問題?估計(jì)不了解Hibernate緩存機(jī)制的人多半會(huì)說沒有問題,但它也一樣不能按照我們的思路正常運(yùn)行,在flush過程中會(huì)產(chǎn)生主鍵沖突,可能你想問:“在save(littleCat)之前不是已經(jīng)更改cat.name并已經(jīng)更新了么?為什么還會(huì)發(fā)生主鍵沖突那?”這里的原因就是我在解釋第一個(gè)例子時(shí)所提到的緩存flush順序的問題,Hibernate按照insert,update,……,delete的順序提交所有登記的操作,所以你的s.update(cat)雖然在程序中出現(xiàn)在s.save(littleCat)之前,但是在flush的過程中,所有的save都將在update之前執(zhí)行,這就造成了主鍵沖突的發(fā)生。
?
這個(gè)例子中的更改方法一樣是在(6)之后加入s.flush()強(qiáng)制Session在保存littleCat之前更新cat的name。這樣在第二次flush時(shí)就只會(huì)執(zhí)行s.save(littleCat)這次登記的動(dòng)作,這樣就不會(huì)出現(xiàn)主鍵沖突的狀況。
?
再看一個(gè)例子(很奇怪的例子,但是能夠說明問題)

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();

?
這個(gè)例子在運(yùn)行時(shí)會(huì)產(chǎn)生異常net.sf.hibernate.HibernateException:?identifier?of?an?instance?of?Cat?altered?from?8b818e920a86f038010a86f03a9d0001?to?null
?
這里例子也是有關(guān)于緩存的問題,但是原因稍有不同:
(7)和(2)的處理相同。
(8)Session會(huì)在deletions中登記這個(gè)刪除動(dòng)作,同時(shí)更新entityEntries中該對(duì)象的登記狀態(tài)為DELETED。
(9)Cat類的標(biāo)識(shí)符字段為id,將其置為null便于重新分配id并保存進(jìn)數(shù)據(jù)庫(kù)。
(10)此時(shí)Session會(huì)首先在entityEntries查找cat對(duì)象是否曾經(jīng)與Session做過關(guān)聯(lián),因?yàn)閏at只改變了屬性值,引用并未改變,所以會(huì)取得狀態(tài)為DELETED的那個(gè)登記對(duì)象。由于第二次保存的對(duì)象已經(jīng)在當(dāng)前Session中刪除,save會(huì)強(qiáng)制Session將緩存flush才會(huì)繼續(xù),flush的過程中首先要執(zhí)行最開始的save動(dòng)作,在這個(gè)save中檢查了cat這個(gè)對(duì)象的id是否與原來執(zhí)行動(dòng)作時(shí)的id相同。不幸的是,此時(shí)cat的id被賦為null,異常被拋出,程序終止(此處要注意,我們?cè)谝院蟮拈_發(fā)過程盡量不要在flush之前改變已經(jīng)進(jìn)行了操作的對(duì)象的id)。
?
這個(gè)例子中的錯(cuò)誤也是由于緩存的延時(shí)更新造成的(當(dāng)然,與不正規(guī)的使用Hibernate也有關(guān)系),處理方法有兩種:
1、在(8)之后flush,這樣就可以保證(10)處save將cat作為一個(gè)全新的對(duì)象進(jìn)行保存。
2、刪除(9),這樣第二次save所引起的強(qiáng)制flush可以正常的執(zhí)行,在數(shù)據(jù)庫(kù)中插入cat對(duì)象后將其刪除,然后繼續(xù)第二次save重新插入cat對(duì)象,此時(shí)cat的id仍與從前一致。
?
這兩種方法可以根據(jù)不同的需要來使用,呵呵,總覺得好像是很不正規(guī)的方式來解決問題,但是問題本身也不夠正規(guī),只希望能夠在應(yīng)用開發(fā)中給大家一些幫助,不對(duì)的地方也希望各位給與指正。
?
  總的來說,由于Hibernate的flush處理機(jī)制,我們?cè)谝恍?fù)雜的對(duì)象更新和保存的過程中就要考慮數(shù)據(jù)庫(kù)操作順序的改變以及延時(shí)flush是否對(duì)程序的結(jié)果有影響。如果確實(shí)存在著影響,那就可以在需要保持這種操作順序的位置加入flush強(qiáng)制Hibernate將緩存中記錄的操作flush入數(shù)據(jù)庫(kù),這樣看起來也許不太美觀,但很有效。