主题:关于hibernate延迟加载的错误解决方案

在项目开发中,对于struts的数据读取,当遇到多个表关联的数据读取的时候,精彩会出现session close的错误,一般的解决方案是将hib的延迟加载错误取消掉,但是这样赶鸭子上架的解决机制无疑是对程序的极度不负责,在这里我们用到 hibernte自带的一种机制,session.load。

例如

 

notic----user

user----userInfo

 

三个表,notic a = (notic)session.load(notic,id);

 

a.getuser()

a.getuser.getuserinfo();

 

session.transaction.commit();

 

问题解决。

 

 

当然hibernate也有相关的迟缓策略,官方文档如下:

 

 

20.1. 抓取策略(Fetching strategies)

抓取策略(fetching strategy) 是指:当应用程序需要在(Hibernate实体对象图的)关联关系间进行导航的时候, Hibernate如何获取关联对象的策略。抓取策略可以在O/R映射的元数据中声明,也可以在特定的HQL 或条件查询(Criteria Query) 中重载声明。

Hibernate3 定义了如下几种抓取策略:

  • 连接抓取(Join fetching) - Hibernate通过 在SELECT 语句使用OUTER JOIN (外连接)来 获得对象的关联实例或者关联集合。

  • 查询抓取(Select fetching) - 另外发送一条 SELECT 语句抓取当前对象的关联实体或集合。除非你显式的指定lazy="false" 禁止 延迟抓取(lazy fetching),否则只有当你真正访问关联关系的时候,才会执行第二条select语句。

  • 子查询抓取(Subselect fetching) - 另外发送一条SELECT 语句抓取在前面查询到(或者抓取到)的所有实体对象的关联集合。除非你显式的指定lazy="false" 禁止延迟抓取(lazy fetching),否则只有当你真正访问关联关系的时候,才会执行第二条select语句。

  • 批量抓取(Batch fetching) - 对查询抓取的优化方案, 通过指定一个主键或外键列表,Hibernate使用单条SELECT 语句获取一批对象实例或集合。

Hibernate会区分下列各种情况:

  • Immediate fetching,立即抓取 - 当宿主被加载时,关联、集合或属性被立即抓取。

  • Lazy collection fetching,延迟集合抓取 - 直到应用程序对集合进行了一次操作时,集合才被抓取。(对集合而言这是默认行为。)

  • Proxy fetching,代理抓取 - 对返回单值的关联而言,当其某个方法被调用,而非对其关键字进行get操作时才抓取。

  • Lazy attribute fetching,属性延迟加载 - 对属性或返回单值的关联而言,当其实例变量被访问的时候进行抓取(需要运行时字节码强化)。这一方法很少是必要的。

这里有两个正交的概念:关联何时 被抓取,以及被如何 抓取(会采用什么样的SQL语句)。不要混淆它们!我们使用抓取 来改善性能。我们使用延迟 来定义一些契约,对某特定类的某个脱管的实例,知道有哪些数据是可以使用的。

20.1.1. 操作延迟加载的关联

默认情况下,Hibernate 3对集合使用延迟select抓取,对返回单值的关联使用延迟代理抓取。对几乎是所有的应用而言,其绝大多数的关联,这种策略都是有效的。

注意: 假若你设置了hibernate.default_batch_fetch_size ,Hibernate会对延迟加载采取批量抓取优化措施(这种优化也可能会在更细化的级别打开)。

然而,你必须了解延迟抓取带来的一个问题。在一个打开的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 关闭后,permessions集合将是未实例化的、不再可用,因此无法正常载入其状态。 Hibernate对脱管对象不支持延迟实例化 . 这里的修改方法是:将permissions读取数据的代码 移到tx.commit()之前。

除此之外,通过对关联映射指定lazy="false" ,我们也可以使用非延迟的集合或关联。但是, 对绝大部分集合来说,更推荐使用延迟方式抓取数据。如果在你的对象模型中定义了太多的非延迟关联,Hibernate最终几乎需要在每个事务中载入整个数据库到内存中!

