Hibernate是完整的對象/關(guān)系映射解決方案,它提供了對象狀態(tài)管理(state management)的功能,使開發(fā)者不再需要理會(huì)底層數(shù)據(jù)庫系統(tǒng)的細(xì)節(jié)。 也就是說,相對于常見的JDBC/SQL持久層方案中需要管理SQL語句,Hibernate采用了更自然的面向?qū)ο蟮囊暯莵沓志没疛ava應(yīng)用中的數(shù)據(jù)。
換句話說,使用Hibernate的開發(fā)者應(yīng)該總是關(guān)注對象的狀態(tài)(state),不必考慮SQL語句的執(zhí)行。 這部分細(xì)節(jié)已經(jīng)由Hibernate掌管妥當(dāng),只有開發(fā)者在進(jìn)行系統(tǒng)性能調(diào)優(yōu)的時(shí)候才需要進(jìn)行了解。
11.1. Hibernate對象狀態(tài)(object states)
Hibernate定義并支持下列對象狀態(tài)(state):
-
瞬時(shí)(Transient) - 由new操作符創(chuàng)建,且尚未與Hibernate Session 關(guān)聯(lián)的對象被認(rèn)定為瞬時(shí)(Transient)的。瞬時(shí)(Transient)對象不會(huì)被持久化到數(shù)據(jù)庫中,也不會(huì)被賦予持久化標(biāo)識(shí)(identifier)。 如果程序中沒有保持對瞬時(shí)(Transient)對象的引用,它會(huì)被垃圾回收器(garbage collector)銷毀。 使用Hibernate Session可以將其變?yōu)槌志?Persistent)狀態(tài)。(Hibernate會(huì)自動(dòng)執(zhí)行必要的SQL語句)
-
持久(Persistent) - 持久(Persistent)的實(shí)例在數(shù)據(jù)庫中有對應(yīng)的記錄,并擁有一個(gè)持久化標(biāo)識(shí)(identifier)。 持久(Persistent)的實(shí)例可能是剛被保存的,或剛被加載的,無論哪一種,按定義對象都僅在相關(guān)聯(lián)的Session生命周期內(nèi)的保持這種狀態(tài)。 Hibernate會(huì)檢測到處于持久(Persistent)狀態(tài)的對象的任何改動(dòng),在當(dāng)前操作單元(unit of work)執(zhí)行完畢時(shí)將對象數(shù)據(jù)(state)與數(shù)據(jù)庫同步(synchronize)。 開發(fā)者不需要手動(dòng)執(zhí)行UPDATE。將對象從持久(Persistent)狀態(tài)變成瞬時(shí)(Transient)狀態(tài)同樣也不需要手動(dòng)執(zhí)行DELETE語句。
-
脫管(Detached) - 與持久(Persistent)對象關(guān)聯(lián)的Session被關(guān)閉后,對象就變?yōu)槊摴?Detached)的。 對脫管(Detached)對象的引用依然有效,對象可繼續(xù)被修改。脫管(Detached)對象如果重新關(guān)聯(lián)到某個(gè)新的Session上, 會(huì)再次轉(zhuǎn)變?yōu)槌志?Persistent)的(Detached其間的改動(dòng)將被持久化到數(shù)據(jù)庫)。 這個(gè)功能使得一種編程模型,即中間會(huì)給用戶思考時(shí)間(user think-time)的長時(shí)間運(yùn)行的操作單元(unit of work)的編程模型成為可能。 我們稱之為應(yīng)用程序事務(wù),即從用戶觀點(diǎn)看是一個(gè)操作單元(unit of work)。
接下來我們來細(xì)致的討論下狀態(tài)(states)及狀態(tài)間的轉(zhuǎn)換(state transitions)(以及觸發(fā)狀態(tài)轉(zhuǎn)換的Hibernate方法)。
Hibernate認(rèn)為持久化類(persistent class)新實(shí)例化的對象是瞬時(shí)(Transient)的。 我們可將瞬時(shí)(Transient)對象與session關(guān)聯(lián)而變?yōu)?span id="ndexysb" class=emphasis>持久(Persistent)的。
DomesticCat fritz = new DomesticCat();
fritz.setColor(Color.GINGER);
fritz.setSex('M');
fritz.setName("Fritz");
Long generatedId = (Long) sess.save(fritz);
如果Cat的持久化標(biāo)識(shí)(identifier)是generated類型的, 那么該標(biāo)識(shí)(identifier)會(huì)自動(dòng)在save()被調(diào)用時(shí)產(chǎn)生并分配給cat。 如果Cat的持久化標(biāo)識(shí)(identifier)是assigned類型的,或是一個(gè)復(fù)合主鍵(composite key), 那么該標(biāo)識(shí)(identifier)應(yīng)當(dāng)在調(diào)用save()之前手動(dòng)賦予給cat。 你也可以按照EJB3 early draft中定義的語義,使用persist()替代save()。
此外,你可以用一個(gè)重載版本的save()方法。
DomesticCat pk = new DomesticCat();
pk.setColor(Color.TABBY);
pk.setSex('F');
pk.setName("PK");
pk.setKittens( new HashSet() );
pk.addKitten(fritz);
sess.save( pk, new Long(1234) );
如果你持久化的對象有關(guān)聯(lián)的對象(associated objects)(例如上例中的kittens集合) 那么對這些對象(譯注:pk和kittens)進(jìn)行持久化的順序是任意的(也就是說可以先對kittens進(jìn)行持久化也可以先對pk進(jìn)行持久化), 除非你在外鍵列上有NOT NULL約束。 Hibernate不會(huì)違反外鍵約束,但是如果你用錯(cuò)誤的順序持久化對象(譯注:在pk持久之前持久kitten),那么可能會(huì)違反NOT NULL約束。
通常你不會(huì)為這些細(xì)節(jié)煩心,因?yàn)槟愫芸赡軙?huì)使用Hibernate的 傳播性持久化(transitive persistence)功能自動(dòng)保存相關(guān)聯(lián)那些對象。 這樣連違反NOT NULL約束情況都不會(huì)出現(xiàn)了 - Hibernate會(huì)管好所有的事情。 傳播性持久化(transitive persistence)將在本章稍后討論。
如果你知道某個(gè)實(shí)例的持久化標(biāo)識(shí)(identifier),你就可以使用Session的load()方法 來獲取它。 load()的另一個(gè)參數(shù)是指定類的.class對象。 本方法會(huì)創(chuàng)建指定類的持久化實(shí)例,并從數(shù)據(jù)庫加載其數(shù)據(jù)(state)。
Cat fritz = (Cat) sess.load(Cat.class, generatedId);
// you need to wrap primitive identifiers
long pkId = 1234;
DomesticCat pk = (DomesticCat) sess.load( Cat.class, new Long(pkId) );
此外, 你可以把數(shù)據(jù)(state)加載到指定的對象實(shí)例上(覆蓋掉該實(shí)例原來的數(shù)據(jù))。
Cat cat = new DomesticCat();
// load pk's state into cat
sess.load( cat, new Long(pkId) );
Set kittens = cat.getKittens();
請注意如果沒有匹配的數(shù)據(jù)庫記錄,load()方法可能拋出無法恢復(fù)的異常(unrecoverable exception)。 如果類的映射使用了代理(proxy),load()方法會(huì)返回一個(gè)未初始化的代理,直到你調(diào)用該代理的某方法時(shí)才會(huì)去訪問數(shù)據(jù)庫。 若你希望在某對象中創(chuàng)建一個(gè)指向另一個(gè)對象的關(guān)聯(lián),又不想在從數(shù)據(jù)庫中裝載該對象時(shí)同時(shí)裝載相關(guān)聯(lián)的那個(gè)對象,那么這種操作方式就用得上的了。 如果為相應(yīng)類映射關(guān)系設(shè)置了batch-size, 那么使用這種操作方式允許多個(gè)對象被一批裝載(因?yàn)榉祷氐氖谴恚瑹o需從數(shù)據(jù)庫中抓取所有對象的數(shù)據(jù))。
如果你不確定是否有匹配的行存在,應(yīng)該使用get()方法,它會(huì)立刻訪問數(shù)據(jù)庫,如果沒有對應(yīng)的行,會(huì)返回null。
Cat cat = (Cat) sess.get(Cat.class, id);
if (cat==null) {
cat = new Cat();
sess.save(cat, id);
}
return cat;
你甚至可以選用某個(gè)LockMode,用SQL的SELECT ... FOR UPDATE裝載對象。 請查閱API文檔以獲取更多信息。
Cat cat = (Cat) sess.get(Cat.class, id, LockMode.UPGRADE);
注意,任何關(guān)聯(lián)的對象或者包含的集合都不會(huì)被以FOR UPDATE方式返回, 除非你指定了lock或者all作為關(guān)聯(lián)(association)的級(jí)聯(lián)風(fēng)格(cascade style)。
任何時(shí)候都可以使用refresh()方法強(qiáng)迫裝載對象和它的集合。如果你使用數(shù)據(jù)庫觸發(fā)器功能來處理對象的某些屬性,這個(gè)方法就很有用了。
sess.save(cat);
sess.flush(); //force the SQL INSERT
sess.refresh(cat); //re-read the state (after the trigger executes)
此處通常會(huì)出現(xiàn)一個(gè)重要問題: Hibernate會(huì)從數(shù)據(jù)庫中裝載多少東西?會(huì)執(zhí)行多少條相應(yīng)的SQLSELECT語句? 這取決于抓取策略(fetching strategy),會(huì)在第 20.1 節(jié) “ 抓取策略(Fetching strategies) ”中解釋。
如果不知道所要尋找的對象的持久化標(biāo)識(shí),那么你需要使用查詢。Hibernate支持強(qiáng)大且易于使用的面向?qū)ο蟛樵冋Z言(HQL)。 如果希望通過編程的方式創(chuàng)建查詢,Hibernate提供了完善的按條件(Query By Criteria, QBC)以及按樣例(Query By Example, QBE)進(jìn)行查詢的功能。 你也可以用原生SQL(native SQL)描述查詢,Hibernate提供了將結(jié)果集(result set)轉(zhuǎn)化為對象的部分支持。
HQL和原生SQL(native SQL)查詢要通過為org.hibernate.Query的實(shí)例來表達(dá)。 這個(gè)接口提供了參數(shù)綁定、結(jié)果集處理以及運(yùn)行實(shí)際查詢的方法。 你總是可以通過當(dāng)前Session獲取一個(gè)Query對象:
List cats = session.createQuery(
"from Cat as cat where cat.birthdate < ?")
.setDate(0, date)
.list();
List mothers = session.createQuery(
"select mother from Cat as cat join cat.mother as mother where cat.name = ?")
.setString(0, name)
.list();
List kittens = session.createQuery(
"from Cat as cat where cat.mother = ?")
.setEntity(0, pk)
.list();
Cat mother = (Cat) session.createQuery(
"select cat.mother from Cat as cat where cat = ?")
.setEntity(0, izi)
.uniqueResult();
一個(gè)查詢通常在調(diào)用list()時(shí)被執(zhí)行,執(zhí)行結(jié)果會(huì)完全裝載進(jìn)內(nèi)存中的一個(gè)集合(collection)。 查詢返回的對象處于持久(persistent)狀態(tài)。如果你知道的查詢只會(huì)返回一個(gè)對象,可使用list()的快捷方式uniqueResult()。
11.4.1.1. 迭代式獲取結(jié)果(Iterating results)
某些情況下,你可以使用iterate()方法得到更好的性能。 這通常是你預(yù)期返回的結(jié)果在session,或二級(jí)緩存(second-level cache)中已經(jīng)存在時(shí)的情況。 如若不然,iterate()會(huì)比list()慢,而且可能簡單查詢也需要進(jìn)行多次數(shù)據(jù)庫訪問: iterate()會(huì)首先使用1條語句得到所有對象的持久化標(biāo)識(shí)(identifiers),再根據(jù)持久化標(biāo)識(shí)執(zhí)行n條附加的select語句實(shí)例化實(shí)際的對象。
// fetch ids
Iterator iter = sess.createQuery("from eg.Qux q order by q.likeliness").iterate();
while ( iter.hasNext() ) {
Qux qux = (Qux) iter.next(); // fetch the object
// something we couldnt express in the query
if ( qux.calculateComplicatedAlgorithm() ) {
// delete the current instance
iter.remove();
// dont need to process the rest
break;
}
}
11.4.1.2. 返回元組(tuples)的查詢
(譯注:元組(tuples)指一條結(jié)果行包含多個(gè)對象) Hibernate查詢有時(shí)返回元組(tuples),每個(gè)元組(tuples)以數(shù)組的形式返回:
Iterator kittensAndMothers = sess.createQuery(
"select kitten, mother from Cat kitten join kitten.mother mother")
.list()
.iterator();
while ( kittensAndMothers.hasNext() ) {
Object[] tuple = (Object[]) kittensAndMothers.next();
Cat kitten = tuple[0];
Cat mother = tuple[1];
....
}
11.4.1.3. 標(biāo)量(Scalar)結(jié)果
查詢可在select從句中指定類的屬性,甚至可以調(diào)用SQL統(tǒng)計(jì)(aggregate)函數(shù)。 屬性或統(tǒng)計(jì)結(jié)果被認(rèn)定為"標(biāo)量(Scalar)"的結(jié)果(而不是持久(persistent state)的實(shí)體)。
Iterator results = sess.createQuery(
"select cat.color, min(cat.birthdate), count(cat) from Cat cat " +
"group by cat.color")
.list()
.iterator();
while ( results.hasNext() ) {
Object[] row = results.next();
Color type = (Color) row[0];
Date oldest = (Date) row[1];
Integer count = (Integer) row[2];
.....
}
接口Query提供了對命名參數(shù)(named parameters)、JDBC風(fēng)格的問號(hào)(?)參數(shù)進(jìn)行綁定的方法。 不同于JDBC,Hibernate對參數(shù)從0開始計(jì)數(shù)。 命名參數(shù)(named parameters)在查詢字符串中是形如:name的標(biāo)識(shí)符。 命名參數(shù)(named parameters)的優(yōu)點(diǎn)是:
//named parameter (preferred)
Query q = sess.createQuery("from DomesticCat cat where cat.name = :name");
q.setString("name", "Fritz");
Iterator cats = q.iterate();
//positional parameter
Query q = sess.createQuery("from DomesticCat cat where cat.name = ?");
q.setString(0, "Izi");
Iterator cats = q.iterate();
//named parameter list
List names = new ArrayList();
names.add("Izi");
names.add("Fritz");
Query q = sess.createQuery("from DomesticCat cat where cat.name in (:namesList)");
q.setParameterList("namesList", names);
List cats = q.list();
如果你需要指定結(jié)果集的范圍(希望返回的最大行數(shù)/或開始的行數(shù)),應(yīng)該使用Query接口提供的方法:
Query q = sess.createQuery("from DomesticCat cat");
q.setFirstResult(20);
q.setMaxResults(10);
List cats = q.list();
Hibernate 知道如何將這個(gè)有限定條件的查詢轉(zhuǎn)換成你的數(shù)據(jù)庫的原生SQL(native SQL)。
11.4.1.6. 可滾動(dòng)遍歷(Scrollable iteration)
如果你的JDBC驅(qū)動(dòng)支持可滾動(dòng)的ResuleSet,Query接口可以使用ScrollableResults,允許你在查詢結(jié)果中靈活游走。
Query q = sess.createQuery("select cat.name, cat from DomesticCat cat " +
"order by cat.name");
ScrollableResults cats = q.scroll();
if ( cats.first() ) {
// find the first name on each page of an alphabetical list of cats by name
firstNamesOfPages = new ArrayList();
do {
String name = cats.getString(0);
firstNamesOfPages.add(name);
}
while ( cats.scroll(PAGE_SIZE) );
// Now get the first page of cats
pageOfCats = new ArrayList();
cats.beforeFirst();
int i=0;
while( ( PAGE_SIZE > i++ ) && cats.next() ) pageOfCats.add( cats.get(1) );
}
cats.close()
請注意,使用此功能需要保持?jǐn)?shù)據(jù)庫連接(以及游標(biāo)(cursor))處于一直打開狀態(tài)。 如果你需要斷開連接使用分頁功能,請使用setMaxResult()/setFirstResult()
11.4.1.7. 外置命名查詢(Externalizing named queries)
你可以在映射文件中定義命名查詢(named queries)。 (如果你的查詢串中包含可能被解釋為XML標(biāo)記(markup)的字符,別忘了用CDATA包裹起來。)
<query name="eg.DomesticCat.by.name.and.minimum.weight"><![CDATA[
from eg.DomesticCat as cat
where cat.name = ?
and cat.weight > ?
] ]></query>
參數(shù)綁定及執(zhí)行以編程方式(programatically)完成:
Query q = sess.getNamedQuery("eg.DomesticCat.by.name.and.minimum.weight");
q.setString(0, name);
q.setInt(1, minWeight);
List cats = q.list();
請注意實(shí)際的程序代碼與所用的查詢語言無關(guān),你也可在元數(shù)據(jù)中定義原生SQL(native SQL)查詢, 或?qū)⒃械钠渌牟樵冋Z句放在配置文件中,這樣就可以讓Hibernate統(tǒng)一管理,達(dá)到遷移的目的。
集合過濾器(filter)是一種用于一個(gè)持久化集合或者數(shù)組的特殊的查詢。查詢字符串中可以使用"this"來引用集合中的當(dāng)前元素。
Collection blackKittens = session.createFilter(
pk.getKittens(),
"where this.color = ?")
.setParameter( Color.BLACK, Hibernate.custom(ColorUserType.class) )
.list()
);
返回的集合可以被認(rèn)為是一個(gè)包(bag, 無順序可重復(fù)的集合(collection)),它是所給集合的副本。 原來的集合不會(huì)被改動(dòng)(這與“過濾器(filter)”的隱含的含義不符,不過與我們期待的行為一致)。
請注意過濾器(filter)并不需要from子句(當(dāng)然需要的話它們也可以加上)。過濾器(filter)不限定于只能返回集合元素本身。
Collection blackKittenMates = session.createFilter(
pk.getKittens(),
"select this.mate where this.color = eg.Color.BLACK.intValue")
.list();
即使無條件的過濾器(filter)也是有意義的。例如,用于加載一個(gè)大集合的子集:
Collection tenKittens = session.createFilter(
mother.getKittens(), "")
.setFirstResult(0).setMaxResults(10)
.list();
11.4.3. 條件查詢(Criteria queries)
HQL極為強(qiáng)大,但是有些人希望能夠動(dòng)態(tài)的使用一種面向?qū)ο驛PI創(chuàng)建查詢,而非在他們的Java代碼中嵌入字符串。對于那部分人來說,Hibernate提供了直觀的Criteria查詢API。
Criteria crit = session.createCriteria(Cat.class);
crit.add( Expression.eq( "color", eg.Color.BLACK ) );
crit.setMaxResults(10);
List cats = crit.list();
Criteria以及相關(guān)的樣例(Example)API將會(huì)再第 16 章 條件查詢(Criteria Queries) 中詳細(xì)討論。
你可以使用createSQLQuery()方法,用SQL來描述查詢,并由Hibernate處理將結(jié)果集轉(zhuǎn)換成對象的工作。 請注意,你可以在任何時(shí)候調(diào)用session.connection()來獲得并使用JDBC Connection對象。 如果你選擇使用Hibernate的API, 你必須把SQL別名用大括號(hào)包圍起來:
List cats = session.createSQLQuery(
"SELECT {cat.*} FROM CAT {cat} WHERE ROWNUM<10",
"cat",
Cat.class
).list();
List cats = session.createSQLQuery(
"SELECT {cat}.ID AS {cat.id}, {cat}.SEX AS {cat.sex}, " +
"{cat}.MATE AS {cat.mate}, {cat}.SUBCLASS AS {cat.class}, ... " +
"FROM CAT {cat} WHERE ROWNUM<10",
"cat",
Cat.class
).list()
和Hibernate查詢一樣,SQL查詢也可以包含命名參數(shù)和占位參數(shù)。 可以在第 17 章 Native SQL查詢找到更多關(guān)于Hibernate中原生SQL(native SQL)的信息。
事務(wù)中的持久實(shí)例(就是通過session裝載、保存、創(chuàng)建或者查詢出的對象) 被應(yīng)用程序操作所造成的任何修改都會(huì)在Session被刷出(flushed)的時(shí)候被持久化(本章后面會(huì)詳細(xì)討論)。 這里不需要調(diào)用某個(gè)特定的方法(比如update(),設(shè)計(jì)它的目的是不同的)將你的修改持久化。 所以最直接的更新一個(gè)對象的方法就是在Session處于打開狀態(tài)時(shí)load()它,然后直接修改即可:
DomesticCat cat = (DomesticCat) sess.load( Cat.class, new Long(69) );
cat.setName("PK");
sess.flush(); // changes to cat are automatically detected and persisted
有時(shí)這種程序模型效率低下,因?yàn)樗谕籗ession里需要一條SQL SELECT語句(用于加載對象) 以及一條SQL UPDATE語句(持久化更新的狀態(tài))。 為此Hibernate提供了另一種途徑,使用脫管(detached)實(shí)例。
請注意Hibernate本身不提供直接執(zhí)行UPDATE或DELETE語句的API。 Hibernate提供的是狀態(tài)管理(state management)服務(wù),你不必考慮要使用的語句(statements)。 JDBC是出色的執(zhí)行SQL語句的API,任何時(shí)候調(diào)用session.connection()你都可以得到一個(gè)JDBC Connection對象。 此外,在聯(lián)機(jī)事務(wù)處理(OLTP)程序中,大量操作(mass operations)與對象/關(guān)系映射的觀點(diǎn)是相沖突的。 Hibernate的將來版本可能會(huì)提供專門的進(jìn)行大量操作(mass operation)的功能。 參考第 14 章 批量處理(Batch processing),尋找一些可用的批量(batch)操作技巧。
很多程序需要在某個(gè)事務(wù)中獲取對象,然后將對象發(fā)送到界面層去操作,最后在一個(gè)新的事務(wù)保存所做的修改。 在高并發(fā)訪問的環(huán)境中使用這種方式,通常使用附帶版本信息的數(shù)據(jù)來保證這些“長“工作單元之間的隔離。
Hibernate通過提供使用Session.update()或Session.merge()方法 重新關(guān)聯(lián)脫管實(shí)例的辦法來支持這種模型。
// in the first session
Cat cat = (Cat) firstSession.load(Cat.class, catId);
Cat potentialMate = new Cat();
firstSession.save(potentialMate);
// in a higher layer of the application
cat.setMate(potentialMate);
// later, in a new session
secondSession.update(cat); // update cat
secondSession.update(mate); // update mate
如果具有catId持久化標(biāo)識(shí)的Cat之前已經(jīng)被另一Session(secondSession)裝載了, 應(yīng)用程序進(jìn)行重關(guān)聯(lián)操作(reattach)的時(shí)候會(huì)拋出一個(gè)異常。
如果你確定當(dāng)前session沒有包含與之具有相同持久化標(biāo)識(shí)的持久實(shí)例,使用update()。 如果想隨時(shí)合并你的的改動(dòng)而不考慮session的狀態(tài),使用merge()。 換句話說,在一個(gè)新session中通常第一個(gè)調(diào)用的是update()方法,以便保證重新關(guān)聯(lián)脫管(detached)對象的操作首先被執(zhí)行。
希望相關(guān)聯(lián)的脫管對象(通過引用“可到達(dá)”的脫管對象)的數(shù)據(jù)也要更新到數(shù)據(jù)庫時(shí)(并且也僅僅在這種情況), 應(yīng)用程序需要對該相關(guān)聯(lián)的脫管對象單獨(dú)調(diào)用update() 當(dāng)然這些可以自動(dòng)完成,即通過使用傳播性持久化(transitive persistence),請看第 11.11 節(jié) “傳播性持久化(transitive persistence)”。
lock()方法也允許程序重新關(guān)聯(lián)某個(gè)對象到一個(gè)新session上。不過,該脫管(detached)的對象必須是沒有修改過的!
//just reassociate:
sess.lock(fritz, LockMode.NONE);
//do a version check, then reassociate:
sess.lock(izi, LockMode.READ);
//do a version check, using SELECT ... FOR UPDATE, then reassociate:
sess.lock(pk, LockMode.UPGRADE);
請注意,lock()可以搭配多種LockMode, 更多信息請閱讀API文檔以及關(guān)于事務(wù)處理(transaction handling)的章節(jié)。重新關(guān)聯(lián)不是lock()的唯一用途。
其他用于長時(shí)間工作單元的模型會(huì)在第 12.3 節(jié) “樂觀并發(fā)控制(Optimistic concurrency control)”中討論。
Hibernate的用戶曾要求一個(gè)既可自動(dòng)分配新持久化標(biāo)識(shí)(identifier)保存瞬時(shí)(transient)對象,又可更新/重新關(guān)聯(lián)脫管(detached)實(shí)例的通用方法。 saveOrUpdate()方法實(shí)現(xiàn)了這個(gè)功能。
// in the first session
Cat cat = (Cat) firstSession.load(Cat.class, catID);
// in a higher tier of the application
Cat mate = new Cat();
cat.setMate(mate);
// later, in a new session
secondSession.saveOrUpdate(cat); // update existing state (cat has a non-null id)
secondSession.saveOrUpdate(mate); // save the new instance (mate has a null id)
saveOrUpdate()用途和語義可能會(huì)使新用戶感到迷惑。 首先,只要你沒有嘗試在某個(gè)session中使用來自另一session的實(shí)例,你應(yīng)該就不需要使用update(), saveOrUpdate(),或merge()。有些程序從來不用這些方法。
通常下面的場景會(huì)使用update()或saveOrUpdate():
saveOrUpdate()做下面的事:
-
如果對象已經(jīng)在本session中持久化了,不做任何事
-
如果另一個(gè)與本session關(guān)聯(lián)的對象擁有相同的持久化標(biāo)識(shí)(identifier),拋出一個(gè)異常
-
如果對象沒有持久化標(biāo)識(shí)(identifier)屬性,對其調(diào)用save()
-
如果對象的持久標(biāo)識(shí)(identifier)表明其是一個(gè)新實(shí)例化的對象,對其調(diào)用save()
-
如果對象是附帶版本信息的(通過<version>或<timestamp>) 并且版本屬性的值表明其是一個(gè)新實(shí)例化的對象,save()它。
-
否則update() 這個(gè)對象
merge()可非常不同:
-
如果session中存在相同持久化標(biāo)識(shí)(identifier)的實(shí)例,用用戶給出的對象的狀態(tài)覆蓋舊有的持久實(shí)例
-
如果session沒有相應(yīng)的持久實(shí)例,則嘗試從數(shù)據(jù)庫中加載,或創(chuàng)建新的持久化實(shí)例
-
最后返回該持久實(shí)例
-
用戶給出的這個(gè)對象沒有被關(guān)聯(lián)到session上,它依舊是脫管的
使用Session.delete()會(huì)把對象的狀態(tài)從數(shù)據(jù)庫中移除。 當(dāng)然,你的應(yīng)用程序可能仍然持有一個(gè)指向已刪除對象的引用。所以,最好這樣理解:delete()的用途是把一個(gè)持久實(shí)例變成瞬時(shí)(transient)實(shí)例。
sess.delete(cat);
你可以用你喜歡的任何順序刪除對象,不用擔(dān)心外鍵約束沖突。當(dāng)然,如果你搞錯(cuò)了順序,還是有可能引發(fā)在外鍵字段定義的NOT NULL約束沖突。 例如你刪除了父對象,但是忘記刪除孩子們。
11.9. 在兩個(gè)不同數(shù)據(jù)庫間復(fù)制對象
偶爾會(huì)用到不重新生成持久化標(biāo)識(shí)(identifier),將持久實(shí)例以及其關(guān)聯(lián)的實(shí)例持久到不同的數(shù)據(jù)庫中的操作。
//retrieve a cat from one database
Session session1 = factory1.openSession();
Transaction tx1 = session1.beginTransaction();
Cat cat = session1.get(Cat.class, catId);
tx1.commit();
session1.close();
//reconcile with a second database
Session session2 = factory2.openSession();
Transaction tx2 = session2.beginTransaction();
session2.replicate(cat, ReplicationMode.LATEST_VERSION);
tx2.commit();
session2.close();
ReplicationMode決定數(shù)據(jù)庫中已存在相同行時(shí),replicate()如何處理。
-
ReplicationMode.IGNORE - 忽略它
-
ReplicationMode.OVERWRITE - 覆蓋相同的行
-
ReplicationMode.EXCEPTION - 拋出異常
-
ReplicationMode.LATEST_VERSION - 如果當(dāng)前的版本較新,則覆蓋,否則忽略
這個(gè)功能的用途包括使錄入的數(shù)據(jù)在不同數(shù)據(jù)庫中一致,產(chǎn)品升級(jí)時(shí)升級(jí)系統(tǒng)配置信息,回滾non-ACID事務(wù)中的修改等等。 (譯注,non-ACID,非ACID;ACID,Atomic,Consistent,Isolated and Durable的縮寫)
每間隔一段時(shí)間,Session會(huì)執(zhí)行一些必需的SQL語句來把內(nèi)存中的對象的狀態(tài)同步到JDBC連接中。這個(gè)過程被稱為刷出(flush),默認(rèn)會(huì)在下面的時(shí)間點(diǎn)執(zhí)行:
涉及的SQL語句會(huì)按照下面的順序發(fā)出執(zhí)行:
-
所有對實(shí)體進(jìn)行插入的語句,其順序按照對象執(zhí)行Session.save()的時(shí)間順序
-
所有對實(shí)體進(jìn)行更新的語句
-
所有進(jìn)行集合刪除的語句
-
所有對集合元素進(jìn)行刪除,更新或者插入的語句
-
所有進(jìn)行集合插入的語句
-
所有對實(shí)體進(jìn)行刪除的語句,其順序按照對象執(zhí)行Session.delete()的時(shí)間順序
(有一個(gè)例外是,如果對象使用native方式來生成ID(持久化標(biāo)識(shí))的話,它們一執(zhí)行save就會(huì)被插入。)
除非你明確地發(fā)出了flush()指令,關(guān)于Session何時(shí)會(huì)執(zhí)行這些JDBC調(diào)用是完全無法保證的,只能保證它們執(zhí)行的前后順序。 當(dāng)然,Hibernate保證,Query.list(..)絕對不會(huì)返回已經(jīng)失效的數(shù)據(jù),也不會(huì)返回錯(cuò)誤數(shù)據(jù)。
也可以改變默認(rèn)的設(shè)置,來讓刷出(flush)操作發(fā)生的不那么頻繁。 FlushMode類定義了三種不同的方式。 僅在提交時(shí)刷出(僅當(dāng)Hibernate的Transaction API被使用時(shí)有效), 按照剛才說的方式刷出, 以及除非明確使用flush()否則從不刷出。 最后一種模式對于那些需要長時(shí)間保持Session為打開或者斷線狀態(tài)的長時(shí)間運(yùn)行的工作單元很有用。 (參見 第 12.3.2 節(jié) “長生命周期session和自動(dòng)版本化”).
sess = sf.openSession();
Transaction tx = sess.beginTransaction();
sess.setFlushMode(FlushMode.COMMIT); // allow queries to return stale state
Cat izi = (Cat) sess.load(Cat.class, id);
izi.setName(iznizi);
// might return stale data
sess.find("from Cat as cat left outer join cat.kittens kitten");
// change to izi is not flushed!
...
tx.commit(); // flush occurs
刷出(flush)期間,可能會(huì)拋出異常。(例如一個(gè)DML操作違反了約束) 異常處理涉及到對Hibernate事務(wù)性行為的理解,因此我們將在第 12 章 事務(wù)和并發(fā)中討論。
11.11. 傳播性持久化(transitive persistence)
對每一個(gè)對象都要執(zhí)行保存,刪除或重關(guān)聯(lián)操作讓人感覺有點(diǎn)麻煩,尤其是在處理許多彼此關(guān)聯(lián)的對象的時(shí)候。 一個(gè)常見的例子是父子關(guān)系。考慮下面的例子:
如果一個(gè)父子關(guān)系中的子對象是值類型(value typed)(例如,地址或字符串的集合)的,他們的生命周期會(huì)依賴于父對象,可以享受方便的級(jí)聯(lián)操作(Cascading),不需要額外的動(dòng)作。 父對象被保存時(shí),這些值類型(value typed)子對象也將被保存;父對象被刪除時(shí),子對象也將被刪除。 這對將一個(gè)子對象從集合中移除是同樣有效:Hibernate會(huì)檢測到,并且因?yàn)橹殿愋?value typed)的對象不可能被其他對象引用,所以Hibernate會(huì)在數(shù)據(jù)庫中刪除這個(gè)子對象。
現(xiàn)在考慮同樣的場景,不過父子對象都是實(shí)體(entities)類型,而非值類型(value typed)(例如,類別與個(gè)體,或母貓和小貓)。 實(shí)體有自己的生命期,允許共享對其的引用(因此從集合中移除一個(gè)實(shí)體,不意味著它可以被刪除), 并且實(shí)體到其他關(guān)聯(lián)實(shí)體之間默認(rèn)沒有級(jí)聯(lián)操作的設(shè)置。 Hibernate默認(rèn)不實(shí)現(xiàn)所謂的可到達(dá)即持久化(persistence by reachability)的策略。
每個(gè)Hibernate session的基本操作 - 包括 persist(), merge(), saveOrUpdate(), delete(), lock(), refresh(), evict(), replicate() - 都有對應(yīng)的級(jí)聯(lián)風(fēng)格(cascade style)。 這些級(jí)聯(lián)風(fēng)格(cascade style)風(fēng)格分別命名為 create, merge, save-update, delete, lock, refresh, evict, replicate。 如果你希望一個(gè)操作被順著關(guān)聯(lián)關(guān)系級(jí)聯(lián)傳播,你必須在映射文件中指出這一點(diǎn)。例如:
<one-to-one name="person" cascade="persist"/>
級(jí)聯(lián)風(fēng)格(cascade style)是可組合的:
<one-to-one name="person" cascade="persist,delete,lock"/>
你可以使用cascade="all"來指定全部操作都順著關(guān)聯(lián)關(guān)系級(jí)聯(lián)(cascaded)。 默認(rèn)值是cascade="none",即任何操作都不會(huì)被級(jí)聯(lián)(cascaded)。
注意有一個(gè)特殊的級(jí)聯(lián)風(fēng)格(cascade style) delete-orphan,只應(yīng)用于one-to-many關(guān)聯(lián),表明delete()操作 應(yīng)該被應(yīng)用于所有從關(guān)聯(lián)中刪除的對象。
建議:
-
通常在<many-to-one>或<many-to-many>關(guān)系中應(yīng)用級(jí)聯(lián)(cascade)沒什么意義。 級(jí)聯(lián)(cascade)通常在 <one-to-one>和<one-to-many>關(guān)系中比較有用。
-
如果子對象的壽命限定在父親對象的壽命之內(nèi),可通過指定cascade="all,delete-orphan"將其變?yōu)?span id="pfrkhmg" class=emphasis>自動(dòng)生命周期管理的對象(lifecycle object)。
-
其他情況,你可根本不需要級(jí)聯(lián)(cascade)。但是如果你認(rèn)為你會(huì)經(jīng)常在某個(gè)事務(wù)中同時(shí)用到父對象與子對象,并且你希望少打點(diǎn)兒字,可以考慮使用cascade="persist,merge,save-update"。
可以使用cascade="all"將一個(gè)關(guān)聯(lián)關(guān)系(無論是對值對象的關(guān)聯(lián),或者對一個(gè)集合的關(guān)聯(lián))標(biāo)記為父/子關(guān)系的關(guān)聯(lián)。 這樣對父對象進(jìn)行save/update/delete操作就會(huì)導(dǎo)致子對象也進(jìn)行save/update/delete操作。
此外,一個(gè)持久的父對象對子對象的淺引用(mere reference)會(huì)導(dǎo)致子對象被同步save/update。 不過,這個(gè)隱喻(metaphor)的說法并不完整。除非關(guān)聯(lián)是<one-to-many>關(guān)聯(lián)并且被標(biāo)記為cascade="delete-orphan", 否則父對象失去對某個(gè)子對象的引用不會(huì)導(dǎo)致該子對象被自動(dòng)刪除。 父子關(guān)系的級(jí)聯(lián)(cascading)操作準(zhǔn)確語義如下:
-
如果父對象被persist(),那么所有子對象也會(huì)被persist()
-
如果父對象被merge(),那么所有子對象也會(huì)被merge()
-
如果父對象被save(),update()或 saveOrUpdate(),那么所有子對象則會(huì)被saveOrUpdate()
-
如果某個(gè)持久的父對象引用了瞬時(shí)(transient)或者脫管(detached)的子對象,那么子對象將會(huì)被saveOrUpdate()
-
如果父對象被刪除,那么所有子對象也會(huì)被delete()
-
除非被標(biāo)記為cascade="delete-orphan"(刪除“孤兒”模式,此時(shí)不被任何一個(gè)父對象引用的子對象會(huì)被刪除), 否則子對象失掉父對象對其的引用時(shí),什么事也不會(huì)發(fā)生。 如果有特殊需要,應(yīng)用程序可通過顯式調(diào)用delete()刪除子對象。
Hibernate中有一個(gè)非常豐富的元級(jí)別(meta-level)的模型,含有所有的實(shí)體和值類型數(shù)據(jù)的元數(shù)據(jù)。 有時(shí)這個(gè)模型對應(yīng)用程序本身也會(huì)非常有用。 比如說,應(yīng)用程序可能在實(shí)現(xiàn)一種“智能”的深度拷貝算法時(shí), 通過使用Hibernate的元數(shù)據(jù)來了解哪些對象應(yīng)該被拷貝(比如,可變的值類型數(shù)據(jù)), 那些不應(yīng)該(不可變的值類型數(shù)據(jù),也許還有某些被關(guān)聯(lián)的實(shí)體)。
Hibernate提供了ClassMetadata接口,CollectionMetadata接口和Type層次體系來訪問元數(shù)據(jù)。 可以通過SessionFactory獲取元數(shù)據(jù)接口的實(shí)例。
Cat fritz = ......;
ClassMetadata catMeta = sessionfactory.getClassMetadata(Cat.class);
Object[] propertyValues = catMeta.getPropertyValues(fritz);
String[] propertyNames = catMeta.getPropertyNames();
Type[] propertyTypes = catMeta.getPropertyTypes();
// get a Map of all properties which are not collections or associations
Map namedValues = new HashMap();
for ( int i=0; i<propertyNames.length; i++ ) {
if ( !propertyTypes[i].isEntityType() && !propertyTypes[i].isCollectionType() ) {
namedValues.put( propertyNames[i], propertyValues[i] );
}
}
}
Hibernate的事務(wù)和并發(fā)控制很容易掌握。Hibernate直接使用JDBC連接和JTA資源,不添加任何附加鎖定 行為。我們強(qiáng)烈推薦你花點(diǎn)時(shí)間了解JDBC編程,ANSI SQL查詢語言和你使用 的數(shù)據(jù)庫系統(tǒng)的事務(wù)隔離規(guī)范。Hibernate只添加自動(dòng)版本管理,而不會(huì)鎖 定內(nèi)存中的對象,也不會(huì)改變數(shù)據(jù)庫事務(wù)的隔離級(jí)別。基本上,使用 Hibernate就好像直接使用JDBC(或者JTA/CMT)來訪問你的數(shù)據(jù)庫資源。
除了自動(dòng)版本管理,針對行級(jí)悲觀鎖定,Hibernate也提供了輔助的API,它使用了 SELECT FOR UPDATE的SQL語法。本章后面會(huì)討論這個(gè)API。
我們從Configuration層、SessionFactory層, 和 Session層開始討論Hibernate的并行控制、數(shù)據(jù)庫事務(wù)和應(yīng)用 程序的長事務(wù)。
12.1. Session和事務(wù)范圍(transaction scopes)
一個(gè)SessionFactory對象的創(chuàng)建代價(jià)很昂貴,它是線程安全的對象,它被設(shè)計(jì)成可以 為所有的應(yīng)用程序線程所共享。它只創(chuàng)建一次,通常是在應(yīng)用程序啟動(dòng)的時(shí)候,由一個(gè) Configuraion的實(shí)例來創(chuàng)建。
一個(gè)Session的對象是輕型的,非線程安全的,對于單個(gè)業(yè)務(wù)進(jìn)程,單個(gè)的 工作單元而言,它只被使用一次,然后就丟棄。只有在需要的時(shí)候,Session 才會(huì)獲取一個(gè)JDBC的Connection(或一個(gè)Datasource) 對象。所以你可以放心的打開和關(guān)閉Session,甚至當(dāng)你并不確定一個(gè)特定的請 求是否需要數(shù)據(jù)訪問時(shí),你也可以這樣做。(一旦你實(shí)現(xiàn)下面提到的使用了請求攔截的模式,這就 變得很重要了。
此外我們還要考慮數(shù)據(jù)庫事務(wù)。數(shù)據(jù)庫事務(wù)應(yīng)該盡可能的短,降低數(shù)據(jù)庫鎖定造成的資源爭用。 數(shù)據(jù)庫長事務(wù)會(huì)導(dǎo)致你的應(yīng)用程序無法擴(kuò)展到高的并發(fā)負(fù)載。
一個(gè)操作單元(Unit of work)的范圍是多大?單個(gè)的Hibernate Session能跨越多個(gè) 數(shù)據(jù)庫事務(wù)嗎?還是一個(gè)Session的作用范圍對應(yīng)一個(gè)數(shù)據(jù)庫事務(wù)的范圍?應(yīng)該何時(shí)打開 Session,何時(shí)關(guān)閉Session?,你又如何劃分?jǐn)?shù)據(jù)庫事務(wù)的邊界呢?
12.1.1. 操作單元(Unit of work)
首先,別再用session-per-operation這種反模式了,也就是說,在單個(gè)線程中, 不要因?yàn)橐淮魏唵蔚臄?shù)據(jù)庫調(diào)用,就打開和關(guān)閉一次Session!數(shù)據(jù)庫事務(wù)也是如此。 應(yīng)用程序中的數(shù)據(jù)庫調(diào)用是按照計(jì)劃好的次序,分組為原子的操作單元。(注意,這也意味著,應(yīng)用程 序中,在單個(gè)的SQL語句發(fā)送之后,自動(dòng)事務(wù)提交(auto-commit)模式失效了。這種模式專門為SQL控制臺(tái)操作設(shè)計(jì)的。 Hibernate禁止立即自動(dòng)事務(wù)提交模式,或者期望應(yīng)用服務(wù)器禁止立即自動(dòng)事務(wù)提交模式。)
在多用戶的client/server應(yīng)用程序中,最常用的模式是 每個(gè)請求一個(gè)會(huì)話(session-per-request)。 在這種模式下,來自客戶端的請求被發(fā)送到服務(wù)器端(即Hibernate持久化層運(yùn)行的地方),一 個(gè)新的Hibernate Session被打開,并且執(zhí)行這個(gè)操作單元中所有的數(shù)據(jù)庫操作。 一旦操作完成(同時(shí)發(fā)送到客戶端的響應(yīng)也準(zhǔn)備就緒),session被同步,然后關(guān)閉。你也可以使用單 個(gè)數(shù)據(jù)庫事務(wù)來處理客戶端請求,在你打開Session之后啟動(dòng)事務(wù),在你關(guān)閉 Session之前提交事務(wù)。會(huì)話和請求之間的關(guān)系是一對一的關(guān)系,這種模式對 于大多數(shù)應(yīng)用程序來說是很棒的。
真正的挑戰(zhàn)在于如何去實(shí)現(xiàn)這種模式:不僅Session和事務(wù)必須被正確的開始和結(jié)束, 而且他們也必須能被數(shù)據(jù)訪問操作訪問。用攔截器來實(shí)現(xiàn)操作單元的劃分,該攔截器在客戶端請求達(dá)到服 務(wù)器端的時(shí)候開始,在服務(wù)器端發(fā)送響應(yīng)(即,ServletFilter)之前結(jié)束。我們推薦 使用一個(gè)ThreadLocal 變量,把 Session綁定到處理客戶端請求的線 程上去。這種方式可以讓運(yùn)行在該線程上的所有程序代碼輕松的訪問Session(就像訪問一 個(gè)靜態(tài)變量那樣)。你也可以在一個(gè)ThreadLocal 變量中保持事務(wù)上下文環(huán)境,不過這依賴 于你所選擇的數(shù)據(jù)庫事務(wù)劃分機(jī)制。這種實(shí)現(xiàn)模式被稱之為 ThreadLocal Session和 Open Session in View。你可以很容易的擴(kuò)展本文前面章節(jié)展示的 HibernateUtil 輔助類來實(shí)現(xiàn)這種模式。當(dāng)然,你必須找到一種實(shí)現(xiàn)攔截器的方法,并 且可以把攔截器集成到你的應(yīng)用環(huán)境中。請參考Hibernate網(wǎng)站上面的提示和例子。
12.1.2. 應(yīng)用程序事務(wù)(Application transactions)
session-per-request模式不僅僅是一個(gè)可以用來設(shè)計(jì)操作單元的有用概念。很多業(yè)務(wù)處理流程都需 要一系列完整的和用戶之間的交互,即用戶對數(shù)據(jù)庫的交叉訪問。在基于web的應(yīng)用和企業(yè) 應(yīng)用中,跨用戶交互的數(shù)據(jù)庫事務(wù)是無法接受的。考慮下面的例子:
從用戶的角度來看,我們把這個(gè)操作單元稱為應(yīng)用程序長事務(wù)(application transaction)。 在你的應(yīng)用程序中,可以有很多種方法來實(shí)現(xiàn)它。
頭一個(gè)幼稚的做法是,在用戶思考的過程中,保持Session和數(shù)據(jù)庫事務(wù)是打開的, 保持?jǐn)?shù)據(jù)庫鎖定,以阻止并發(fā)修改,從而保證數(shù)據(jù)庫事務(wù)隔離級(jí)別和原子操作。這種方式當(dāng)然是一個(gè)反模式, 因?yàn)閿?shù)據(jù)庫鎖定的維持會(huì)導(dǎo)致應(yīng)用程序無法擴(kuò)展并發(fā)用戶的數(shù)目。
很明顯,我們必須使用多個(gè)數(shù)據(jù)庫事務(wù)來實(shí)現(xiàn)一個(gè)應(yīng)用程序事務(wù)。在這個(gè)例子中,維護(hù)業(yè)務(wù)處理流程的 事務(wù)隔離變成了應(yīng)用程序?qū)拥牟糠重?zé)任。單個(gè)應(yīng)用程序事務(wù)通常跨越多個(gè)數(shù)據(jù)庫事務(wù)。如果僅僅只有一 個(gè)數(shù)據(jù)庫事務(wù)(最后的那個(gè)事務(wù))保存更新過的數(shù)據(jù),而所有其他事務(wù)只是單純的讀取數(shù)據(jù)(例如在一 個(gè)跨越多個(gè)請求/響應(yīng)周期的向?qū)эL(fēng)格的對話框中),那么應(yīng)用程序事務(wù)將保證其原子性。這種方式比聽 起來還要容易實(shí)現(xiàn),特別是當(dāng)你使用了Hibernate的下述特性的時(shí)候:
-
自動(dòng)版本化 - Hibernate能夠自動(dòng)進(jìn)行樂觀并發(fā)控制 ,如果在用戶思考 的過程中發(fā)生并發(fā)修改沖突,Hibernate能夠自動(dòng)檢測到。
-
脫管對象(Detached Objects)- 如果你決定采用前面已經(jīng)討論過的 session-per-request模式,所有載入的實(shí)例在用戶思考的過程 中都處于與Session脫離的狀態(tài)。Hibernate允許你把與Session脫離的對象重新關(guān)聯(lián)到Session 上,并且對修改進(jìn)行持久化,這種模式被稱為 session-per-request-with-detached-objects。自動(dòng)版本化被用來隔離并發(fā)修改。
-
長生命周期的Session (Long Session)- Hibernate 的Session 可以在數(shù)據(jù)庫事務(wù)提交之后和底層的JDBC連接斷開,當(dāng)一個(gè)新的客戶端請求到來的時(shí)候,它又重新連接上底層的 JDBC連接。這種模式被稱之為session-per-application-transaction,這種情況可 能會(huì)造成不必要的Session和JDBC連接的重新關(guān)聯(lián)。自動(dòng)版本化被用來隔離并發(fā)修改。
session-per-request-with-detached-objects 和 session-per-application-transaction 各有優(yōu)缺點(diǎn),我們在本章后面樂觀并發(fā) 控制那部分再進(jìn)行討論。
12.1.3. 關(guān)注對象標(biāo)識(shí)(Considering object identity)
應(yīng)用程序可能在兩個(gè)不同的Session中并發(fā)訪問同一持久化狀態(tài),但是, 一個(gè)持久化類的實(shí)例無法在兩個(gè) Session中共享。因此有兩種不同的標(biāo)識(shí)語義:
- 數(shù)據(jù)庫標(biāo)識(shí)
-
foo.getId().equals( bar.getId() )
- JVM 標(biāo)識(shí)
-
foo==bar
對于那些關(guān)聯(lián)到 特定Session (也就是在單個(gè)Session的范圍內(nèi))上的對象來說,這 兩種標(biāo)識(shí)的語義是等價(jià)的,與數(shù)據(jù)庫標(biāo)識(shí)對應(yīng)的JVM標(biāo)識(shí)是由Hibernate來保 證的。不過,當(dāng)應(yīng)用程序在兩個(gè)不同的session中并發(fā)訪問具有同一持久化標(biāo) 識(shí)的業(yè)務(wù)對象實(shí)例的時(shí)候,這個(gè)業(yè)務(wù)對象的兩個(gè)實(shí)例事實(shí)上是不相同的(從 JVM識(shí)別來看)。這種沖突可以通過在同步和提交的時(shí)候使用自動(dòng)版本化和樂 觀鎖定方法來解決。
這種方式把關(guān)于并發(fā)的頭疼問題留給了Hibernate和數(shù)據(jù)庫;由于在單個(gè)線程內(nèi),操作單元中的對象識(shí)別不 需要代價(jià)昂貴的鎖定或其他意義上的同步,因此它同時(shí)可以提供最好的可伸縮性。只要在單個(gè)線程只持有一個(gè) Session,應(yīng)用程序就不需要同步任何業(yè)務(wù)對象。在Session 的范圍內(nèi),應(yīng)用程序可以放心的使用==進(jìn)行對象比較。
不過,應(yīng)用程序在Session的外面使用==進(jìn)行對象比較可能會(huì) 導(dǎo)致無法預(yù)期的結(jié)果。在一些無法預(yù)料的場合,例如,如果你把兩個(gè)脫管對象實(shí)例放進(jìn)同一個(gè) Set的時(shí)候,就可能發(fā)生。這兩個(gè)對象實(shí)例可能有同一個(gè)數(shù)據(jù)庫標(biāo)識(shí)(也就是說, 他們代表了表的同一行數(shù)據(jù)),從JVM標(biāo)識(shí)的定義上來說,對脫管的對象而言,Hibernate無法保證他們 的的JVM標(biāo)識(shí)一致。開發(fā)人員必須覆蓋持久化類的equals()方法和 hashCode() 方法,從而實(shí)現(xiàn)自定義的對象相等語義。警告:不要使用數(shù)據(jù)庫標(biāo)識(shí) 來實(shí)現(xiàn)對象相等,應(yīng)該使用業(yè)務(wù)鍵值,由唯一的,通常不變的屬性組成。當(dāng)一個(gè)瞬時(shí)對象被持久化的時(shí) 候,它的數(shù)據(jù)庫標(biāo)識(shí)會(huì)發(fā)生改變。如果一個(gè)瞬時(shí)對象(通常也包括脫管對象實(shí)例)被放入一 個(gè)Set,改變它的hashcode會(huì)導(dǎo)致與這個(gè)Set的關(guān)系中斷。雖 然業(yè)務(wù)鍵值的屬性不象數(shù)據(jù)庫主鍵那樣穩(wěn)定不變,但是你只需要保證在同一個(gè)Set 中的對象屬性的穩(wěn)定性就足夠了。請到Hibernate網(wǎng)站去尋求這個(gè)問題更多的詳細(xì)的討論。請注意,這不是一 個(gè)有關(guān)Hibernate的問題,而僅僅是一個(gè)關(guān)于Java對象標(biāo)識(shí)和判等行為如何實(shí)現(xiàn)的問題。
決不要使用反模式session-per-user-session或者 session-per-application(當(dāng)然,這個(gè)規(guī)定幾乎沒有例外)。請注意, 下述一些問題可能也會(huì)出現(xiàn)在我們推薦的模式中,在你作出某個(gè)設(shè)計(jì)決定之前,請務(wù)必理解該模式的應(yīng)用前提。
-
Session 是一個(gè)非線程安全的類。如果一個(gè)Session 實(shí)例允許共享的話,那些支持并發(fā)運(yùn)行的東東,例如HTTP request,session beans,或者是 Swing workers,將會(huì)導(dǎo)致出現(xiàn)資源爭用(race condition)。如果在HttpSession中有 Hibernate 的Session的話(稍后討論),你應(yīng)該考慮同步訪問你的Http session。 否則,只要用戶足夠快的點(diǎn)擊瀏覽器的“刷新”,就會(huì)導(dǎo)致兩個(gè)并發(fā)運(yùn)行線程使用同一個(gè) Session。
-
一個(gè)由Hibernate拋出的異常意味著你必須立即回滾數(shù)據(jù)庫事務(wù),并立即關(guān)閉Session (稍后會(huì)展開討論)。如果你的Session綁定到一個(gè)應(yīng)用程序上,你必 須停止該應(yīng)用程序。回滾數(shù)據(jù)庫事務(wù)并不會(huì)把你的業(yè)務(wù)對象退回到事務(wù)啟動(dòng)時(shí)候的狀態(tài)。這 意味著數(shù)據(jù)庫狀態(tài)和業(yè)務(wù)對象狀態(tài)不同步。通常情況下,這不是什么問題,因?yàn)楫惓J遣豢?恢復(fù)的,你必須在回滾之后重新開始執(zhí)行。
-
Session 緩存了處于持久化狀態(tài)的每個(gè)對象(Hibernate會(huì)監(jiān)視和檢查臟數(shù)據(jù))。 這意味著,如果你讓Session打開很長一段時(shí)間,或是僅僅載入了過多的數(shù)據(jù), Session占用的內(nèi)存會(huì)一直增長,直到拋出OutOfMemoryException異常。這個(gè) 問題的一個(gè)解決方法是調(diào)用clear() 和evict()來管理 Session的緩存,但是如果你需要大批量數(shù)據(jù)操作的話,最好考慮 使用存儲(chǔ)過程。在第 14 章 批量處理(Batch processing)中有一些解決方案。在用戶會(huì)話期間一直保持 Session打開也意味著出現(xiàn)臟數(shù)據(jù)的可能性很高。
12.2. 數(shù)據(jù)庫事務(wù)聲明
數(shù)據(jù)庫(或者系統(tǒng))事務(wù)的聲明總是必須的。在數(shù)據(jù)庫事務(wù)之外,就無法和數(shù)據(jù)庫通訊(這可能會(huì)讓那些習(xí)慣于 自動(dòng)提交事務(wù)模式的開發(fā)人員感到迷惑)。永遠(yuǎn)使用清晰的事務(wù)聲明,即使只讀操作也是如此。進(jìn)行 顯式的事務(wù)聲明并不總是需要的,這取決于你的事務(wù)隔離級(jí)別和數(shù)據(jù)庫的能力,但不管怎么說,聲明事務(wù)總歸有益無害。
一個(gè)Hibernate應(yīng)用程序可以運(yùn)行在非托管環(huán)境中(也就是獨(dú)立運(yùn)行的應(yīng)用程序,簡單Web應(yīng)用程序, 或者Swing圖形桌面應(yīng)用程序),也可以運(yùn)行在托管的J2EE環(huán)境中。在一個(gè)非托管環(huán)境中,Hibernate 通常自己負(fù)責(zé)管理數(shù)據(jù)庫連接池。應(yīng)用程序開發(fā)人員必須手工設(shè)置事務(wù)聲明,換句話說,就是手工啟 動(dòng),提交,或者回滾數(shù)據(jù)庫事務(wù)。一個(gè)托管的環(huán)境通常提供了容器管理事務(wù),例如事務(wù)裝配通過可聲 明的方式定義在EJB session beans的部署描述符中。可編程式事務(wù)聲明不再需要,即使是 Session 的同步也可以自動(dòng)完成。
讓持久層具備可移植性是人們的理想。Hibernate提供了一套稱為Transaction的封裝API, 用來把你的部署環(huán)境中的本地事務(wù)管理系統(tǒng)轉(zhuǎn)換到Hibernate事務(wù)上。這個(gè)API是可選的,但是我們強(qiáng)烈 推薦你使用,除非你用CMT session bean。
通常情況下,結(jié)束 Session 包含了四個(gè)不同的階段:
-
同步session(flush,刷出到磁盤)
-
提交事務(wù)
-
關(guān)閉session
-
處理異常
session的同步(flush,刷出)前面已經(jīng)討論過了,我們現(xiàn)在進(jìn)一步考察在托管和非托管環(huán)境下的事務(wù)聲明和異常處理。
如果Hibernat持久層運(yùn)行在一個(gè)非托管環(huán)境中,數(shù)據(jù)庫連接通常由Hibernate的連接池機(jī)制 來處理。session/transaction處理方式如下所示:
//Non-managed environment idiom
Session sess = factory.openSession();
Transaction tx = null;
try {
tx = sess.beginTransaction();
// do some work
...
tx.commit();
}
catch (RuntimeException e) {
if (tx != null) tx.rollback();
throw e; // or display error message
}
finally {
sess.close();
}
你不需要顯式flush() Session - 對commit()的調(diào)用會(huì)自動(dòng)觸發(fā)session的同步。
調(diào)用 close() 標(biāo)志session的結(jié)束。 close()方法重要的暗示是,session釋放了JDBC連接。
這段Java代碼是可移植的,可以在非托管環(huán)境和JTA環(huán)境中運(yùn)行。
你很可能從未在一個(gè)標(biāo)準(zhǔn)的應(yīng)用程序的業(yè)務(wù)代碼中見過這樣的用法;致命的(系統(tǒng))異常應(yīng)該總是 在應(yīng)用程序“頂層”被捕獲。換句話說,執(zhí)行Hibernate調(diào)用的代碼(在持久層)和處理 RuntimeException異常的代碼(通常只能清理和退出應(yīng)用程序)應(yīng)該在不同 的應(yīng)用程序邏輯層。這對于你設(shè)計(jì)自己的軟件系統(tǒng)來說是一個(gè)挑戰(zhàn),只要有可能,你就應(yīng)該使用 J2EE/EJB容器服務(wù)。異常處理將在本章稍后進(jìn)行討論。
請注意,你應(yīng)該選擇 org.hibernate.transaction.JDBCTransactionFactory (這是默認(rèn)選項(xiàng)).
如果你的持久層運(yùn)行在一個(gè)應(yīng)用服務(wù)器中(例如,在EJB session beans的后面),Hibernate獲取 的每個(gè)數(shù)據(jù)源連接將自動(dòng)成為全局JTA事務(wù)的一部分。Hibernate提供了兩種策略進(jìn)行JTA集成。
如果你使用bean管理事務(wù)(BMT),可以通過使用Hibernate的 Transaction API來告訴 應(yīng)用服務(wù)器啟動(dòng)和結(jié)束BMT事務(wù)。因此,事務(wù)管理代碼和在非托管環(huán)境下是一樣的。
// BMT idiom
Session sess = factory.openSession();
Transaction tx = null;
try {
tx = sess.beginTransaction();
// do some work
...
tx.commit();
}
catch (RuntimeException e) {
if (tx != null) tx.rollback();
throw e; // or display error message
}
finally {
sess.close();
}
在CMT方式下,事務(wù)聲明是在session bean的部署描述符中,而不需要編程。 除非你設(shè)置了屬性hibernate.transaction.flush_before_completion和 hibernate.transaction.auto_close_session為true, 否則你必須自己同步和關(guān)閉Session。Hibernate可以為你自動(dòng)同步和關(guān)閉 Session。你唯一要做的就是當(dāng)發(fā)生異常時(shí)進(jìn)行事務(wù)回滾。幸運(yùn)的是, 在一個(gè)CMT bean中,事務(wù)回滾甚至可以由容器自動(dòng)進(jìn)行,因?yàn)橛蓅ession bean方法拋出的未處理的 RuntimeException異常可以通知容器設(shè)置全局事務(wù)回滾。這意味著 在CMT中,你完全無需使用Hibernate的Transaction API 。
請注意,當(dāng)你配置Hibernate事務(wù)工廠的時(shí)候,在一個(gè)BMT session bean中,你應(yīng)該選擇 org.hibernate.transaction.JTATransactionFactory,在一個(gè) CMT session bean中選擇org.hibernate.transaction.CMTTransactionFactory。 記住,同時(shí)也要設(shè)置org.hibernate.transaction.manager_lookup_class。
如果你使用CMT環(huán)境,并且讓容器自動(dòng)同步和關(guān)閉session,你可能也希望在你代碼的不同部分使用 同一個(gè)session。一般來說,在一個(gè)非托管環(huán)境中,你可以使用一個(gè)ThreadLocal 變量來持有這個(gè)session,但是單個(gè)EJB方法調(diào)用可能會(huì)在不同的線程中執(zhí)行(舉例來說,一個(gè)session bean調(diào)用另一個(gè)session bean)。如果你不想在應(yīng)用代碼中被傳遞Session對 象實(shí)例的問題困擾的話,那么SessionFactory 提供的 getCurrentSession()方法就很適合你,該方法返回一個(gè)綁定到JTA事務(wù) 上下文環(huán)境中的session實(shí)例。這也是把Hibernate集成到一個(gè)應(yīng)用程序中的最簡單的方法!這個(gè)“當(dāng) 前的”session總是可以自動(dòng)同步和自動(dòng)關(guān)閉(不考慮上述的屬性設(shè)置)。我們的session/transaction 管理代碼減少到如下所示:
// CMT idiom
Session sess = factory.getCurrentSession();
// do some work
...
換句話來說,在一個(gè)托管環(huán)境下,你要做的所有的事情就是調(diào)用 SessionFactory.getCurrentSession(),然后進(jìn)行你的數(shù)據(jù)訪問,把其余的工作 交給容器來做。事務(wù)在你的session bean的部署描述符中以可聲明的方式來設(shè)置。session的生命周期完全 由Hibernate來管理。
對after_statement連接釋放方式有一個(gè)警告。因?yàn)镴TA規(guī)范的一個(gè)很愚蠢的限制,Hibernate不可能自動(dòng)清理任何未關(guān)閉的ScrollableResults 或者Iterator,它們是由scroll()或iterate()產(chǎn)生的。你must通過在finally塊中,顯式調(diào)用ScrollableResults.close()或者Hibernate.close(Iterator)方法來釋放底層數(shù)據(jù)庫游標(biāo)。(當(dāng)然,大部分程序完全可以很容易的避免在CMT代碼中出現(xiàn)scroll()或iterate()。)
如果 Session 拋出異常 (包括任何SQLException), 你應(yīng)該立即回滾數(shù)據(jù)庫事務(wù),調(diào)用 Session.close() ,丟棄該 Session實(shí)例。Session的某些方法可能會(huì)導(dǎo)致session 處于不一致的狀態(tài)。所有由Hibernate拋出的異常都視為不可以恢復(fù)的。確保在 finally 代碼塊中調(diào)用close()方法,以關(guān)閉掉 Session。
HibernateException是一個(gè)非檢查期異常(這不同于Hibernate老的版本), 它封裝了Hibernate持久層可能出現(xiàn)的大多數(shù)錯(cuò)誤。我們的觀點(diǎn)是,不應(yīng)該強(qiáng)迫應(yīng)用程序開發(fā)人員 在底層捕獲無法恢復(fù)的異常。在大多數(shù)軟件系統(tǒng)中,非檢查期異常和致命異常都是在相應(yīng)方法調(diào)用 的堆棧的頂層被處理的(也就是說,在軟件上面的邏輯層),并且提供一個(gè)錯(cuò)誤信息給應(yīng)用軟件的用戶 (或者采取其他某些相應(yīng)的操作)。請注意,Hibernate也有可能拋出其他并不屬于 HibernateException的非檢查期異常。這些異常同樣也是無法恢復(fù)的,應(yīng)該 采取某些相應(yīng)的操作去處理。
在和數(shù)據(jù)庫進(jìn)行交互時(shí),Hibernate把捕獲的SQLException封裝為Hibernate的 JDBCException。事實(shí)上,Hibernate嘗試把異常轉(zhuǎn)換為更有實(shí)際含義 的JDBCException異常的子類。底層的SQLException可以 通過JDBCException.getCause()來得到。Hibernate通過使用關(guān)聯(lián)到 SessionFactory上的SQLExceptionConverter來 把SQLException轉(zhuǎn)換為一個(gè)對應(yīng)的JDBCException 異常的子類。默認(rèn)情況下,SQLExceptionConverter可以通過配置dialect 選項(xiàng)指定;此外,也可以使用用戶自定義的實(shí)現(xiàn)類(參考javadocs SQLExceptionConverterFactory類來了解詳情)。標(biāo)準(zhǔn)的 JDBCException子類型是:
-
JDBCConnectionException - 指明底層的JDBC通訊出現(xiàn)錯(cuò)誤
-
SQLGrammarException - 指明發(fā)送的SQL語句的語法或者格式錯(cuò)誤
-
ConstraintViolationException - 指明某種類型的約束違例錯(cuò)誤
-
LockAcquisitionException - 指明了在執(zhí)行請求操作時(shí),獲取 所需的鎖級(jí)別時(shí)出現(xiàn)的錯(cuò)誤。
-
GenericJDBCException - 不屬于任何其他種類的原生異常
12.3. 樂觀并發(fā)控制(Optimistic concurrency control)
唯一能夠同時(shí)保持高并發(fā)和高可伸縮性的方法就是使用帶版本化的樂觀并發(fā)控制。版本檢查使用版本號(hào)、 或者時(shí)間戳來檢測更新沖突(并且防止更新丟失)。Hibernate為使用樂觀并發(fā)控制的代碼提供了三種可 能的方法,應(yīng)用程序在編寫這些代碼時(shí),可以采用它們。我們已經(jīng)在前面應(yīng)用程序長事務(wù)那部分展示了 樂觀并發(fā)控制的應(yīng)用場景,此外,在單個(gè)數(shù)據(jù)庫事務(wù)范圍內(nèi),版本檢查也提供了防止更新丟失的好處。
12.3.1. 應(yīng)用程序級(jí)別的版本檢查(Application version checking)
未能充分利用Hibernate功能的實(shí)現(xiàn)代碼中,每次和數(shù)據(jù)庫交互都需要一個(gè)新的 Session,而且開發(fā)人員必須在顯示數(shù)據(jù)之前從數(shù)據(jù)庫中重 新載入所有的持久化對象實(shí)例。這種方式迫使應(yīng)用程序自己實(shí)現(xiàn)版本檢查來確保 應(yīng)用程序事務(wù)的隔離,從數(shù)據(jù)訪問的角度來說是最低效的。這種使用方式和 entity EJB最相似。
// foo is an instance loaded by a previous Session
session = factory.openSession();
Transaction t = session.beginTransaction();
int oldVersion = foo.getVersion();
session.load( foo, foo.getKey() ); // load the current state
if ( oldVersion!=foo.getVersion ) throw new StaleObjectStateException();
foo.setProperty("bar");
t.commit();
session.close();
version 屬性使用 <version>來映射,如果對象 是臟數(shù)據(jù),在同步的時(shí)候,Hibernate會(huì)自動(dòng)增加版本號(hào)。
當(dāng)然,如果你的應(yīng)用是在一個(gè)低數(shù)據(jù)并發(fā)環(huán)境下,并不需要版本檢查的話,你照樣可以使用 這種方式,只不過跳過版本檢查就是了。在這種情況下,最晚提交生效 (last commit wins)就是你的應(yīng)用程序長事務(wù)的默認(rèn)處理策略。 請記住這種策略可能會(huì)讓應(yīng)用軟件的用戶感到困惑,因?yàn)樗麄冇锌赡軙?huì)碰上更新丟失掉卻沒 有出錯(cuò)信息,或者需要合并更改沖突的情況。
很明顯,手工進(jìn)行版本檢查只適合于某些軟件規(guī)模非常小的應(yīng)用場景,對于大多數(shù)軟件應(yīng)用場景 來說并不現(xiàn)實(shí)。通常情況下,不僅是單個(gè)對象實(shí)例需要進(jìn)行版本檢查,整個(gè)被修改過的關(guān) 聯(lián)對象圖也都需要進(jìn)行版本檢查。作為標(biāo)準(zhǔn)設(shè)計(jì)范例,Hibernate使用長生命周期 Session的方式,或者脫管對象實(shí)例的方式來提供自動(dòng)版本檢查。
12.3.2. 長生命周期session和自動(dòng)版本化
單個(gè) Session實(shí)例和它所關(guān)聯(lián)的所有持久化對象實(shí)例都被用于整個(gè) 應(yīng)用程序事務(wù)。Hibernate在同步的時(shí)候進(jìn)行對象實(shí)例的版本檢查,如果檢測到并發(fā)修 改則拋出異常。由開發(fā)人員來決定是否需要捕獲和處理這個(gè)異常(通常的抉擇是給用戶 提供一個(gè)合并更改,或者在無臟數(shù)據(jù)情況下重新進(jìn)行業(yè)務(wù)操作的機(jī)會(huì))。
在等待用戶交互的時(shí)候, Session 斷開底層的JDBC連接。這種方式 以數(shù)據(jù)庫訪問的角度來說是最高效的方式。應(yīng)用程序不需要關(guān)心版本檢查或脫管對象實(shí)例 的重新關(guān)聯(lián),在每個(gè)數(shù)據(jù)庫事務(wù)中,應(yīng)用程序也不需要載入讀取對象實(shí)例。
// foo is an instance loaded earlier by the Session
session.reconnect(); // Obtain a new JDBC connection
Transaction t = session.beginTransaction();
foo.setProperty("bar");
t.commit(); // End database transaction, flushing the change and checking the version
session.disconnect(); // Return JDBC connection
foo 對象始終和載入它的Session相關(guān)聯(lián)。 Session.reconnect()獲取一個(gè)新的數(shù)據(jù)庫連接(或者 你可以提供一個(gè)),并且繼續(xù)當(dāng)前的session。Session.disconnect() 方法把session與JDBC連接斷開,把數(shù)據(jù)庫連接返回到連接池(除非是你自己提供的數(shù)據(jù) 庫連接)。在Session重新連接上數(shù)據(jù)庫連接之后,你可以對任何可能被其他事務(wù)更新過 的對象調(diào)用Session.lock(),設(shè)置LockMode.READ 鎖定模式,這樣你就可以對那些你不準(zhǔn)備更新的數(shù)據(jù)進(jìn)行強(qiáng)制版本檢查。此外,你并不需要 鎖定那些你準(zhǔn)備更新的數(shù)據(jù)。
假若對disconnect()和reconnect()的顯式調(diào)用發(fā)生得太頻繁了,你可以使用hibernate.connection.release_mode來代替。
如果在用戶思考的過程中,Session因?yàn)樘罅硕荒鼙4妫敲催@種模式是有 問題的。舉例來說,一個(gè)HttpSession應(yīng)該盡可能的小。由于 Session是一級(jí)緩存,并且保持了所有被載入過的對象,因此 我們只應(yīng)該在那些少量的request/response情況下使用這種策略。而且在這種情況下, Session 里面很快就會(huì)有臟數(shù)據(jù)出現(xiàn),因此請牢牢記住這一建議。
此外,也請注意,你應(yīng)該讓與數(shù)據(jù)庫連接斷開的Session對持久層保持 關(guān)閉狀態(tài)。換句話說,使用有狀態(tài)的EJB session bean來持有Session, 而不要把它傳遞到web層(甚至把它序列化到一個(gè)單獨(dú)的層),保存在HttpSession中。
12.3.3. 脫管對象(deatched object)和自動(dòng)版本化
這種方式下,與持久化存儲(chǔ)的每次交互都發(fā)生在一個(gè)新的Session中。 然而,同一持久化對象實(shí)例可以在多次與數(shù)據(jù)庫的交互中重用。應(yīng)用程序操縱脫管對象實(shí)例 的狀態(tài),這個(gè)脫管對象實(shí)例最初是在另一個(gè)Session 中載入的,然后 調(diào)用 Session.update(),Session.saveOrUpdate(), 或者 Session.merge() 來重新關(guān)聯(lián)該對象實(shí)例。
// foo is an instance loaded by a previous Session
foo.setProperty("bar");
session = factory.openSession();
Transaction t = session.beginTransaction();
session.saveOrUpdate(foo); // Use merge() if "foo" might have been loaded already
t.commit();
session.close();
Hibernate會(huì)再一次在同步的時(shí)候檢查對象實(shí)例的版本,如果發(fā)生更新沖突,就拋出異常。
如果你確信對象沒有被修改過,你也可以調(diào)用lock() 來設(shè)置 LockMode.READ(繞過所有的緩存,執(zhí)行版本檢查),從而取 代 update()操作。
對于特定的屬性和集合,通過為它們設(shè)置映射屬性optimistic-lock的值 為false,來禁止Hibernate的版本自動(dòng)增加。這樣的話,如果該屬性 臟數(shù)據(jù),Hibernate將不再增加版本號(hào)。
遺留系統(tǒng)的數(shù)據(jù)庫Schema通常是靜態(tài)的,不可修改的。或者,其他應(yīng)用程序也可能訪問同一數(shù)據(jù) 庫,根本無法得知如何處理版本號(hào),甚至?xí)r間戳。在以上的所有場景中,實(shí)現(xiàn)版本化不能依靠 數(shù)據(jù)庫表的某個(gè)特定列。在<class>的映射中設(shè)置 optimistic-lock="all"可以在沒有版本或者時(shí)間戳屬性映射的情況下實(shí)現(xiàn) 版本檢查,此時(shí)Hibernate將比較一行記錄的每個(gè)字段的狀態(tài)。請注意,只有當(dāng)Hibernate能夠比 較新舊狀態(tài)的情況下,這種方式才能生效,也就是說, 你必須使用單個(gè)長生命周期Session模式,而不能使用 session-per-request-with-detached-objects模式。
有些情況下,只要更改不發(fā)生交錯(cuò),并發(fā)修改也是允許的。當(dāng)你在<class> 的映射中設(shè)置optimistic-lock="dirty",Hibernate在同步的時(shí)候?qū)⒅槐容^有臟 數(shù)據(jù)的字段。
在以上所有場景中,不管是專門設(shè)置一個(gè)版本/時(shí)間戳列,還是進(jìn)行全部字段/臟數(shù)據(jù)字段比較, Hibernate都會(huì)針對每個(gè)實(shí)體對象發(fā)送一條UPDATE(帶有相應(yīng)的 WHERE語句 )的SQL語句來執(zhí)行版本檢查和數(shù)據(jù)更新。如果你對關(guān)聯(lián)實(shí)體 設(shè)置級(jí)聯(lián)關(guān)系使用傳播性持久化(transitive persistence),那么Hibernate可能會(huì)執(zhí)行不必 要的update語句。這通常不是個(gè)問題,但是數(shù)據(jù)庫里面對on update點(diǎn)火 的觸發(fā)器可能在脫管對象沒有任何更改的情況下被觸發(fā)。因此,你可以在 <class>的映射中,通過設(shè)置select-before-update="true" 來定制這一行為,強(qiáng)制Hibernate SELECT這個(gè)對象實(shí)例,從而保證, 在更新記錄之前,對象的確是被修改過。
12.4. 悲觀鎖定(Pessimistic Locking)
用戶其實(shí)并不需要花很多精力去擔(dān)心鎖定策略的問題。通常情況下,只要為JDBC連接指定一下隔 離級(jí)別,然后讓數(shù)據(jù)庫去搞定一切就夠了。然而,高級(jí)用戶有時(shí)候希望進(jìn)行一個(gè)排它的悲觀鎖定, 或者在一個(gè)新的事務(wù)啟動(dòng)的時(shí)候,重新進(jìn)行鎖定。
Hibernate總是使用數(shù)據(jù)庫的鎖定機(jī)制,從不在內(nèi)存中鎖定對象!
類LockMode 定義了Hibernate所需的不同的鎖定級(jí)別。一個(gè)鎖定 可以通過以下的機(jī)制來設(shè)置:
-
當(dāng)Hibernate更新或者插入一行記錄的時(shí)候,鎖定級(jí)別自動(dòng)設(shè)置為LockMode.WRITE。
-
當(dāng)用戶顯式的使用數(shù)據(jù)庫支持的SQL格式SELECT ... FOR UPDATE 發(fā)送SQL的時(shí)候,鎖定級(jí)別設(shè)置為LockMode.UPGRADE
-
當(dāng)用戶顯式的使用Oracle數(shù)據(jù)庫的SQL語句SELECT ... FOR UPDATE NOWAIT 的時(shí)候,鎖定級(jí)別設(shè)置LockMode.UPGRADE_NOWAIT
-
當(dāng)Hibernate在“可重復(fù)讀”或者是“序列化”數(shù)據(jù)庫隔離級(jí)別下讀取數(shù)據(jù)的時(shí)候,鎖定模式 自動(dòng)設(shè)置為LockMode.READ。這種模式也可以通過用戶顯式指定進(jìn)行設(shè)置。
-
LockMode.NONE 代表無需鎖定。在Transaction結(jié)束時(shí), 所有的對象都切換到該模式上來。與session相關(guān)聯(lián)的對象通過調(diào)用update() 或者saveOrUpdate()脫離該模式。
"顯式的用戶指定"可以通過以下幾種方式之一來表示:
如果在UPGRADE或者UPGRADE_NOWAIT鎖定模式下調(diào) 用Session.load(),并且要讀取的對象尚未被session載入過,那么對象 通過SELECT ... FOR UPDATE這樣的SQL語句被載入。如果為一個(gè)對象調(diào)用 load()方法時(shí),該對象已經(jīng)在另一個(gè)較少限制的鎖定模式下被載入了,那 么Hibernate就對該對象調(diào)用lock() 方法。
如果指定的鎖定模式是READ, UPGRADE 或 UPGRADE_NOWAIT,那么Session.lock()就 執(zhí)行版本號(hào)檢查。(在UPGRADE 或者UPGRADE_NOWAIT 鎖定模式下,執(zhí)行SELECT ... FOR UPDATE這樣的SQL語句。)
如果數(shù)據(jù)庫不支持用戶設(shè)置的鎖定模式,Hibernate將使用適當(dāng)?shù)奶娲J剑ǘ皇侨映霎惓#?這一點(diǎn)可以確保應(yīng)用程序的可移植性。
攔截器與事件(Interceptors and events)
應(yīng)用程序能夠響應(yīng)Hibernate內(nèi)部產(chǎn)生的特定事件是非常有用的。這樣就允許實(shí)現(xiàn)某些通用的功能 以及允許對Hibernate功能進(jìn)行擴(kuò)展。
Interceptor接口提供了從會(huì)話(session)回調(diào)(callback)應(yīng)用程序(application)的機(jī)制, 這種回調(diào)機(jī)制可以允許應(yīng)用程序在持久化對象被保存、更新、刪除或是加載之前,檢查并(或)修改其 屬性。一個(gè)可能的用途,就是用來跟蹤審核(auditing)信息。例如:下面的這個(gè)攔截器,會(huì)在一個(gè)實(shí)現(xiàn)了 Auditable接口的對象被創(chuàng)建時(shí)自動(dòng)地設(shè)置createTimestamp屬性,并在實(shí)現(xiàn)了 Auditable接口的對象被更新時(shí),同步更新lastUpdateTimestamp屬性。
package org.hibernate.test;
import java.io.Serializable;
import java.util.Date;
import java.util.Iterator;
import org.hibernate.Interceptor;
import org.hibernate.type.Type;
public class AuditInterceptor implements Interceptor, Serializable {
private int updates;
private int creates;
public void onDelete(Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
// do nothing
}
public boolean onFlushDirty(Object entity,
Serializable id,
Object[] currentState,
Object[] previousState,
String[] propertyNames,
Type[] types) {
if ( entity instanceof Auditable ) {
updates++;
for ( int i=0; i < propertyNames.length; i++ ) {
if ( "lastUpdateTimestamp".equals( propertyNames[i] ) ) {
currentState[i] = new Date();
return true;
}
}
}
return false;
}
public boolean onLoad(Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
return false;
}
public boolean onSave(Object entity,
Serializable id,
Object[] state,
String[] propertyNames,
Type[] types) {
if ( entity instanceof Auditable ) {
creates++;
for ( int i=0; i<propertyNames.length; i++ ) {
if ( "createTimestamp".equals( propertyNames[i] ) ) {
state[i] = new Date();
return true;
}
}
}
return false;
}
public void postFlush(Iterator entities) {
System.out.println("Creations: " + creates + ", Updates: " + updates);
}
public void preFlush(Iterator entities) {
updates=0;
creates=0;
}
...
}
創(chuàng)建會(huì)話(session)的時(shí)候可以指定攔截器。
Session session = sf.openSession( new AuditInterceptor() );
你也可以使用Configuration來設(shè)置一個(gè)全局范圍的攔截器。
new Configuration().setInterceptor( new AuditInterceptor() );
13.2. 事件系統(tǒng)(Event system)
如果需要響應(yīng)持久層的某些特殊事件,你也可以使用Hibernate3的事件框架。 該事件系統(tǒng)可以用來替代攔截器,也可以作為攔截器的補(bǔ)充來使用。
基本上,Session接口的每個(gè)方法都有相對應(yīng)的事件。比如 LoadEvent,FlushEvent,等等(查閱XML配置文件 的DTD,以及org.hibernate.event包來獲得所有已定義的事件的列表)。當(dāng)某個(gè)方 法被調(diào)用時(shí),Hibernate Session會(huì)生成一個(gè)相對應(yīng)的事件并激活所 有配置好的事件監(jiān)聽器。系統(tǒng)預(yù)設(shè)的監(jiān)聽器實(shí)現(xiàn)的處理過程就是被監(jiān)聽的方法要做的(被監(jiān)聽的方法所做的其實(shí)僅僅是激活監(jiān)聽器, “實(shí)際”的工作是由監(jiān)聽器完成的)。不過,你可以自由地選擇實(shí)現(xiàn) 一個(gè)自己定制的監(jiān)聽器(比如,實(shí)現(xiàn)并注冊用來處理處理LoadEvent的LoadEventListener接口), 來負(fù)責(zé)處理所有的調(diào)用Session的load()方法的請求。
監(jiān)聽器應(yīng)該被看作是單例(singleton)對象,也就是說,所有同類型的事件的處理共享同一個(gè)監(jiān)聽器實(shí)例,因此監(jiān)聽器 不應(yīng)該保存任何狀態(tài)(也就是不應(yīng)該使用成員變量)。
用戶定制的監(jiān)聽器應(yīng)該實(shí)現(xiàn)與所要處理的事件相對應(yīng)的接口,或者從一個(gè)合適的基類繼承(甚至是從Hibernate自帶的默認(rèn)事件監(jiān)聽器類繼承, 為了方便你這樣做,這些類都被聲明成non-final的了)。用戶定制的監(jiān)聽器可以通過編程使用Configuration對象 來注冊,也可以在Hibernate的XML格式的配置文件中進(jìn)行聲明(不支持在Properties格式的配置文件聲明監(jiān)聽器)。 下面是一個(gè)用戶定制的加載事件(load event)的監(jiān)聽器:
public class MyLoadListener extends DefaultLoadEventListener {
// this is the single method defined by the LoadEventListener interface
public Object onLoad(LoadEvent event, LoadEventListener.LoadType loadType)
throws HibernateException {
if ( !MySecurity.isAuthorized( event.getEntityClassName(), event.getEntityId() ) ) {
throw MySecurityException("Unauthorized access");
}
return super.onLoad(event, loadType);
}
}
你還需要修改一處配置,來告訴Hibernate以使用選定的監(jiān)聽器來替代默認(rèn)的監(jiān)聽器。
<hibernate-configuration>
<session-factory>
...
<listener type="load" class="MyLoadListener"/>
</session-factory>
</hibernate-configuration>
看看用另一種方式,通過編程的方式來注冊它。
Configuration cfg = new Configuration();
cfg.getSessionEventListenerConfig().setLoadEventListener( new MyLoadListener() );
通過在XML配置文件聲明而注冊的監(jiān)聽器不能共享實(shí)例。如果在多個(gè)<listener/>節(jié)點(diǎn)中使用 了相同的類的名字,則每一個(gè)引用都將會(huì)產(chǎn)生一個(gè)獨(dú)立的實(shí)例。如果你需要在多個(gè)監(jiān)聽器類型之間共享 監(jiān)聽器的實(shí)例,則你必須使用編程的方式來進(jìn)行注冊。
為什么我們實(shí)現(xiàn)了特定監(jiān)聽器的接口,在注冊的時(shí)候還要明確指出我們要注冊哪個(gè)事件的監(jiān)聽器呢? 這是因?yàn)橐粋€(gè)類可能實(shí)現(xiàn)多個(gè)監(jiān)聽器的接口。在注冊的時(shí)候明確指定要監(jiān)聽的事件,可以讓啟用或者禁用對某個(gè)事件的監(jiān)聽的配置工作簡單些。
13.3. Hibernate的聲明式安全機(jī)制
通常,Hibernate應(yīng)用程序的聲明式安全機(jī)制由會(huì)話外觀層(session facade)所管理。 現(xiàn)在,Hibernate3允許某些特定的行為由JACC進(jìn)行許可管理,由JAAS進(jìn)行授權(quán)管理。 本功能是一個(gè)建立在事件框架之上的可選的功能。
首先,你必須要配置適當(dāng)?shù)氖录O(jiān)聽器(event listener),來激活使用JAAS管理授權(quán)的功能。
<listener type="pre-delete" class="org.hibernate.secure.JACCPreDeleteEventListener"/>
<listener type="pre-update" class="org.hibernate.secure.JACCPreUpdateEventListener"/>
<listener type="pre-insert" class="org.hibernate.secure.JACCPreInsertEventListener"/>
<listener type="pre-load" class="org.hibernate.secure.JACCPreLoadEventListener"/>
接下來,仍然在hibernate.cfg.xml文件中,綁定角色的權(quán)限:
<grant role="admin" entity-name="User" actions="insert,update,read"/>
<grant role="su" entity-name="User" actions="*"/>
這些角色的名字就是你的JACC provider所定義的角色的名字。