Fetching strategies(取策略)
Fetching stategies是指hibernate在需要關聯數據的時候所采用的取關聯數據的策略。這個策略既可以在O/R映射文件里配,也可以通過特殊的
HQL:或Criteria語句實現。
Hibernate定義了以下取策略:
Join fetching : Hibernate取關聯數據或集合是通過OUTER JOIN的方式,通過同一條select 語句來實現。
Select fetching:在沒有指定了lazy = "false"(既延遲加載有效)的情況下,通過另一條select 語句來獲得與已經獲得的實體相關的實體或集合。當然
這種情況發生在用戶真正要獲得關聯對象的時候。
Subselect fetching:在沒有指定了lazy = "false"(既延遲加載有效)的情況下,先通過一條查詢語句獲得了一個實體集,? 然后對這個實體集中的每一個對象通過另一條select 語句來獲得與它相關的實體或集合。
當然這種情況發生在用戶真正要獲得關聯對象的時候。
Batch fetching :它是為查詢數據提供的一種優化策略。通過指定主鍵或外鍵的列表的方式來實現一條select 語句獲得一批實體或集合。
從另一個角度來看,hibernate的fetching 分成以下幾種。
Immediate fetching: 如果實體已經被加載了,他的關聯對象,關聯集合,屬性也要及時加栽。
lazy collection fetching: 只有應用程序真正使用這個集合的時候,才加栽這個集合。
"Extra-lazy" collection fetching : hibernate 不加載一個集合的所有對象到內存里,需要哪個個體,加載哪個。
Proxy fetching :當前對象的單值相關對象只有在調用它的主鍵外的其他屬性的get方法時才加載它。
"NO-proxy"fetching :當前對象的單值相關對象在它的實體變量被訪問的時候就被加載。相對于Proxy fetching來說,Proxy fetching更延遲。
(因為"NO-proxy"fetching即使是訪問關聯對象的主健,關聯對象都要被加載)。"NO-proxy"fetching對于應用來說更條理清晰。因為在應用
中沒有一個可見的proxy.
?? 個人認為可以這樣理解上述情況,假如在數據庫中存在兩張表 A,B.表A中有一個指向表B主健的外鍵。如果想知道A表中的某條
??? 數據對應B表中的那條記錄的主鍵。完全不用訪問B表,A表中的此條數據的外鍵值就是B表中對應數據的主鍵。所有只有訪問B表中
??? 對應數據的主鍵外其他屬性時,才需要加載B表中的這條數據。
Lazy attribute fetching :當前對象的某個屬性或單值相關對象只有在與它對應的實體變量被訪問的時候才加載。
Working with lazy associations
默認的情況下,Hibernate3 在獲取關聯對象集合的時候使用的是lazy策略,獲得單值關聯對象的時候使用的是lazy proxy策略。這樣的策略
幾乎適用所有的應用。
如果你設置了hibernate.default_batch_fetch_size,Hibernate就會通過批量獲取來優化lazy fetching.
lazy fetching 會引起一個問題。就是關閉了hibernate session以后加載延遲加載的對象。這樣會引起異常。如下:
s = sessions.openSession();
Transaction tx = s.beginTransaction();
User u = (User) s.createQuery("from User u where u.name=:userName")
.setString("userName", userName).uniqueResult();
Map permissions = u.getPermissions();
tx.commit();
s.close();
Integer accessLevel = (Integer) permissions.get("accounts"); // Error!
由于在session被關閉之前,permissions 沒有被初始化,所以它的數據沒有被加載。hibernate不支持已經被分離的對象
的延遲加載。修改的方法是把相關代碼移到tx.commit()之前。
或者我們可以在配置文件里通過在關聯對象那里指定 lazy="false"來使關聯集合或對象不被延遲加載。但是如果你定義太多的
非延遲加載對象,hibernate 在一次事務中可以需要把整個數據庫加載到內存中。
從另一個角度來說,在一次事務中,我們經常使用joint fetching 這種方式(它天生就不是延遲加載)來代替select fetching 這種方式。
下邊我們就要看到怎么自定義 取策略。在hibernate3中,單值和集合關聯對象的取策略的指定方式是一致的。
Tuning fetch strategies
默認的select fetching 這種取策略很容器導致N+1次select 操作這樣的問題。所以我們可以在配置文件里指定join fetching 策略。如下:
Cat對應的配置文件:
<set name="permissions"
fetch="join">
<key column="userId"/>
<one-to-many class="Permission"/>
</set>
Permission對應的配置文件:
<many-to-one name="mother" class="Cat" fetch="join"/>
在映射文件里定義的取策略會影響如下操作:
由 get() 或load()執行的取操作。
操作關聯對象而引發的取操作。
Criteria查詢。
如果使用了subselect 這種取策略還會影響HQL這種查詢方式。
一般來說,我們不是通過在映射配置文件自定義取策略,而是通過在一個事務里,通過在特定的HQL里使用 left join 來覆蓋默認的取
策略。對于Criteria 來說,提供了setFetchMode(FetchMode.JOIN) API.如下:
User user = (User) session.createCriteria(User.class)
.setFetchMode("permissions", FetchMode.JOIN)
.add( Restrictions.idEq(userId) )
.uniqueResult();
另一種完全不同的避免N+1次selects 的方式是使用second-level cache.
Single-ended association proxies
集合的延遲加載是通過Hibernate自己的持久化集合實現的,但是對于單個相關對象的延遲加載
需要一個不同的機制.相關的對象必須被代理.Hibernate 對持久化對象的代理的延遲加載是通過
對運行時字節的動態注入實現的(通過CGLIB實現).
默認的情況下,Hibernate3為所有的持久化類生成代理,通過這些代理來完成 many-to-one 和
one-to-one 關聯對象的延遲加載.
在映射文件中可以為類聲明一個接口做為它的代理接口,通過proxy屬性指定。實際上,hibernate真正代理的是
這個類的子類。需要注意的是,被代理的類必須實現一個默認的構造函數(此構造函數的范圍至少是包內可見的)。
推薦所有的持久化類使用這種構造函數。我們現在可以看到的是在類的多態的時候會采用這種方式:
<class name="Cat" proxy="Cat">
......
<subclass name="DomesticCat">
.....
</subclass>
</class>
首先要注意的是,Cat的實例不能當作DomesticCat實例使用。即使Cat和DomesticCat對應的是同一條數據。
Cat cat = (Cat) session.load(Cat.class, id); // instantiate a proxy (does not hit the db)
if ( cat.isDomesticCat() ) { // hit the db to initialize the proxy
DomesticCat dc = (DomesticCat) cat; // Error!
....
}
其次,兩者之間不能使用==
Cat cat = (Cat) session.load(Cat.class, id); // instantiate a Cat proxy
DomesticCat dc =
(DomesticCat) session.load(DomesticCat.class, id); // acquire new DomesticCat proxy!
System.out.println(cat==dc);
實際情況并非如我們看到的那么糟糕。即使我們引用了兩個不同的代理對象,實際的對象卻是相同的。
cat.setWeight(11.0); // hit the db to initialize the proxy
System.out.println( dc.getWeight() ); // 11.0
還需注意的是如果一個類是final class,或者它有final方法。我們就不能使用CGLIB代理.
最后,如果你的持久化對象在實例化的過程中獲得的任何資源(例如 在initializers或者默認的構造函數里),這些
資源也將被proxy獲得.實際上代理的是這個類的子類。
這些問題的根源是java不能多重繼承.如果你想避免這些問題,你應該讓每一個類(子類和父類)實現一個聲明了業務方法的接口.
在你的映射文件中指定這些接口,如下:
<class name="CatImpl" proxy="Cat">
......
<subclass name="DomesticCatImpl" proxy="DomesticCat">
.....
</subclass>
</class>
CatImpl實現了接口Cat,DomesticCatImpl實現了接口DomesticCat.Cat和DomesticCat實例的代理可以
被load()或iterator()方法返回.(list()方法一般不返回代理).
Cat cat = (Cat) session.load(CatImpl.class, catid);
Iterator iter = session.iterate("from CatImpl as cat where cat.name='fritz'");
Cat fritz = (Cat) iter.next();
關系也被延遲加載.這意味這你必須在Cat中聲明所有的屬性,而不僅僅是CatImpl.
以下方法不需要代理的初始值。
equals()? 此方法沒有被覆蓋的時候。
hashCode() 此方法沒有被覆蓋的時候。
主鍵對應的get方法。
Initializing collections and proxies
如果在session的外邊訪問一個沒有初始化的集合或代理,會拋出一個LazyInitializationException異常。例如在分離的
狀態下(session 已經close的情況下)訪問一個實體的延遲加載的集合或代理對象。
有時候我們需要在session關閉之前確保一個代理或集合被初始化。當然我們可以通過cat.getSex()或cat.getKittents().size()
這種方式來強迫初試化。但是這樣會使代碼閱讀者迷茫而且不是一種通用的方便的編碼格式。
靜態方法Hibernate.initialize() 和Hibernate.isInitialized()為應用提供了處理延遲加載集合或代理的一種便捷方式。
Hibernate.initialize(Cat) 會強制加載代理 cat. Hibernate.initialize(cat.getKittens())初始化kittens集合。當然這些方法要在
session關閉之前執行。
另一種方式是在所有需要的集合和代理對象都被加載之后再關閉session. 在一些應用中,尤其是當應用使用hibernate來獲取
數據,卻在其他的應用層處理這些數據。或是這些數據是在其他的處理過程中使用。為了確保這些集合在初始化的時候session
還處于打開狀態,可以通過以下兩種方式:
1 基于 web 的應用可以通過filter 在一次請求的最后關閉session.當然這樣做是基于你的應用可以正確處理異常。非常重要的一點是要確保把信息返回給用戶之前把事務結束和把session關掉,即使是在你的頁面處理發生異常的情況下。(spring 的OpenSessionInViewFilter就是基于此開發出來的)
2 如果你的應用有一個單獨的業務層。在業務邏輯這里要保證在返回給web 層信息之前完成所有的集合初始化工作。這意味著你的
業務層需要加載所有的數據并且把這些包括延遲加載的數據傳給與一個特定的用戶請求的相關呈現部分。一般來說這是通過在session
關閉之前針對相關的集合調用Hibernate.initialize()方法或者是采用Criteria 的FetchMode.JOIN 方式。采用命令模式往往比采用session
Facade容易一些。
3 你也可以在訪問沒有初試化的集合(或代理)之前把先前加載的一個對象通過merge()或lock()放到新的Session里。但是hibernate 不
會也不應該自動完成這樣的工作,因為這樣需要使用特殊的事務處理語法。
有時候,你需要獲得集合中數據的個數,或者集合數據的一部分就不需要初始話整個集合。你可以通過Collection filter來獲得集合中數據
的個數(不需要初始化整個集合)
( (Integer) s.createFilter( collection, "select count(*)" ).list().get(0) ).intValue()。
當然Collection filter也可以獲取集合的一部分數據
s.createFilter( lazyCollection, "").setFirstResult(0).setMaxResults(10).list();
Using batch fetching
批量獲取數據可以提高Hibernate的效率.批量獲取是延遲select fetching策略的一種優化.我們可以對類或者集合兩個角度采用批量取數據.
批量獲取類/實體容易理解,假設有如下情況:
????? 在你的session里加載了25個Cat實例。每一個Cat都有一個own的引用指向一個person.在這里這個關聯的person是通過代理的方式延遲加載
?(單值關聯對象)。如果你現在要通過循環調用所有cat的getOwner()方法。hibernate會默認的執行25個select 語句來獲得被代理的owner對象。
我們可以通過在Person這個表的映射文件中指定batch-size來實現批量取數據。
<class name="Person" batch-size="10">...</class>
Hibernate 現在會執行三條查詢語句來完成查詢,模式是10,10,5.
你也可以對集合進行批量取操作.例如,每一個person都有一個被延遲加載的集合Cats.現在在session中已經加載了10個 person實例.循環調用
所有的person的getCats()方法會產生10條select 語句.如果你在person的映射文件中定義了批量獲取模式:
<class name="Person">
<set name="cats" batch-size="3">
...
</set>
</class>
通過設置batch-size設為3,Hibernate 會以3,3,3,1的模式通過四條select語句加載集合。
Using subselect fetching
如果要加載一個延遲加載的集合或一個單值的代理,Hibernate通過一個subselect 運行原來的查詢語句,這種情況和batch-fetching是異曲同工的。
Using lazy property fetching
Hibernate支持對單個屬性的延遲加載。這個優化技術也被 稱為fetch groups. 需要注意的是,這個技術還處于推銷階段。因為在實際中,對行的讀取
優化比對列的優化更重要。然而在一些特殊情況下,加載一個類的部分屬性還是有必要的,比如一個繼承的表有幾百列而且數據模型還不能改變。
為了使某個屬性被延遲加載,只需要在這個屬性的影射文件中加上lazy屬性即可。
<class name="Document">
<id name="id">
<generator class="native"/>
</id>
<property name="name" not-null="true" length="50"/>
<property name="summary" not-null="true" length="200" lazy="true"/>
<property name="text" not-null="true" length="2000" lazy="true"/>
</class>
屬性的延遲加載需要使用運行時的字節設備來處理。如果你的持久化類還沒有被這個設備處理。hibernate 會忽略這個設置
采用及時加載的方式。
要想使用此字節設備處理持久化類,使用如下的Ant 任務。
<target name="instrument" depends="compile">
<taskdef name="instrument" classname="org.hibernate.tool.instrument.InstrumentTask">
<classpath path="${jar.path}"/>
<classpath path="${classes.dir}"/>
<classpath refid="lib.class.path"/>
</taskdef>
<instrument verbose="true">
<fileset dir="${testclasses.dir}/org/hibernate/auction/model">
<include name="*.class"/>
</fileset>
</instrument>
</target>
另一種避免加載不需要的列的方式,至少在只讀事務中,是通過使用HQL或Criteria查詢屬性。這樣可以避免使用字節
處理工具。
你可以通過在HQL指定fetch all properties 來加載全部屬性。