但是,另一方面,在一些特殊的事务中,我们也经常需要使用到连接抓取(它本身上就是非延迟的),以代替查询抓取。 下面我们将会很快明白如何具体的定制Hibernate中的抓取策略。在Hibernate3中,具体选择哪种抓取策略的机制是和选择 单值关联或集合关联相一致的。

20.1.2. 调整抓取策略(Tuning fetch strategies)

查询抓取(默认的)在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的左连接抓取(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();

(这就是其他ORM解决方案的“抓取计划(fetch plan)”在Hibernate中的等价物。)

截然不同的一种避免N+1次查询的方法是,使用二级缓存。

20.1.3. 单端关联代理(Single-ended association proxies)

在Hinerbate中,对集合的延迟抓取的采用了自己的实现方法。但是,对于单端关联的延迟抓取,则需要采用 其他不同的机制。单端关联的目标实体必须使用代理,Hihernate在运行期二进制级(通过优异的CGLIB库), 为持久对象实现了延迟载入代理。

默认的,Hibernate3将会为所有的持久对象产生代理(在启动阶段),然后使用他们实现 多对一(many-to-one) 关联和一对一(one-to-one) 关联的延迟抓取。

在映射文件中,可以通过设置proxy 属性为目标class声明一个接口供代理接口使用。 默认的,Hibernate将会使用该类的一个子类。 注意:被代理的类必须实现一个至少包可见的默认构造函数,我们建议所有的持久类都应拥有这样的构造函数

在如此方式定义一个多态类的时候,有许多值得注意的常见性的问题,例如:

<class name="Cat" proxy="Cat">
......
<subclass name="DomesticCat">
.....
</subclass>
</class>

首先,Cat 实例永远不可以被强制转换为DomesticCat , 即使它本身就是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); // false

虽然如此,但实际情况并没有看上去那么糟糕。虽然我们现在有两个不同的引用,分别指向这两个不同的代理对象, 但实际上,其底层应该是同一个实例对象:

cat.setWeight(11.0);  // hit the db to initialize the proxy
System.out.println( dc.getWeight() ); // 11.0

第三,你不能对“final类”或“具有final方法的类”使用CGLIB代理。

最后,如果你的持久化对象在实例化时需要某些资源(例如,在实例化方法、默认构造方法中), 那么代理对象也同样需要使用这些资源。实际上,代理类是持久化类的子类。

这些问题都源于Java的单根继承模型的天生限制。如果你希望避免这些问题,那么你的每个持久化类必须实现一个接口, 在此接口中已经声明了其业务方法。然后,你需要在映射文档中再指定这些接口。例如:

<class name="CatImpl" proxy="Cat">
......
<subclass name="DomesticCatImpl" proxy="DomesticCat">
.....
</subclass>
</class>

