站内搜索: 请输入搜索关键词
当前页面: 在线文档首页 > Hibernate reference 2.1.8 ga 正式版中文参考手册

第 14 章 性能提升(Improving performance) - Hibernate reference 2.1.8 ga 正式版中文参考手册

第 14 章 性能提升(Improving performance)

14.1. 理解集合的性能

我们已经花了很长时间在讨论集合(collections)了。在本章,我们会特别关注一些关于集合在运行时如何运作的问题。

14.1.1. 分类

Hibernate定义了三种基本类型的集合:

  • 值集合

  • 一对多关联

  • 多对多关联

这个分类是按照不同的表和外键关系类型来区分的,但是没有告诉我们关于关系模型的一切。要完全理解他们的关系结构和性能特点,我们必须思考用于更新或删除集合行的主键的结构。这得到了如下的分类:

  • 有序集合类

  • 集合(sets)

  • 包(bags)

有序集合类(maps, lists, arrays)有一个包含有<key><index>字段的主键。这种情况下集合类更新是特别高效的——主键会有效索引,当Hibernate试图更新或删除一行时,可以迅速找到这一行。

集合(sets)的主键包含有 <key> 和元素字段。对于有些元素类型来说,这会变得低效,特别是组合元素或者大文本、大二进制字段;数据库可能无法有效对复杂的主键进行索引。另一方面,对于一对多或多对多关联,特别是合成的标识符来说,它会达到同样的高效。(附注:如果你希望SchemaExport为你的<set>创建主键,你必须把所有的字段都声明为not-null="true"。)

Bag是最差的。因为bag允许重复的元素值,也没有索引字段,不可能定义主键。Hibernate没有办法来判断出重复的行。每当这种集合被更改,Hibernate会完整地移除(通过一个DELETE),再重建整个集合。这会非常低效。

请注意对一对多关联来说,“主键”可能是数据库表的物理主键——但就算在这种情况下,上面的分类仍然是有用的。(它会反映Hibernate是如何在集合的各个行中“定位”的。)

14.1.2. Lists, maps 和sets用于更新效率最高

根据我们上面的讨论,显然有序类型和大多数set可以在增加/删除/修改元素的时候得到最好的性能。

但是,在多对多关联,或者对值元素而言,有序集合类比集合(set)有一个好处。因为Set的结构,如果“改变”了一个元素,Hibernate并不会UPDATE这一行。对Set来说,只有INSERTDELETE才有效。注意这一段描述对一对多关联并不适用。

注意到数组无法延迟转载,我们可以得出结论,list, map和set是最高效的集合类型。(当然,我们警告过了,由于集合中的值的关系,set可能性能下降。)

Set可以被看作是Hibernate程序中最普遍的集合类型。

这个版本的Hibernate有一个没有写在文档中的功能。<idbag>可以对值集合和多对多关联实现bag语义,并且性能比上面任何类型都高!

14.1.3. Bag和list是反向集合类中效率最高的

好了,在你把bag扔到水沟里面再踩上一只脚之前要了解,有一种情况下bag(包括list)要比set性能高得多。对于指明了inverse="true"的集合类(比如说,标准的双向一对多关联),我们可以在不初始化(fetch)包元素的情况下就增加新元素!这是因为Collection.add()或者Collection.addAll()对bag或者List总是返回true的(与Set不同)。对于下面的代码来说,速度会快得多。

Parent p = (Parent) sess.load(Parent.class, id);
    Child c = new Child();
    c.setParent(p);
    p.getChildren().add(c);  //no need to fetch the collection!
    sess.flush();

14.1.4. 一次性删除(One shot delete)

有时候,一个一个的删除集合类中的元素是极度低效的。Hibernate没那么笨,如果你想要把整个集合都删除(比如说调用list.clear()),Hibernate只需要一个DELETE就搞定了。

假设我们在一个长度为20的集合类中新增加了一个元素,然后删除了两个。Hibernate会安排一个INSERT语句和两条DELETE语句(除非集合类是一个bag)。这当然是可以想见的。

但是,如果假设我们删除了18个元素,只剩下2个,然后新增3个。有两种处理方式:

  • 把这18个元素一个一个的干掉,再新增三个

  • 把整个集合类都咔嚓掉(只用一句DELETE语句),然后增加5个元素。

Hibernate还没那么聪明,知道第二种选择可能会比较快。(也许让Hibernate不要这么聪明也是好事,否则可能会引发意外的数据库触发器什么的。)

