hibernate-orm/reference/zh-cn/modules/performance.xml

1280 lines
56 KiB
XML
Raw Normal View History

<chapter id="performance">
<title>提升性能
</title>
<sect1 id="performance-fetching">
<title>
抓取策略(Fetching strategies)
</title>
<para>
<emphasis>抓取策略fetching strategy</emphasis> 是指当应用程序需要在Hibernate实体对象图的关联关系间进行导航的时候
Hibernate如何获取关联对象的策略。抓取策略可以在O/R映射的元数据中声明也可以在特定的HQL
<literal>条件查询Criteria Query</literal>中重载声明。
</para>
<para>
Hibernate3 定义了如下几种抓取策略:
</para>
<itemizedlist>
<listitem>
<para>
<emphasis>连接抓取Join fetching</emphasis> - Hibernate通过
<literal>SELECT</literal>语句使用<literal>OUTER JOIN</literal>(外连接)来
获得对象的关联实例或者关联集合。
</para>
</listitem>
<listitem>
<para>
<emphasis>查询抓取Select fetching</emphasis> - 另外发送一条 <literal>SELECT</literal>
语句抓取当前对象的关联实体或集合。除非你显式的指定<literal>lazy="false"</literal>禁止
延迟抓取lazy fetching否则只有当你真正访问关联关系的时候才会执行第二条select语句。
</para>
</listitem>
<listitem>
<para>
<emphasis>子查询抓取Subselect fetching</emphasis> - 另外发送一条<literal>SELECT</literal>
语句抓取在前面查询到(或者抓取到)的所有实体对象的关联集合。除非你显式的指定<literal>lazy="false"</literal>
禁止延迟抓取lazy fetching否则只有当你真正访问关联关系的时候才会执行第二条select语句。
</para>
</listitem>
<listitem>
<para>
<emphasis>批量抓取Batch fetching</emphasis> - 对查询抓取的优化方案,
通过指定一个主键或外键列表Hibernate使用单条<literal>SELECT</literal>语句获取一批对象实例或集合。
</para>
</listitem>
</itemizedlist>
<para>
Hibernate会区分下列各种情况
</para>
<itemizedlist>
<listitem>
<para>
<emphasis>Immediate fetching立即抓取</emphasis> - 当宿主被加载时,关联、集合或属性被立即抓取。
</para>
</listitem>
<listitem>
<para>
<emphasis>Lazy collection fetching延迟集合抓取</emphasis>- 直到应用程序对集合进行了一次操作时,集合才被抓取。(对集合而言这是默认行为。)
</para>
</listitem>
<listitem>
<para>
<emphasis>Proxy fetching代理抓取</emphasis> - 对返回单值的关联而言当其某个方法被调用而非对其关键字进行get操作时才抓取。
</para>
</listitem>
<listitem>
<para>
<emphasis>Lazy attribute fetching属性延迟加载</emphasis> - 对属性或返回单值的关联而言,当其实例变量被访问的时候进行抓取(需要运行时字节码强化)。这一方法很少是必要的。
</para>
</listitem>
</itemizedlist>
<para>
这里有两个正交的概念:关联<emphasis>何时</emphasis>被抓取,以及被<emphasis>如何</emphasis>抓取会采用什么样的SQL语句。不要混淆它们我们使用<literal>抓取</literal>来改善性能。我们使用<literal>延迟</literal>来定义一些契约,对某特定类的某个脱管的实例,知道有哪些数据是可以使用的。
</para>
<sect2 id="performance-fetching-lazy">
<title>操作延迟加载的关联</title>
<para>
默认情况下Hibernate 3对集合使用延迟select抓取对返回单值的关联使用延迟代理抓取。对几乎是所有的应用而言其绝大多数的关联这种策略都是有效的。
</para>
<para>
<emphasis>注意:</emphasis>假若你设置了<literal>hibernate.default_batch_fetch_size</literal>,Hibernate会对延迟加载采取批量抓取优化措施这种优化也可能会在更细化的级别打开
</para>
<para>
然而你必须了解延迟抓取带来的一个问题。在一个打开的Hibernate session上下文之外调用延迟集合会导致一次意外。比如
</para>
<programlisting><![CDATA[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!]]></programlisting>
<para>
<literal>Session</literal>关闭后permessions集合将是未实例化的、不再可用因此无法正常载入其状态。
<emphasis>Hibernate对脱管对象不支持延迟实例化</emphasis>. 这里的修改方法是将permissions读取数据的代码
移到tx.commit()之前。
</para>
<para>
除此之外,通过对关联映射指定<literal>lazy="false"</literal>,我们也可以使用非延迟的集合或关联。但是,
对绝大部分集合来说更推荐使用延迟方式抓取数据。如果在你的对象模型中定义了太多的非延迟关联Hibernate最终几乎需要在每个事务中载入整个数据库到内存中
</para>
<para>
但是,另一方面,在一些特殊的事务中,我们也经常需要使用到连接抓取(它本身上就是非延迟的),以代替查询抓取。
下面我们将会很快明白如何具体的定制Hibernate中的抓取策略。在Hibernate3中具体选择哪种抓取策略的机制是和选择
单值关联或集合关联相一致的。
</para>
</sect2>
<sect2 id="performance-fetching-custom" revision="3">
<title>
调整抓取策略Tuning fetch strategies
</title>
<para>
查询抓取默认的在N+1查询的情况下是极其脆弱的因此我们可能会要求在映射文档中定义使用连接抓取
</para>
<programlisting><![CDATA[<set name="permissions"
fetch="join">
<key column="userId"/>
<one-to-many class="Permission"/>
</set]]></programlisting>
<programlisting><![CDATA[<many-to-one name="mother" class="Cat" fetch="join"/>]]></programlisting>
<para>
在映射文档中定义的抓取策略将会有产生以下影响:
</para>
<itemizedlist>
<listitem>
<para>
通过<literal>get()</literal><literal>load()</literal>方法取得数据。
</para>
</listitem>
<listitem>
<para>
只有在关联之间进行导航时,才会隐式的取得数据(延迟抓取)。
</para>
</listitem>
<listitem>
<para>
<literal>条件查询</literal>
</para>
</listitem>
</itemizedlist>
<para>
通常情况下,我们并不使用映射文档进行抓取策略的定制。更多的是,保持其默认值,然后在特定的事务中,
使用HQL的<literal>左连接抓取left join fetch</literal> 对其进行重载。这将通知
Hibernate在第一次查询中使用外部关联outer join直接得到其关联数据。
<literal>条件查询</literal> API中应该调用 <literal>setFetchMode(FetchMode.JOIN)</literal>语句。
</para>
<para>
也许你喜欢仅仅通过条件查询,就可以改变<literal>get()</literal>
<literal>load()</literal>语句中的数据抓取策略。例如:
</para>
<programlisting><![CDATA[User user = (User) session.createCriteria(User.class)
.setFetchMode("permissions", FetchMode.JOIN)
.add( Restrictions.idEq(userId) )
.uniqueResult();]]></programlisting>
<para>
这就是其他ORM解决方案的“抓取计划(fetch plan)”在Hibernate中的等价物。
</para>
<para>
截然不同的一种避免N+1次查询的方法是使用二级缓存。
</para>
</sect2>
<sect2 id="performance-fetching-proxies" revision="2">
<title>单端关联代理Single-ended association proxies
</title>
<para>
在Hinerbate中对集合的延迟抓取的采用了自己的实现方法。但是对于单端关联的延迟抓取则需要采用
其他不同的机制。单端关联的目标实体必须使用代理Hihernate在运行期二进制级通过优异的CGLIB库
为持久对象实现了延迟载入代理。
</para>
<para>
默认的Hibernate3将会为所有的持久对象产生代理在启动阶段然后使用他们实现
<literal>多对一many-to-one</literal>关联和<literal>一对一one-to-one</literal>
关联的延迟抓取。
</para>
<para>
在映射文件中,可以通过设置<literal>proxy</literal>属性为目标class声明一个接口供代理接口使用。
默认的Hibernate将会使用该类的一个子类。
<emphasis>注意:被代理的类必须实现一个至少包可见的默认构造函数,我们建议所有的持久类都应拥有这样的构造函数</emphasis>
</para>
<para>
在如此方式定义一个多态类的时候,有许多值得注意的常见性的问题,例如:
</para>
<programlisting><![CDATA[<class name="Cat" proxy="Cat">
......
<subclass name="DomesticCat">
.....
</subclass>
</class>]]></programlisting>
<para>
首先,<literal>Cat</literal>实例永远不可以被强制转换为<literal>DomesticCat</literal>,
即使它本身就是<literal>DomesticCat</literal>实例。
</para>
<programlisting><![CDATA[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!
....
}]]></programlisting>
<para>
其次,代理的“<literal>==</literal>”可能不再成立。
</para>
<programlisting><![CDATA[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]]></programlisting>
<para>
虽然如此,但实际情况并没有看上去那么糟糕。虽然我们现在有两个不同的引用,分别指向这两个不同的代理对象,
但实际上,其底层应该是同一个实例对象:
</para>
<programlisting><![CDATA[cat.setWeight(11.0); // hit the db to initialize the proxy
System.out.println( dc.getWeight() ); // 11.0]]></programlisting>
<para>
第三你不能对“final类”或“具有final方法的类”使用CGLIB代理。
</para>
<para>
最后,如果你的持久化对象在实例化时需要某些资源(例如,在实例化方法、默认构造方法中),
那么代理对象也同样需要使用这些资源。实际上,代理类是持久化类的子类。
</para>
<para>
这些问题都源于Java的单根继承模型的天生限制。如果你希望避免这些问题那么你的每个持久化类必须实现一个接口
在此接口中已经声明了其业务方法。然后,你需要在映射文档中再指定这些接口。例如:
</para>
<programlisting><![CDATA[<class name="CatImpl" proxy="Cat">
......
<subclass name="DomesticCatImpl" proxy="DomesticCat">
.....
</subclass>
</class>]]></programlisting>
<para>
这里<literal>CatImpl</literal>实现了<literal>Cat</literal>接口,
<literal>DomesticCatImpl</literal>实现<literal>DomesticCat</literal>接口。
<literal>load()</literal><literal>iterate()</literal>方法中就会返回
<literal>Cat</literal><literal>DomesticCat</literal>的代理对象。
(注意<literal>list()</literal>并不会返回代理对象。)
</para>
<programlisting><![CDATA[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();]]></programlisting>
<para>
这里,对象之间的关系也将被延迟载入。这就意味着,你应该将属性声明为<literal>Cat</literal>,而不是<literal>CatImpl</literal>
</para>
<para>
但是,在有些方法中是<emphasis>不需要</emphasis>使用代理的。例如:
</para>
<itemizedlist spacing="compact">
<listitem>
<para>
<literal>equals()</literal>方法,如果持久类没有重载<literal>equals()</literal>方法。
</para>
</listitem>
<listitem>
<para>
<literal>hashCode()</literal>方法,如果持久类没有重载<literal>hashCode()</literal>方法。
</para>
</listitem>
<listitem>
<para>
标志符的getter方法。
</para>
</listitem>
</itemizedlist>
<para>
Hibernate将会识别出那些重载了<literal>equals()</literal>、或<literal>hashCode()</literal>方法的持久化类。
</para>
</sect2>
<sect2 id="performance-fetching-initialization">
<title>实例化集合和代理Initializing collections and proxies
</title>
<para>
<literal>Session</literal>范围之外访问未初始化的集合或代理Hibernate将会抛出<literal>LazyInitializationException</literal>异常。
也就是说,在分离状态下,访问一个实体所拥有的集合,或者访问其指向代理的属性时,会引发此异常。
</para>
<para>
有时候我们需要保证某个代理或者集合在Session关闭前就已经被初始化了。
当然,我们可以通过强行调用<literal>cat.getSex()</literal>或者<literal>cat.getKittens().size()</literal>之类的方法来确保这一点。
但是这样的程序会造成读者的疑惑,也不符合通常的代码规范。
</para>
<para>
静态方法<literal>Hibernate.initialized()</literal>
为你的应用程序提供了一个便捷的途径来延迟加载集合或代理。
只要它的Session处于open状态<literal>Hibernate.initialize(cat)</literal> 将会为cat强制对代理实例化。
同样,<literal>Hibernate.initialize( cat.getKittens() )</literal> 对kittens的集合具有同样的功能。
</para>
<para>
还有另外一种选择,就是保持<literal>Session</literal>一直处于open状态直到所有需要的集合或代理都被载入。
在某些应用架构中特别是对于那些使用Hibernate进行数据访问的代码以及那些在不同应用层和不同物理进程中使用Hibernate的代码。
在集合实例化时,如何保证<literal>Session</literal>处于open状态经常会是一个问题。有两种方法可以解决此问题
</para>
<itemizedlist>
<listitem>
<para>
在一个基于Web的应用中可以利用servlet过滤器filter在用户请求request结束、页面生成
结束时关闭<literal>Session</literal>(这里使用了<emphasis>在展示层保持打开Session模式Open Session in View</emphasis>
当然,这将依赖于应用框架中异常需要被正确的处理。在返回界面给用户之前,乃至在生成界面过程中发生异常的情况下,
正确关闭<literal>Session</literal>和结束事务将是非常重要的,
Servlet过滤器必须如此访问<literal>Session</literal>才能保证正确使用Session。
我们推荐使用<literal>ThreadLocal</literal> 变量保存当前的<literal>Session</literal>
(可以参考<xref linkend="quickstart-playingwithcats"/>的例子实现)。
</para>
</listitem>
<listitem>
<para>
在一个拥有单独业务层的应用中业务层必须在返回之前为web层“准备”好其所需的数据集合。这就意味着
业务层应该载入所有表现层/web层所需的数据并将这些已实例化完毕的数据返回。通常应用程序应该
为web层所需的每个集合调用<literal>Hibernate.initialize()</literal>这个调用必须发生咱session关闭之前
或者使用带有<literal>FETCH</literal>从句,或<literal>FetchMode.JOIN</literal>的Hibernate查询
事先取得所有的数据集合。如果你在应用中使用了<emphasis>Command</emphasis>模式,代替<emphasis>Session Facade</emphasis>
那么这项任务将会变得简单的多。
</para>
</listitem>
<listitem>
<para>
你也可以通过<literal>merge()</literal><literal>lock()</literal>方法,在访问未实例化的集合(或代理)之前,
为先前载入的对象绑定一个新的<literal>Session</literal>
显然Hibernate将不会也不<emphasis>应该</emphasis>自动完成这些任务,因为这将引入一个特殊的事务语义。
</para>
</listitem>
</itemizedlist>
<para>
有时候,你并不需要完全实例化整个大的集合,仅需要了解它的部分信息(例如其大小)、或者集合的部分内容。
</para>
<para>
你可以使用集合过滤器得到其集合的大小,而不必实例化整个集合:
</para>
<programlisting><![CDATA[( (Integer) s.createFilter( collection, "select count(*)" ).list().get(0) ).intValue()]]></programlisting>
<para>
这里的<literal>createFilter()</literal>方法也可以被用来有效的抓取集合的部分内容,而无需实例化整个集合:
</para>
<programlisting><![CDATA[s.createFilter( lazyCollection, "").setFirstResult(0).setMaxResults(10).list();]]></programlisting>
</sect2>
<sect2 id="performance-fetching-batch">
<title>使用批量抓取Using batch fetching
</title>
<para>
Hibernate可以充分有效的使用批量抓取也就是说如果仅一个访问代理或集合那么Hibernate将不载入其他未实例化的代理。
批量抓取是延迟查询抓取的优化方案,你可以在两种批量抓取方案之间进行选择:在类级别和集合级别。
</para>
<para>
类/实体级别的批量抓取很容易理解。假设你在运行时将需要面对下面的问题:你在一个<literal>Session</literal>中载入了25个
<literal>Cat</literal>实例,每个<literal>Cat</literal>实例都拥有一个引用成员<literal>owner</literal>
其指向<literal>Person</literal>,而<literal>Person</literal>类是代理,同时<literal>lazy="true"</literal>
如果你必须遍历整个cats集合对每个元素调用<literal>getOwner()</literal>方法Hibernate将会默认的执行25次<literal>SELECT</literal>查询,
得到其owner的代理对象。这时你可以通过在映射文件的<literal>Person</literal>属性,显式声明<literal>batch-size</literal>,改变其行为:
</para>
<programlisting><![CDATA[<class name="Person" batch-size="10">...</class>]]></programlisting>
<para>
随之Hibernate将只需要执行三次查询分别为10、10、 5。
</para>
<para>
你也可以在集合级别定义批量抓取。例如,如果每个<literal>Person</literal>都拥有一个延迟载入的<literal>Cats</literal>集合,
现在,<literal>Sesssion</literal>中载入了10个person对象遍历person集合将会引起10次<literal>SELECT</literal>查询,
每次查询都会调用<literal>getCats()</literal>方法。如果你在<literal>Person</literal>的映射定义部分,允许对<literal>cats</literal>批量抓取,
那么Hibernate将可以预先抓取整个集合。请看例子
</para>
<programlisting><![CDATA[<class name="Person">
<set name="cats" batch-size="3">
...
</set>
</class>]]></programlisting>
<para>
如果整个的<literal>batch-size</literal>是3笔误那么Hibernate将会分四次执行<literal>SELECT</literal>查询,
按照3、3、3、1的大小分别载入数据。这里的每次载入的数据量还具体依赖于当前<literal>Session</literal>中未实例化集合的个数。
</para>
<para>
如果你的模型中有嵌套的树状结构例如典型的帐单原料结构bill-of-materials pattern集合的批量抓取是非常有用的。
(尽管在更多情况下对树进行读取时,<emphasis>嵌套集合nested set</emphasis><emphasis>原料路径(materialized path)</emphasis>××
是更好的解决方法。)
</para>
</sect2>
<sect2 id="performance-fetching-subselect">
<title>使用子查询抓取Using subselect fetching
</title>
<para>
假若一个延迟集合或单值代理需要抓取Hibernate会使用一个subselect重新运行原来的查询一次性读入所有的实例。这和批量抓取的实现方法是一样的不会有破碎的加载。
</para>
<!-- TODO: Write more about this -->
</sect2>
<sect2 id="performance-fetching-lazyproperties">
<title>使用延迟属性抓取Using lazy property fetching
</title>
<para>
Hibernate3对单独的属性支持延迟抓取这项优化技术也被称为<emphasis>组抓取fetch groups</emphasis>
请注意,该技术更多的属于市场特性。在实际应用中,优化行读取比优化列读取更重要。但是,仅载入类的部分属性在某些特定情况下会有用,例如在原有表中拥有几百列数据、数据模型无法改动的情况下。
</para>
<para>
可以在映射文件中对特定的属性设置<literal>lazy</literal>,定义该属性为延迟载入。
</para>
<programlisting><![CDATA[<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>]]></programlisting>
<para>
属性的延迟载入要求在其代码构建时加入二进制指示指令bytecode instrumentation如果你的持久类代码中未含有这些指令
Hibernate将会忽略这些属性的延迟设置仍然将其直接载入。
</para>
<para>
你可以在Ant的Task中进行如下定义对持久类代码加入“二进制指令。”
</para>
<programlisting><![CDATA[<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>]]></programlisting>
<para>
还有一种可以优化的方法它使用HQL或条件查询的投影projection特性可以避免读取非必要的列
这一点至少对只读事务是非常有用的。它无需在代码构建时“二进制指令”处理,因此是一个更加值得选择的解决方法。
</para>
<para>
有时你需要在HQL中通过<literal>抓取所有属性</literal>,强行抓取所有内容。
</para>
</sect2>
</sect1>
<sect1 id="performance-cache" revision="1">
<title>二级缓存The Second Level Cache
</title>
<para>
Hibernate的<literal>Session</literal>在事务级别进行持久化数据的缓存操作。
当然,也有可能分别为每个类(或集合)配置集群、或JVM级别(<literal>SessionFactory级别</literal>)的缓存。
你甚至可以为之插入一个集群的缓存。注意,缓存永远不知道其他应用程序对持久化仓库(数据库)可能进行的修改
(即使可以将缓存数据设定为定期失效)。
</para>
<para>
默认情况下Hibernate使用EHCache进行JVM级别的缓存目前Hibernate已经废弃了对JCS的支持未来版本中将会去掉它
你可以通过设置<literal>hibernate.cache.provider_class</literal>属性,指定其他的缓存策略,
该缓存策略必须实现<literal>org.hibernate.cache.CacheProvider</literal>接口。
</para>
<table frame="topbot" id="cacheproviders" revision="1">
<title>
缓存策略提供商Cache Providers
</title>
<tgroup cols='5' align='left' colsep='1' rowsep='1'>
<colspec colname='c1' colwidth="1*"/>
<colspec colname='c2' colwidth="3*"/>
<colspec colname='c3' colwidth="1*"/>
<colspec colname='c4' colwidth="1*"/>
<colspec colname='c5' colwidth="1*"/>
<thead>
<row>
<entry>Cache</entry>
<entry>Provider class</entry>
<entry>Type</entry>
<entry>Cluster Safe</entry>
<entry>Query Cache Supported</entry>
</row>
</thead>
<tbody>
<row>
<entry>Hashtable (not intended for production use)</entry>
<entry><literal>org.hibernate.cache.HashtableCacheProvider</literal></entry>
<entry>memory</entry>
<entry></entry>
<entry>yes</entry>
</row>
<row>
<entry>EHCache</entry>
<entry><literal>org.hibernate.cache.EhCacheProvider</literal></entry>
<entry>memory, disk</entry>
<entry></entry>
<entry>yes</entry>
</row>
<row>
<entry>OSCache</entry>
<entry><literal>org.hibernate.cache.OSCacheProvider</literal></entry>
<entry>memory, disk</entry>
<entry></entry>
<entry>yes</entry>
</row>
<row>
<entry>SwarmCache</entry>
<entry><literal>org.hibernate.cache.SwarmCacheProvider</literal></entry>
<entry>clustered (ip multicast)</entry>
<entry>yes (clustered invalidation)</entry>
<entry></entry>
</row>
<row>
<entry>JBoss TreeCache</entry>
<entry><literal>org.hibernate.cache.TreeCacheProvider</literal></entry>
<entry>clustered (ip multicast), transactional</entry>
<entry>yes (replication)</entry>
<entry>yes (clock sync req.)</entry>
</row>
</tbody>
</tgroup>
</table>
<sect2 id="performance-cache-mapping">
<title>缓存映射Cache mappings
</title>
<para>
类或者集合映射的“<literal>&lt;cache&gt;</literal>元素”可以有下列形式:
</para>
<programlistingco>
<areaspec>
<area id="cache1" coords="2 70"/>
</areaspec>
<programlisting><![CDATA[<cache
usage="transactional|read-write|nonstrict-read-write|read-only"
/>]]></programlisting>
<calloutlist>
<callout arearefs="cache1">
<para>
<literal>usage</literal>说明了缓存的策略:
<literal>transactional</literal>
<literal>read-write</literal>
<literal>nonstrict-read-write</literal>
<literal>read-only</literal>
</para>
</callout>
</calloutlist>
</programlistingco>
<para>
另外(首选?), 你可以在hibernate.cfg.xml中指定<literal>&lt;class-cache&gt;</literal>
<literal>&lt;collection-cache&gt;</literal> 元素。
</para>
<para>
这里的<literal>usage</literal> 属性指明了<emphasis>缓存并发策略cache concurrency strategy</emphasis>
</para>
</sect2>
<sect2 id="performance-cache-readonly">
<title>策略只读缓存Strategy: read only
</title>
<para>
如果你的应用程序只需读取一个持久化类的实例,而无需对其修改,
那么就可以对其进行<literal>只读</literal> 缓存。这是最简单,也是实用性最好的方法。甚至在集群中,它也能完美地运作。
</para>
<programlisting><![CDATA[<class name="eg.Immutable" mutable="false">
<cache usage="read-only"/>
....
</class>]]></programlisting>
</sect2>
<sect2 id="performance-cache-readwrite">
<title>
策略:读/写缓存Strategy: read/write
</title>
<para>
如果应用程序需要更新数据,那么使用<literal>读/写缓存</literal> 比较合适。
如果应用程序要求“序列化事务”的隔离级别serializable transaction isolation level那么就决不能使用这种缓存策略。
如果在JTA环境中使用缓存你必须指定<literal>hibernate.transaction.manager_lookup_class</literal>属性的值,
通过它Hibernate才能知道该应用程序中JTA的<literal>TransactionManager</literal>的具体策略。
在其它环境中,你必须保证在<literal>Session.close()</literal>、或<literal>Session.disconnect()</literal>调用前,
整个事务已经结束。 如果你想在集群环境中使用此策略,你必须保证底层的缓存实现支持锁定(locking)。Hibernate内置的缓存策略并不支持锁定功能。
</para>
<programlisting><![CDATA[<class name="eg.Cat" .... >
<cache usage="read-write"/>
....
<set name="kittens" ... >
<cache usage="read-write"/>
....
</set>
</class>]]></programlisting>
</sect2>
<sect2 id="performance-cache-nonstrict">
<title>
策略:非严格读/写缓存Strategy: nonstrict read/write
</title>
<para>
如果应用程序只偶尔需要更新数据(也就是说,两个事务同时更新同一记录的情况很不常见),也不需要十分严格的事务隔离,
那么比较适合使用<literal>非严格读/写缓存</literal>策略。如果在JTA环境中使用该策略
你必须为其指定<literal>hibernate.transaction.manager_lookup_class</literal>属性的值,
在其它环境中,你必须保证在<literal>Session.close()</literal>、或<literal>Session.disconnect()</literal>调用前,
整个事务已经结束。
</para>
</sect2>
<sect2 id="performance-cache-transactional">
<title>
策略:事务缓存transactional
</title>
<para>
Hibernate的<literal>事务缓存</literal>策略提供了全事务的缓存支持,
例如对JBoss TreeCache的支持。这样的缓存只能用于JTA环境中你必须指定
为其<literal>hibernate.transaction.manager_lookup_class</literal>属性。
</para>
</sect2>
<para>
没有一种缓存提供商能够支持上列的所有缓存并发策略。下表中列出了各种提供器、及其各自适用的并发策略。
</para>
<table frame="topbot">
<title>
各种缓存提供商对缓存并发策略的支持情况Cache Concurrency Strategy Support
</title>
<tgroup cols='5' align='left' colsep='1' rowsep='1'>
<colspec colname='c1' colwidth="1*"/>
<colspec colname='c2' colwidth="1*"/>
<colspec colname='c3' colwidth="1*"/>
<colspec colname='c4' colwidth="1*"/>
<colspec colname='c5' colwidth="1*"/>
<thead>
<row>
<entry>Cache</entry>
<entry>read-only</entry>
<entry>nonstrict-read-write</entry>
<entry>read-write</entry>
<entry>transactional</entry>
</row>
</thead>
<tbody>
<row>
<entry>Hashtable (not intended for production use)</entry>
<entry>yes</entry>
<entry>yes</entry>
<entry>yes</entry>
<entry></entry>
</row>
<row>
<entry>EHCache</entry>
<entry>yes</entry>
<entry>yes</entry>
<entry>yes</entry>
<entry></entry>
</row>
<row>
<entry>OSCache</entry>
<entry>yes</entry>
<entry>yes</entry>
<entry>yes</entry>
<entry></entry>
</row>
<row>
<entry>SwarmCache</entry>
<entry>yes</entry>
<entry>yes</entry>
<entry></entry>
<entry></entry>
</row>
<row>
<entry>JBoss TreeCache</entry>
<entry>yes</entry>
<entry></entry>
<entry></entry>
<entry>yes</entry>
</row>
</tbody>
</tgroup>
</table>
</sect1>
<sect1 id="performance-sessioncache" revision="2">
<title>
管理缓存Managing the caches
</title>
<para>
无论何时,当你给<literal>save()</literal><literal>update()</literal>
<literal>saveOrUpdate()</literal>方法传递一个对象时,或使用<literal>load()</literal>
<literal>get()</literal><literal>list()</literal><literal>iterate()</literal>
<literal>scroll()</literal>方法获得一个对象时,
该对象都将被加入到<literal>Session</literal>的内部缓存中。
</para>
<para>
当随后flush()方法被调用时,对象的状态会和数据库取得同步。
如果你不希望此同步操作发生,或者你正处理大量对象、需要对有效管理内存时,你可以调用<literal>evict()</literal>
方法,从一级缓存中去掉这些对象及其集合。
</para>
<programlisting><![CDATA[ScrollableResult cats = sess.createQuery("from Cat as cat").scroll(); //a huge result set
while ( cats.next() ) {
Cat cat = (Cat) cats.get(0);
doSomethingWithACat(cat);
sess.evict(cat);
}]]></programlisting>
<para>
Session还提供了一个<literal>contains()</literal>方法用来判断某个实例是否处于当前session的缓存中。
</para>
<para>
如若要把所有的对象从session缓存中彻底清除则需要调用<literal>Session.clear()</literal>
</para>
<para>
对于二级缓存来说,在<literal>SessionFactory</literal>中定义了许多方法,
清除缓存中实例、整个类、集合实例或者整个集合。
</para>
<programlisting><![CDATA[sessionFactory.evict(Cat.class, catId); //evict a particular Cat
sessionFactory.evict(Cat.class); //evict all Cats
sessionFactory.evictCollection("Cat.kittens", catId); //evict a particular collection of kittens
sessionFactory.evictCollection("Cat.kittens"); //evict all kitten collections]]></programlisting>
<para>
<literal>CacheMode</literal>参数用于控制具体的Session如何与二级缓存进行交互。
</para>
<itemizedlist>
<listitem>
<para>
<literal>CacheMode.NORMAL</literal> - 从二级缓存中读、写数据。
</para>
</listitem>
<listitem>
<para>
<literal>CacheMode.GET</literal> - 从二级缓存中读取数据,仅在数据更新时对二级缓存写数据。
</para>
</listitem>
<listitem>
<para>
<literal>CacheMode.PUT</literal> - 仅向二级缓存写数据,但不从二级缓存中读数据。
</para>
</listitem>
<listitem>
<para>
<literal>CacheMode.REFRESH</literal> - 仅向二级缓存写数据,但不从二级缓存中读数据。通过
<literal>hibernate.cache.use_minimal_puts</literal>的设置,强制二级缓存从数据库中读取数据,刷新缓存内容。
</para>
</listitem>
</itemizedlist>
<para>
如若需要查看二级缓存或查询缓存区域的内容,你可以使用<literal>统计Statistics</literal> API。
</para>
<programlisting><![CDATA[Map cacheEntries = sessionFactory.getStatistics()
.getSecondLevelCacheStatistics(regionName)
.getEntries();]]></programlisting>
<para>
此时你必须手工打开统计选项。可选的你可以让Hibernate更人工可读的方式维护缓存内容。
</para>
<programlisting><![CDATA[hibernate.generate_statistics true
hibernate.cache.use_structured_entries true]]></programlisting>
</sect1>
<sect1 id="performance-querycache" revision="1">
<title>查询缓存The Query Cache
</title>
<para>
查询的结果集也可以被缓存。只有当经常使用同样的参数进行查询时,这才会有些用处。
要使用查询缓存,首先你必须打开它:
</para>
<programlisting><![CDATA[hibernate.cache.use_query_cache true]]></programlisting>
<para>
该设置将会创建两个缓存区域 - 一个用于保存查询结果集(<literal>org.hibernate.cache.StandardQueryCache</literal>)
另一个则用于保存最近查询的一系列表的时间戳(<literal>org.hibernate.cache.UpdateTimestampsCache</literal>)。
请注意:在查询缓存中,它并不缓存结果集中所包含的实体的确切状态;它只缓存这些实体的标识符属性的值、以及各值类型的结果。
所以查询缓存通常会和二级缓存一起使用。
</para>
<para>
绝大多数的查询并不能从查询缓存中受益所以Hibernate默认是不进行查询缓存的。如若需要进行缓存请调用
<literal>Query.setCacheable(true)</literal>方法。这个调用会让查询在执行过程中时先从缓存中查找结果,
并将自己的结果集放到缓存中去。
</para>
<para>
如果你要对查询缓存的失效政策进行精确的控制,你必须调用<literal>Query.setCacheRegion()</literal>方法,
为每个查询指定其命名的缓存区域。
</para>
<programlisting><![CDATA[List blogs = sess.createQuery("from Blog blog where blog.blogger = :blogger")
.setEntity("blogger", blogger)
.setMaxResults(15)
.setCacheable(true)
.setCacheRegion("frontpages")
.list();]]></programlisting>
<para>
如果查询需要强行刷新其查询缓存区域,那么你应该调用<literal>Query.setCacheMode(CacheMode.REFRESH)</literal>方法。
这对在其他进程中修改底层数据例如不通过Hibernate修改数据或对那些需要选择性更新特定查询结果集的情况特别有用。
这是对<literal>SessionFactory.evictQueries()</literal>的更为有效的替代方案,同样可以清除查询缓存区域。
</para>
</sect1>
<sect1 id="performance-collections">
<title>
理解集合性能Understanding Collection performance
</title>
<para>
前面我们已经对集合进行了足够的讨论。本段中,我们将着重讲述集合在运行时的事宜。
</para>
<sect2 id="performance-collections-taxonomy">
<title>
分类Taxonomy
</title>
<para>
Hibernate定义了三种基本类型的集合
</para>
<itemizedlist>
<listitem>
<para>
值数据集合
</para>
</listitem>
<listitem>
<para>
一对多关联
</para>
</listitem>
<listitem>
<para>
多对多关联
</para>
</listitem>
</itemizedlist>
<para>
这个分类是区分了不同的表和外键关系类型,但是它没有告诉我们关系模型的所有内容。
要完全理解他们的关系结构和性能特点我们必须同时考虑“用于Hibernate更新或删除集合行数据的主键的结构”。
因此得到了如下的分类:
</para>
<itemizedlist>
<listitem>
<para>
有序集合类
</para>
</listitem>
<listitem>
<para>
集合sets
</para>
</listitem>
<listitem>
<para>
bags)
</para>
</listitem>
</itemizedlist>
<para>
所有的有序集合类maps, lists, arrays)都拥有一个由<literal>&lt;key&gt;</literal>
<literal>&lt;index&gt;</literal>组成的主键。
这种情况下集合类的更新是非常高效的——主键已经被有效的索引因此当Hibernate试图更新或删除一行时可以迅速找到该行数据。
</para>
<para>
集合(sets)的主键由<literal>&lt;key&gt;</literal>和其他元素字段构成。
对于有些元素类型来说,这很低效,特别是组合元素或者大文本、大二进制字段;
数据库可能无法有效的对复杂的主键进行索引。
另一方面,对于一对多、多对多关联,特别是合成的标识符来说,集合也可以达到同样的高效性能。(
附注:如果你希望<literal>SchemaExport</literal>为你的<literal>&lt;set&gt;</literal>创建主键,
你必须把所有的字段都声明为<literal>not-null="true"</literal>。)
</para>
<para>
<literal>&lt;idbag&gt;</literal>映射定义了代理键,因此它总是可以很高效的被更新。事实上,
<literal>&lt;idbag&gt;</literal>拥有着最好的性能表现。
</para>
<para>
Bag是最差的。因为bag允许重复的元素值也没有索引字段因此不可能定义主键。
Hibernate无法判断出重复的行。当这种集合被更改时Hibernate将会先完整地移除
(通过一个(in a single <literal>DELETE</literal>))整个集合,然后再重新创建整个集合。
因此Bag是非常低效的。
</para>
<para>
请注意:对于一对多关联来说,“主键”很可能并不是数据库表的物理主键。
但就算在此情况下上面的分类仍然是有用的。它仍然反映了Hibernate在集合的各数据行中是如何进行“定位”的。
</para>
</sect2>
<sect2 id="performance-collections-mostefficientupdate">
<title>
Lists, maps 和sets用于更新效率最高
</title>
<para>
根据我们上面的讨论显然有序集合类型和大多数set都可以在增加、删除、修改元素中拥有最好的性能。
</para>
<para>
可论证的是对于多对多关联、值数据集合而言,有序集合类比集合(set)有一个好处。因为<literal>Set</literal>的内在结构,
如果“改变”了一个元素Hibernate并不会<literal>更新UPDATE</literal>这一行。
对于<literal>Set</literal>来说,只有在<literal>插入INSERT</literal><literal>删除DELETE</literal>
操作时“改变”才有效。再次强调:这段讨论对“一对多关联”并不适用。
</para>
<para>
注意到数组无法延迟载入我们可以得出结论list, map和idbags是最高效的非反向集合类型set则紧随其后。
在Hibernate中set应该时最通用的集合类型这时因为“set”的语义在关系模型中是最自然的。
</para>
<para>
但是在设计良好的Hibernate领域模型中我们通常可以看到更多的集合事实上是带有<literal>inverse="true"</literal>
的一对多的关联。对于这些关联,更新操作将会在多对一的这一端进行处理。因此对于此类情况,无需考虑其集合的更新性能。
</para>
</sect2>
<sect2 id="performance-collections-mostefficentinverse">
<title>
Bag和list是反向集合类中效率最高的
</title>
<para>
在把bag扔进水沟之前你必须了解在一种情况下bag的性能(包括list)要比set高得多
对于指明了<literal>inverse="true"</literal>的集合类(比如说,标准的双向的一对多关联),
我们可以在未初始化(fetch)包元素的情况下直接向bag或list添加新元素
这是因为<literal>Collection.add()</literal>)或者<literal>Collection.addAll()</literal> 方法
对bag或者List总是返回true这点与与Set不同。因此对于下面的相同代码来说速度会快得多。
</para>
<programlisting><![CDATA[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();]]></programlisting>
</sect2>
<sect2 id="performance-collections-oneshotdelete">
<title>
一次性删除One shot delete
</title>
<para>
偶尔的逐个删除集合类中的元素是相当低效的。Hibernate并没那么笨
如果你想要把整个集合都删除比如说调用list.clear()Hibernate只需要一个DELETE就搞定了。
</para>
<para>
假设我们在一个长度为20的集合类中新增加了一个元素然后再删除两个。
Hibernate会安排一条<literal>INSERT</literal>语句和两条<literal>DELETE</literal>语句除非集合类是一个bag)。
这当然是显而易见的。
</para>
<para>
但是假设我们删除了18个数据只剩下2个然后新增3个。则有两种处理方式
</para>
<itemizedlist>
<listitem>
<para>
逐一的删除这18个数据再新增三个
</para>
</listitem>
<listitem>
<para>
删除整个集合类只用一句DELETE语句然后增加5个数据。
</para>
</listitem>
</itemizedlist>
<para>
Hibernate还没那么聪明知道第二种选择可能会比较快。
也许让Hibernate不这么聪明也是好事否则可能会引发意外的“数据库触发器”之类的问题。
</para>
<para>
幸运的是,你可以强制使用第二种策略。你需要取消原来的整个集合类(解除其引用),
然后再返回一个新的实例化的集合类,只包含需要的元素。有些时候这是非常有用的。
</para>
<para>
显然,一次性删除并不适用于被映射为<literal>inverse="true"</literal>的集合。
</para>
</sect2>
</sect1>
<sect1 id="performance-monitoring" revision="1">
<title>
监测性能Monitoring performance
</title>
<para>
没有监测和性能参数而进行优化是毫无意义的。Hibernate为其内部操作提供了一系列的示意图因此可以从
每个<literal>SessionFactory</literal>抓取其统计数据。
</para>
<sect2 id="performance-monitoring-sf" revision="2">
<title>
监测SessionFactory
</title>
<para>
你可以有两种方式访问<literal>SessionFactory</literal>的数据记录,第一种就是自己直接调用
<literal>sessionFactory.getStatistics()</literal>方法读取、显示<literal>统计</literal>数据。
</para>
<para>
此外,如果你打开<literal>StatisticsService</literal> MBean选项那么Hibernate则可以使用JMX技术
发布其数据记录。你可以让应用中所有的<literal>SessionFactory</literal>同时共享一个MBean也可以每个
SessionFactory分配一个MBean。下面的代码即是其演示代码
</para>
<programlisting><![CDATA[// MBean service registration for a specific SessionFactory
Hashtable tb = new Hashtable();
tb.put("type", "statistics");
tb.put("sessionFactory", "myFinancialApp");
ObjectName on = new ObjectName("hibernate", tb); // MBean object name
StatisticsService stats = new StatisticsService(); // MBean implementation
stats.setSessionFactory(sessionFactory); // Bind the stats to a SessionFactory
server.registerMBean(stats, on); // Register the Mbean on the server]]></programlisting>
<programlisting><![CDATA[// MBean service registration for all SessionFactory's
Hashtable tb = new Hashtable();
tb.put("type", "statistics");
tb.put("sessionFactory", "all");
ObjectName on = new ObjectName("hibernate", tb); // MBean object name
StatisticsService stats = new StatisticsService(); // MBean implementation
server.registerMBean(stats, on); // Register the MBean on the server]]></programlisting>
<para>
TODO仍需要说明的是在第一个例子中我们直接得到和使用MBean而在第二个例子中在使用MBean之前
我们则需要给出SessionFactory的JNDI名使用<literal>hibernateStatsBean.setSessionFactoryJNDIName("my/JNDI/Name")</literal>
得到SessionFactory然后将MBean保存于其中。
</para>
<para>
你可以通过以下方法打开或关闭<literal>SessionFactory</literal>的监测功能:
</para>
<itemizedlist>
<listitem>
<para>
在配置期间,将<literal>hibernate.generate_statistics</literal>设置为<literal>true</literal><literal>false</literal>
</para>
</listitem>
</itemizedlist>
<itemizedlist>
<listitem>
<para>
在运行期间,则可以可以通过<literal>sf.getStatistics().setStatisticsEnabled(true)</literal>
<literal>hibernateStatsBean.setStatisticsEnabled(true)</literal>
</para>
</listitem>
</itemizedlist>
<para>
你也可以在程序中调用<literal>clear()</literal>方法重置统计数据,调用<literal>logSummary()</literal>
在日志中记录info级别其总结。
</para>
</sect2>
<sect2 id="performance-monitoring-metrics" revision="1">
<title>
数据记录Metrics
</title>
<para>
Hibernate提供了一系列数据记录其记录的内容包括从最基本的信息到与具体场景的特殊信息。所有的测量值都可以由
<literal>Statistics</literal>接口进行访问,主要分为三类:
</para>
<itemizedlist>
<listitem>
<para>
使用<literal>Session</literal>的普通数据记录例如打开的Session的个数、取得的JDBC的连接数等
</para>
</listitem>
<listitem>
<para>
实体、集合、查询、缓存等内容的统一数据记录
</para>
</listitem>
<listitem>
<para>
和具体实体、集合、查询、缓存相关的详细数据记录
</para>
</listitem>
</itemizedlist>
<para>
例如:你可以检查缓存的命中成功次数,缓存的命中失败次数,实体、集合和查询的使用概率,查询的平均时间等。请注意
Java中时间的近似精度是毫秒。Hibernate的数据精度和具体的JVM有关在有些平台上其精度甚至只能精确到10秒。
</para>
<para>
你可以直接使用getter方法得到全局数据记录例如和具体的实体、集合、缓存区无关的数据你也可以在具体查询中通过标记实体名、
或HQL、SQL语句得到某实体的数据记录。请参考<literal>Statistics</literal><literal>EntityStatistics</literal>
<literal>CollectionStatistics</literal><literal>SecondLevelCacheStatistics</literal>
<literal>QueryStatistics</literal>的API文档以抓取更多信息。下面的代码则是个简单的例子
</para>
<programlisting><![CDATA[Statistics stats = HibernateUtil.sessionFactory.getStatistics();
double queryCacheHitCount = stats.getQueryCacheHitCount();
double queryCacheMissCount = stats.getQueryCacheMissCount();
double queryCacheHitRatio =
queryCacheHitCount / (queryCacheHitCount + queryCacheMissCount);
log.info("Query Hit ratio:" + queryCacheHitRatio);
EntityStatistics entityStats =
stats.getEntityStatistics( Cat.class.getName() );
long changes =
entityStats.getInsertCount()
+ entityStats.getUpdateCount()
+ entityStats.getDeleteCount();
log.info(Cat.class.getName() + " changed " + changes + "times" );]]></programlisting>
<para>
如果你想得到所有实体、集合、查询和缓存区的数据,你可以通过以下方法获得实体、集合、查询和缓存区列表:
<literal>getQueries()</literal><literal>getEntityNames()</literal>
<literal>getCollectionRoleNames()</literal>
<literal>getSecondLevelCacheRegionNames()</literal>
</para>
</sect2>
</sect1>
</chapter>