读取策略是指当应用程序需要在Hibernate实体对象的关联关系间进行导航的时候, Hibernate如何获取关联对象的策略。读取策略可以在O/R映射的元数据中声明,也可以在特定的HQL 或条件查询(Criteria Query)中重载声明。
在Hibernate3中定义了以下几种读取策略:
l 连接读取(Join fetching):是指Hibernate通过在SELECT语句使用OUTER JOIN(外连接)来获得对象的关联实例或者关联集合。
l 查询读取(Select fetching):另外发送一条 SELECT语句抓取当前对象的关联实体或集合。除非显式的指定属性lazy="false"禁止延迟抓取,否则只有当真正访问关联关系的时候,才会执行第二条SELECT语句。
l 子查询读取(Subselect fetching): 另外发送一条SELECT语句抓取在前面查询到的所有实体对象的关联集合。除非显式的指定lazy="false" 禁止延迟抓取,否则只有当真正访问关联关系的时候,才会执行第二条SELECT语句。
l 批量读取(Batch fetching):对查询抓取的优化方案, 通过指定一个主键或外键列表,Hibernate使用单条SELECT语句获取一批对象实例或集合。
在Hibernate中通常会区分下列各种情况:
(1)立即读取:当宿主被加载时,关联、集合或属性被立即读取。
(2)延迟集合读取:直到应用程序对集合进行了一次操作时,集合才被读取。
(3)集合读取:对集合类中的每个元素而言,都是直到需要时才去访问数据库。一般情况下Hibernate不会试图去把整个集合都读取到内存里来。
(4)代理读取: 对返回单值的关联而言,当其某个方法被调用,而非对其关键字进行get操作时才读取。
(5)非代理读取: 对返回单值的关联而言,当实例变量被访问的时候进行读取。与上面的代理抓取相比,这种方法没有那么“延迟”得厉害但是更加透明,因为对应用程序来说,不再看到proxy。这种方法需要在编译期间进行字节码增强操作,因此很少需要用到。
(6)属性延迟加载: 对属性或返回单值的关联而言,当其实例变量被访问的时候进行读取。需要编译期字节码强化,因此这一方法很少是必要的。
默认情况下,Hibernate 3对集合使用延迟读取,对返回单值的关联使用延迟代理读取。 如果设置了hibernate.default_batch_fetch_size属性值,Hibernate会对延迟加载采取批量抓取优化措施。然而,必须了解延迟抓取带来的一个问题。在一个打开的Hibernate session上下文之外调用延迟集合会抛出一个违例。例如:
ses = sessions.openSession(); Transaction txa = s.beginTransaction(); User u = (User) ses.createQuery("from User u where u.name=:userName") .setString("userName", userName).uniqueResult(); Map permissions = u.getPermissions(); txa.commit(); ses.close(); Integer access = (Integer) permissions.get("accounts"); //这是错误的读取方法! |
在上面这段程序中,当关闭Session后,permessions集合将是未实例化的、不再可用,因此无法正常载入其状态。 Hibernate对脱管对象不支持延迟实例化。 正确的方法应该是将permissions读取数据的代码移到txa.commit()之前。
12.4.3 调整读取策略
查询读取(默认查询方法)在N+1查询的情况下是极其脆弱的,因此可以在映射文档中定义使用连接读取,例如:
<set name="permissions" fetch="join">
<key column="userId"/>
<one-to-many class="Permission"/>
</set>
<many-to-one name="mother" class="Cat" fetch="join"/>
在映射文档中定义的读取策略将会对以下列表条目产生影响:
通过get()或load()方法取得数据。 只有在关联之间进行导航时,才会隐式的取得数据。 在条件查询中使用了子查询读取的HQL查询, 不管使用哪种抓取策略,定义为非延迟的Bean会被保证一定装载入内存。通常情况下,我们并不使用映射文档进行读取策略的定制。更多的是,保持其默认值,然后在特定的事务中, 使用HQL的左连接抓取(left join fetch) 对其进行重载。这将通知 Hibernate在第一次查询中使用外部关联(outer join),直接得到其关联数据。 在条件查询 API中,应该调用 setFetchMode(FetchMode.JOIN)语句。
也可以通过条件查询,就可以改变get() 或 load()语句中的数据抓取策略。例如:
User user = (User) session.createCriteria(User.class)
.setFetchMode("permissions", FetchMode.JOIN)
.add( Restrictions.idEq(userId) )
.uniqueResult();
另外一种避免N+1次查询的方法是,使用二级缓存。
12.4.4 单端关联代理
在Hinerbate中,对集合的延迟读取采用了自己的实现方法。但是,对于单端关联的延迟读取,则需要采用其他机制。单端关联的目标实体必须使用代理,Hihernate在运行期间为持久对象实现了延迟载入代理。 默认情况下Hibernate3在启动阶段将会为所有的持久对象产生代理,然后使用它们实现多对一关联和一对一关联的延迟读取。 在映射文件中,可以通过设置proxy属性为目标class声明一个接口供代理接口使用。被代理的类必须实现一个默认的构造函数,默认情况下Hibernate将会使用该类的一个子类,例如:
<class name="Cat" proxy="Cat">
......
<subclass name="SubCat">
</subclass>
</class>
在上面配置中,Cat实例永远不可以被强制转换为SubCat,即使它本身就是SubCat实例。同样不能对“final类”或“具有final方法的类”使用CGLIB代理。如果持久化对象在实例化时需要某些资源, 那么代理对象也同样需要使用这些资源。实际上,代理类是持久化类的子类。
这些问题都源于Java的单根继承模型的限制。如果希望避免这些问题,那么每个持久化类必须实现一个接口, 在此接口中已经声明了其业务方法。然后,需要在映射文档中再指定这些接口。例如:
<class name="CatImpl" proxy="Cat">
<subclass name="SubCatImpl" proxy="SubCat">
.....
</subclass>
</class>
这里CatImpl实现了Cat接口, SubCatImpl实现SubCat接口。 在load()、iterate()方法中就会返回 Cat和SubCat的代理对象。 这里需要注意的是list()并不会返回代理对象。例如:
Cat cat = (Cat) session.load(CatImpl.class, catid);
Iterator iter = session.iterate("from CatImpl as cat where cat.name='mm'");
Cat mm = (Cat) iter.next();
在上面代码中,对象之间的关系也将被延迟载入。这就意味着,应该将属性声明为Cat,而不是CatImpl。 但是,在有些方法中是不需要使用代理的。例如equals()方法,如果持久类没有重载equals()方法。
12.4.5 实例化集合和代理
如果在Session范围之外访问未初始化的集合或代理,Hibernate将会抛出LazyInitializationException异常。 也就是说,在分离状态下,访问一个实体所拥有的集合,或者访问其指向代理的属性时,会引发此异常。 有时候需要保证某个代理或者集合在Session关闭前就已经被初始化,可以应用Hibernate.initialized()静态方法来延迟加载集合或代理。 只要它的Session处于open状态,Hibernate.initialize(cat) 将会为cat强制对代理实例化。 同样,Hibernate.initialize( cat.getKittens() ) 对kittens的集合具有同样的功能。
也可以通过merge()或lock()方法,在访问未实例化的集合(或代理)之前, 为先前载入的对象绑定一个新的Session。有时并不需要完全实例化整个大的集合,仅需要了解它的部分信息(例如其大小)、或者集合的部分内容。 可以使用集合过滤器得到其集合的大小,而不必实例化整个集合,例如:
(Integer) s.createFilter( collection, "select count(*)" ).list().get(0) ).intValue()
这里的createFilter()方法也可以被用来有效的抓取集合的部分内容,而无需实例化整个集合,例如:
s.createFilter( lazyCollection, "").setFirstResult(0).setMaxResults(10).list();
12.4.6 使用批量读取
在Hibernate中可以使用批量读取,也就是说,如果仅一个访问代理(或集合),那么Hibernate将不载入其他未实例化的代理。 批量抓取是延迟查询抓取的优化方案,可以在两种批量抓取方案之间进行选择:在类级别和集合级别。
类/实体级别的批量抓取很容易理解。 例如在一个Session中载入了20个 Cat实例,每个Cat实例都拥有一个引用成员owner, 其指向Person,而Person类是代理,同时lazy="true"。 如果你必须遍历整个cats集合,对每个元素调用getOwner()方法,Hibernate将会默认的执行20次SELECT查询, 得到其owner的代理对象。这时,你可以通过在映射文件的Person属性,显式声明batch-size,改变其行为:
<class name="Person" batch-size="10">...</class>
这样,Hibernate将只需要执行两次查询,分别为10、10。
也可以在集合级别定义批量抓取。例如,如果每个Person都拥有一个延迟载入的Cats集合, 当前Sesssion中载入了10个person对象,遍历person集合将会引起10次SELECT查询, 每次查询都会调用getCats()方法。如果在Person的映射定义部分,允许对cats批量抓取, 那么,Hibernate将可以预先抓取整个集合。例如:
<class name="Person">
<set name="cats" batch-size="3">
...
</set>
</class>
如果整个的batch-size是5,那么Hibernate将会分两次执行SELECT查询, 按照5、5的大小分别载入数据。这里的每次载入的数据量还具体依赖于当前Session中未实例化集合的个数。
12.4.7 使用子查询读取
若一个延迟集合或单值代理需要读取,Hibernate会使用一个subselect重新运行原来的查询,一次性读入所有的实例。这和批量读取的实现方法是一样的,不会有破碎的加载。
12.4.8 使用延迟属性读取
在Hibernate3中对单独的属性支持延迟读取,这项优化技术也被称为组读取(fetch groups)。在实际应用中,优化行读取比优化列读取更重要。但是,仅载入类的部分属性在某些特定情况下会有用,例如在原有表中拥有几百列数据、数据模型无法改动的情况下。 可以在映射文件中对特定的属性设置lazy,定义该属性为延迟载入。例如:
<class name="Document">
<id name="id">
<generator class="native"/>
</id>
<property name="name" not-null="true" length="20"/>
<property name="summary" not-null="true" length="20" lazy="true"/>
<property name="note" not-null="true" length="1000" lazy="true"/>
</class>
属性的延迟载入要求在其代码构建时加入二进制指示指令(bytecode instrumentation),如果持久类代码中未含有这些指令, Hibernate将会忽略这些属性的延迟设置,仍然将其直接载入。