幸运的是,你可以强制使用第二种策略。你需要把原来的整个集合类都取消(取消其引用),然后返回一个新实例化的集合类,只包含需要的元素。有些时候这是非常有用的。

我们已经为您展示了如何在对集合持久化时使用延迟装载(lazy initialization)。对于通常的对象引用,使用CGLIB代理可以达到类似的效果。我们也提到过Hibernate在Session级别缓存持久化对象。还有更多先进的缓存策略,你可以为每一个类单独配置。

这本章里,我们来教你如何使用这些特性,在必要的时候得到高得多的性能。

14.2. 用于延迟装载的代理

Hibernate使用动态字节码增强技术来实现持久化对象的延迟装载代理(使用优秀的CGLIB库)。

映射文件为每一个类声明一个类或者接口作为代理接口。建议使用这个类自身:

<class name="eg.Order" proxy="eg.Order">

运行时的代理应该是Order的子类。注意被代理的类必须实现一个默认的构造器,并且至少在包内可见。

在扩展这种方法来对应多形的类时,要注意一些细节,比如:

<class name="eg.Cat" proxy="eg.Cat">
    ......
    <subclass name="eg.DomesticCat" proxy="eg.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);  // required 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="eg.Cat" proxy="eg.ICat">
    ......
    <subclass name="eg.DomesticCat" proxy="eg.IDomesticCat">
        .....
    </subclass>
</class>

这里Cat实现ICat接口,并且DomesticCat实现IDomesticCat接口。于是 load()或者iterate()就会返回CatDomesticCat的实例的代理。(注意find()不会返回代理。)

ICat cat = (ICat) session.load(Cat.class, catid);
Iterator iter = session.iterate("from cat in class eg.Cat where cat.name='fritz'");
ICat fritz = (ICat) iter.next();

关系也是延迟装载的。这意味着你必须把任何属性声明为ICat类型,而非Cat

某些特定操作需要初始化代理

  • equals(), 假如持久化类没有重载equals()

  • hashCode(), 假如持久化类没有重载hashCode()

  • 标识符的get方法

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

在初始化代理的时候发生的异常会被包装成LazyInitializationException

有时候我们需要保证在Session关闭前某个代理或者集合已经被初始化了。当然,我们总是可以通过调用cat.getSex()或者 cat.getKittens().size()之类的方法来确保这一点。但是这样程序可读性不佳,也不符合通常的代码规范。静态方法Hibernate.initialize()Hibernate.isInitialized()给你的应用程序一个正常的途径来加载集合或代理。Hibernate.initialize(cat) 会强制初始化一个代理,cat,只要它的Session仍然打开。Hibernate.initialize( cat.getKittens() )对kittens的集合具有同样的功能。

14.3. 第二层缓存(The Second Level Cache)s

HibernateSession是事务级别的持久化数据缓存。再为每个类或者每个集合配置一个集群或者JVM级别(SessionFactory级别)的缓存也是有可能的。你甚至可以插入一个集群的缓存。要小心,缓存永远不会知道其他进程可能对持久化仓库(数据库)进行的修改(即使他们可能设定为经常对缓存的数据进行失效)。

默认情况下,Hibernate使用EHCache进行JVM级别的缓存。但是,对JCS的支持现在已经被废弃了,未来版本的Hibernate将会去掉它。通过hibernate.cache.provider_class属性,你也可以指定其他缓存,只要其实现了net.sf.hibernate.cache.CacheProvider接口。

表 14.1. Cache Providers

CacheProvider classTypeCluster SafeQuery Cache Supported
Hashtable (not intended for production use)net.sf.hibernate.cache.HashtableCacheProvidermemory yes
EHCachenet.sf.hibernate.cache.EhCacheProvidermemory, disk yes
OSCachenet.sf.hibernate.cache.OSCacheProvidermemory, disk yes
SwarmCachenet.sf.hibernate.cache.SwarmCacheProviderclustered (ip multicast)yes (clustered invalidation) 
JBoss TreeCachenet.sf.hibernate.cache.TreeCacheProviderclustered (ip multicast), transactionalyes (replication) 

14.3.1. 对映射(Mapping)缓冲

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

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

usage 指定了缓存策略: transactional, read-write, nonstrict-read-write 或者 read-only

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

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

14.3.2. 策略:只读缓存

如果你的应用程序需要读取一个持久化类的实例,但是并不打算修改它们,可以使用read-only 缓存。这是最简单,也是实用性最好的策略。甚至在集群中,它也能完美地运作。

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