这里CatImpl 实现了Cat 接口, DomesticCatImpl 实现DomesticCat 接口。 在load() iterate() 方法中就会返回 Cat DomesticCat 的代理对象。 (注意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() 方法,如果持久类没有重载equals() 方法。

  • hashCode() 方法,如果持久类没有重载hashCode() 方法。

  • 标志符的getter方法。

Hibernate将会识别出那些重载了equals() 、或hashCode() 方法的持久化类。

20.1.4. 实例化集合和代理(Initializing collections and proxies)

Session 范围之外访问未初始化的集合或代理,Hibernate将会抛出LazyInitializationException 异常。 也就是说,在分离状态下,访问一个实体所拥有的集合,或者访问其指向代理的属性时,会引发此异常。

有时候我们需要保证某个代理或者集合在Session关闭前就已经被初始化了。 当然,我们可以通过强行调用cat.getSex() 或者cat.getKittens().size() 之类的方法来确保这一点。 但是这样的程序会造成读者的疑惑,也不符合通常的代码规范。

静态方法Hibernate.initialized() 为你的应用程序提供了一个便捷的途径来延迟加载集合或代理。 只要它的Session处于open状态,Hibernate.initialize(cat) 将会为cat强制对代理实例化。 同样,Hibernate.initialize( cat.getKittens() ) 对kittens的集合具有同样的功能。

还有另外一种选择,就是保持Session 一直处于open状态,直到所有需要的集合或代理都被载入。 在某些应用架构中,特别是对于那些使用Hibernate进行数据访问的代码,以及那些在不同应用层和不同物理进程中使用Hibernate的代码。 在集合实例化时,如何保证Session 处于open状态经常会是一个问题。有两种方法可以解决此问题:

  • 在一个基于Web的应用中,可以利用servlet过滤器(filter),在用户请求(request)结束、页面生成 结束时关闭Session (这里使用了在展示层保持打开Session模式(Open Session in View) ), 当然,这将依赖于应用框架中异常需要被正确的处理。在返回界面给用户之前,乃至在生成界面过程中发生异常的情况下, 正确关闭Session 和结束事务将是非常重要的, Servlet过滤器必须如此访问Session ,才能保证正确使用Session。 我们推荐使用ThreadLocal 变量保存当前的Session (可以参考第 1.4 节 “与Cat同乐” 的例子实现)。

  • 在一个拥有单独业务层的应用中,业务层必须在返回之前,为web层“准备”好其所需的数据集合。这就意味着 业务层应该载入所有表现层/web层所需的数据,并将这些已实例化完毕的数据返回。通常,应用程序应该 为web层所需的每个集合调用Hibernate.initialize() (这个调用必须发生咱session关闭之前); 或者使用带有FETCH 从句,或FetchMode.JOIN 的Hibernate查询, 事先取得所有的数据集合。如果你在应用中使用了Command 模式,代替Session Facade , 那么这项任务将会变得简单的多。

  • 你也可以通过merge() lock() 方法,在访问未实例化的集合(或代理)之前, 为先前载入的对象绑定一个新的Session 。 显然,Hibernate将不会,也不应该 自动完成这些任务,因为这将引入一个特殊的事务语义。

有时候,你并不需要完全实例化整个大的集合,仅需要了解它的部分信息(例如其大小)、或者集合的部分内容。

你可以使用集合过滤器得到其集合的大小,而不必实例化整个集合:

( (Integer) s.createFilter( collection, "select count(*)" ).list().get(0) ).intValue()

这里的createFilter() 方法也可以被用来有效的抓取集合的部分内容,而无需实例化整个集合:

s.createFilter( lazyCollection, "").setFirstResult(0).setMaxResults(10).list();

20.1.5. 使用批量抓取(Using batch fetching)

Hibernate可以充分有效的使用批量抓取,也就是说,如果仅一个访问代理(或集合),那么Hibernate将不载入其他未实例化的代理。 批量抓取是延迟查询抓取的优化方案,你可以在两种批量抓取方案之间进行选择:在类级别和集合级别。

类/实体级别的批量抓取很容易理解。假设你在运行时将需要面对下面的问题:你在一个Session 中载入了25个 Cat 实例,每个Cat 实例都拥有一个引用成员owner , 其指向Person ,而Person 类是代理,同时lazy="true" 。 如果你必须遍历整个cats集合,对每个元素调用getOwner() 方法,Hibernate将会默认的执行25次SELECT 查询, 得到其owner的代理对象。这时,你可以通过在映射文件的Person 属性,显式声明batch-size ,改变其行为:

<class name="Person" batch-size="10">...</class>

随之,Hibernate将只需要执行三次查询,分别为10、10、 5。

你也可以在集合级别定义批量抓取。例如,如果每个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 是3(笔误?),那么Hibernate将会分四次执行SELECT 查询, 按照3、3、3、1的大小分别载入数据。这里的每次载入的数据量还具体依赖于当前Session 中未实例化集合的个数。

如果你的模型中有嵌套的树状结构,例如典型的帐单-原料结构(bill-of-materials pattern),集合的批量抓取是非常有用的。 (尽管在更多情况下对树进行读取时,嵌套集合(nested set) 原料路径(materialized path) (××) 是更好的解决方法。)

20.1.6. 使用子查询抓取(Using subselect fetching)

假若一个延迟集合或单值代理需要抓取,Hibernate会使用一个subselect重新运行原来的查询,一次性读入所有的实例。这和批量抓取的实现方法是一样的,不会有破碎的加载。

20.1.7. 使用延迟属性抓取(Using lazy property fetching)

Hibernate3对单独的属性支持延迟抓取,这项优化技术也被称为组抓取(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>

属性的延迟载入要求在其代码构建时加入二进制指示指令(bytecode instrumentation),如果你的持久类代码中未含有这些指令, Hibernate将会忽略这些属性的延迟设置,仍然将其直接载入。

你可以在Ant的Task中,进行如下定义,对持久类代码加入“二进制指令。”

<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或条件查询的投影(projection)特性,可以避免读取非必要的列, 这一点至少对只读事务是非常有用的。它无需在代码构建时“二进制指令”处理,因此是一个更加值得选择的解决方法。

有时你需要在HQL中通过抓取所有属性 ,强行抓取所有内容。

20.2. 二级缓存(The Second Level Cache)

Hibernate的Session 在事务级别进行持久化数据的缓存操作。 当然,也有可能分别为每个类(或集合),配置集群、或JVM级别(SessionFactory级别 )的缓存。 你甚至可以为之插入一个集群的缓存。注意,缓存永远不知道其他应用程序对持久化仓库(数据库)可能进行的修改 (即使可以将缓存数据设定为定期失效)。

默认情况下,Hibernate使用EHCache进行JVM级别的缓存(目前,Hibernate已经废弃了对JCS的支持,未来版本中将会去掉它)。 你可以通过设置hibernate.cache.provider_class 属性,指定其他的缓存策略, 该缓存策略必须实现org.hibernate.cache.CacheProvider 接口。

表 20.1. 缓存策略提供商(Cache Providers)

CacheProvider classTypeCluster SafeQuery Cache Supported
Hashtable (not intended for production use)org.hibernate.cache.HashtableCacheProvider memory yes
EHCacheorg.hibernate.cache.EhCacheProvider memory, disk yes
OSCacheorg.hibernate.cache.OSCacheProvider memory, disk yes
SwarmCacheorg.hibernate.cache.SwarmCacheProvider clustered (ip multicast)yes (clustered invalidation) 
JBoss TreeCacheorg.hibernate.cache.TreeCacheProvider clustered (ip multicast), transactionalyes (replication)yes (clock sync req.)

20.2.1. 缓存映射(Cache mappings)

类或者集合映射的“<cache> 元素”可以有下列形式:

<cache
usage="transactional|read-write|nonstrict-read-write|read-only" (1)
/>
(1)

usage 说明了缓存的策略: transactional read-write nonstrict-read-write read-only

另外(首选?), 你可以在hibernate.cfg.xml中指定<class-cache> <collection-cache> 元素。

这里的usage 属性指明了缓存并发策略(cache concurrency strategy)

20.2.2. 策略:只读缓存(Strategy: read only)

如果你的应用程序只需读取一个持久化类的实例,而无需对其修改, 那么就可以对其进行只读 缓存。这是最简单,也是实用性最好的方法。甚至在集群中,它也能完美地运作。

<class name="eg.Immutable" mutable="false">
<cache usage="read-only"/>
....
</class>

20.2.3. 策略:读/写缓存(Strategy: read/write)

如果应用程序需要更新数据,那么使用读/写缓存 比较合适。 如果应用程序要求“序列化事务”的隔离级别(serializable transaction isolation level),那么就决不能使用这种缓存策略。 如果在JTA环境中使用缓存,你必须指定hibernate.transaction.manager_lookup_class 属性的值, 通过它,Hibernate才能知道该应用程序中JTA的TransactionManager 的具体策略。 在其它环境中,你必须保证在Session.close() 、或Session.disconnect() 调用前, 整个事务已经结束。 如果你想在集群环境中使用此策略,你必须保证底层的缓存实现支持锁定(locking)。Hibernate内置的缓存策略并不支持锁定功能。

<class name="eg.Cat" .... >
<cache usage="read-write"/>
....
<set name="kittens" ... >
<cache usage="read-write"/>
....
</set>
</class>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值