14.3.3. 策略:读/写缓存

如果应用程序需要更新数据,可能read-write缓存比较合适。如果需要可序列化事务隔离级别(serializable transaction isolation level),这种缓存决不能使用。如果在JTA环境中使用这种缓存,你必须指定hibernate.transaction.manager_lookup_class属性的值,给出得到JTA TransactionManager的策略。在其它环境中,你必须确保在Session.close()或者Session.disconnect()调用前,事务已经结束了。 如果你要在集群环境下使用这一策略,你必须确保底层的缓存实现支持锁定(locking)。内置的缓存提供器并不支持。

<class name="eg.Cat" .... >
    <cache usage="read-write"/>
    ....
    <set name="kittens" ... >
        <cache usage="read-write"/>
        ....
    </set>
</class>

14.3.4. 策略:不严格的读/写缓存

如果程序偶尔需要更新数据(也就是说,出现两个事务同时更新同一个条目的现象很不常见),也不需要十分严格的事务隔离,可能适用nonstrict-read-write缓存。如果在JTA环境中使用这种缓存,你必须指定hibernate.transaction.manager_lookup_class属性的值,给出得到JTA TransactionManager的策略。在其它环境中,你必须确保在Session.close()或者Session.disconnect()调用前,事务已经结束了。

14.3.5. 策略:事务缓存(transactional)

transactional缓存策略提供了对全事务缓存提供,比如JBoss TreeCache的支持。这样的缓存只能用于JTA环境,你必须指定hibernate.transaction.manager_lookup_class

没有一种缓存提供器能够支持所有的缓存并发策略。下面的表列出每种提供器与各种并发策略的兼容性。

表 14.2. 缓存并发策略支持(Cache Concurrency Strategy Support)

Cacheread-onlynonstrict-read-writeread-writetransactional
Hashtable (not intended for production use)yesyesyes 
EHCacheyesyesyes 
OSCacheyesyesyes 
SwarmCacheyesyes  
JBoss TreeCacheyes  yes

14.4. 管理Session缓存

不管何时你传递一个对象给save(), update()或者 saveOrUpdate() ,或者不管何时你使用load(), find(), iterate()或者filter()取得一个对象的时候,该对象被加入到Session的内部缓存中。当后继的flush()被调用时,对象的状态会和数据库进行同步。如果你在处理大量对象并且需要有效的管理内存的时候,你可能不希望发生这种同步,evict()方法可以从缓存中去掉对象和它的集合。

Iterator cats = sess.iterate("from eg.Cat as cat"); //a huge result set
while ( cats.hasNext() ) {
    Cat cat = (Cat) iter.next();
    doSomethingWithACat(cat);
    sess.evict(cat);
}

Hibernate will evict associated entities automatically if the association is mapped with cascade="all" or cascade="all-delete-orphan". 如果关联通过cascade="all" 或者 cascade="all-delete-orphan"实现,Hibernate会自动删除关联的实体。

Session也提供了一个contains()方法来判断是否一个实例处于这个session的缓存中。

要把所有的对象从session缓存中完全清除,请调用Session.clear()

对于第二层缓存来说,在SessionFactory中定义了一些方法来从缓存中清除一个实例、整个类、集合实例或者整个集合。

14.5. 查询缓存(Query Cache)

查询结果集也可以被缓存。只有当经常使用同样的参数进行查询时,这才会有些用处。要使用查询缓存,首先你要打开它,设置hibernate.cache.use_query_cache=true这个属性。这样会创建两个缓存区域——一个保存查询结果集(net.sf.hibernate.cache.QueryCache),另一个保存最近查询的表的时间戳(net.sf.hibernate.cache.UpdateTimestampsCache)。请注意查询缓存并不缓存结果集中包含实体的状态;它只缓存标识符属性的值和值类型的结果。所以查询缓存通常会和第二层缓存一起使用。

大多数查询并不会从缓存中获得什么好处,所以默认查询是不进行缓存的。要进行缓存,调用 Query.setCacheable(true)。这个调用会让查询在执行时去从缓存中查找结果,或者把结果集放到缓存去。

如果你要对查询缓存的失效政策进行精确的控制,你必须调用Query.setCacheRegion()来为每个查询指定一个命名的缓存区域。

List blogs = sess.createQuery("from Blog blog where blog.blogger = :blogger")
    .setEntity("blogger", blogger)
    .setMaxResults(15)
    .setCacheable(true)
    .setCacheRegion("frontpages")
    .list();