Hibernate.orgCommunity Documentation

第 21 章 提升性能

21.1. 抓取策略(Fetching strategies)
21.1.1. 操作延迟加载的关联
21.1.2. 调整抓取策略(Tuning fetch strategies)
21.1.3. 单端关联代理(Single-ended association proxies)
21.1.4. 实例化集合和代理(Initializing collections and proxies)
21.1.5. 使用批量抓取(Using batch fetching)
21.1.6. 使用子查询抓取(Using subselect fetching)
21.1.7. Fetch profile(抓取策略)
21.1.8. 使用延迟属性抓取(Using lazy property fetching)
21.2. 二级缓存(The Second Level Cache)
21.2.1. 缓存映射(Cache mappings)
21.2.2. 策略:只读缓存(Strategy:read only)
21.2.3. 策略:读写/缓存(Strategy:read/write)
21.2.4. 策略:非严格读/写缓存(Strategy:nonstrict read/write)
21.2.5. 策略:事务缓存(transactional)
21.2.6. 各种缓存提供商/缓存并发策略的兼容性
21.3. 管理缓存(Managing the caches)
21.4. 查询缓存(The Query Cache)
21.4.1. 启用查询缓存
21.4.2. 查询缓存区
21.5. 理解集合性能(Understanding Collection performance)
21.5.1. 分类(Taxonomy)
21.5.2. Lists,maps 和 sets 用于更新效率最高
21.5.3. Bag 和 list 是反向集合类中效率最高的
21.5.4. 一次性删除(One shot delete)
21.6. 监测性能(Monitoring performance)
21.6.1. 监测 SessionFactory
21.6.2. 数据记录(Metrics)

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

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

Hibernate 会区分下列各种情况:

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

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

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

然而,你必须了解延迟抓取带来的一个问题。在一个打开的 Hibernate session 上下文之外调用延迟集合会导致一次意外。比如:

= 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 读取数据的代码移到事务提交之前。

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

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

查询抓取(默认的)在 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"/>

在映射文档中定义的抓取策略将会对以下列表条目产生影响:

不管你使用哪种抓取策略,定义为非延迟的类图会被保证一定装载入内存。注意这可能意味着在一条 HQL 查询后紧跟着一系列的查询。

通常情况下,我们并不使用映射文档进行抓取策略的定制。更多的是,保持其默认值,然后在特定的事务中, 使用 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 次查询的方法是,使用二级缓存。

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


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

然后,load()iterate() 永远也不会返回 CatDomesticCat 实例的代理。

Cat cat = (Cat) session.load(CatImpl.class, catid);

Iterator iter = session.createQuery("from CatImpl as cat where cat.name='fritz'").iterate();
Cat fritz = (Cat) iter.next();

这里,对象之间的关系也将被延迟载入。这就意味着,你应该将属性声明为 Cat,而不是 CatImpl

有些方法中是需要代理初始化的:

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

若选择 lazy="no-proxy" 而非默认的 lazy="proxy",我们可以避免类型转换带来的问题。然而,这样我们就需要编译期字节码增强,并且所有的操作都会导致立刻进行代理初始化。

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 状态经常会是一个问题。有两种方法可以解决此问题:

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

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

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

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

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

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)可能是更好的解决方法。)

Another way to affect the fetching strategy for loading associated objects is through something called a fetch profile, which is a named configuration associated with the org.hibernate.SessionFactory but enabled, by name, on the org.hibernate.Session. Once enabled on a org.hibernate.Session, the fetch profile will be in affect for that org.hibernate.Session until it is explicitly disabled.

So what does that mean? Well lets explain that by way of an example which show the different available approaches to configure a fetch profile:




Now normally when you get a reference to a particular customer, that customer's set of orders will be lazy meaning we will not yet have loaded those orders from the database. Normally this is a good thing. Now lets say that you have a certain use case where it is more efficient to load the customer and their orders together. One way certainly is to use "dynamic fetching" strategies via an HQL or criteria queries. But another option is to use a fetch profile to achieve that. The following code will load both the customer andtheir orders:


注意

@FetchProfile definitions are global and it does not matter on which class you place them. You can place the @FetchProfile annotation either onto a class or package (package-info.java). In order to define multiple fetch profiles for the same class or package @FetchProfiles can be used.

目前只有 join 风格的抓取策略被支持,但其他风格也将被支持。更多细节请参考 HHH-3414

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 中通过抓取所有属性,强行抓取所有内容。

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

You have the option to tell Hibernate which caching implementation to use by specifying the name of a class that implements org.hibernate.cache.CacheProvider using the property hibernate.cache.provider_class. Hibernate is bundled with a number of built-in integrations with the open-source cache providers that are listed in 表 21.1 “缓存策略提供商(Cache Providers)”. You can also implement your own and plug it in as outlined above. Note that versions prior to Hibernate 3.2 use EhCache as the default cache provider.


As we have done in previous chapters we are looking at the two different possibiltites to configure caching. First configuration via annotations and then via Hibernate mapping files.

By default, entities are not part of the second level cache and we recommend you to stick to this setting. However, you can override this by setting the shared-cache-mode element in your persistence.xml file or by using the javax.persistence.sharedCache.mode property in your configuration. The following values are possible:

The cache concurrency strategy used by default can be set globaly via the hibernate.cache.default_cache_concurrency_strategy configuration property. The values for this property are:


Hibernate also let's you cache the content of a collection or the identifiers if the collection contains other entities. Use the @Cache annotation on the collection property.


例 21.7 “@Cache annotation with attributes”shows the @org.hibernate.annotations.Cache annotations with its attributes. It allows you to define the caching strategy and region of a given second level cache.


Let's now take a look at Hibernate mapping files. There the <cache> element of a class or collection mapping is used to configure the second level cache. Looking at 例 21.8 “The Hibernate <cache> mapping element” the parallels to anotations is obvious.


Alternatively to <cache>, you can use <class-cache> and <collection-cache> elements in hibernate.cfg.xml.

Let's now have a closer look at the different usage strategies

无论何时,当你给 save()update()saveOrUpdate() 方法传递一个对象时,或使用 load()get()list()iterate()scroll() 方法获得一个对象时,该对象都将被加入到 Session 的内部缓存中。

当随后 flush() 方法被调用时,对象的状态会和数据库取得同步。如果你不希望此同步操作发生,或者你正处理大量对象、需要对有效管理内存时,你可以调用 evict() 方法,从一级缓存中去掉这些对象及其集合。


Session 还提供了一个 contains() 方法,用来判断某个实例是否处于当前 session 的缓存中。

如若要把所有的对象从 session 缓存中彻底清除,则需要调用 Session.clear()

对于二级缓存来说,在 SessionFactory 中定义了许多方法,清除缓存中实例、整个类、集合实例或者整个集合。


CacheMode 参数用于控制具体的 Session 如何与二级缓存进行交互。

  • CacheMode.NORMAL:从二级缓存中读、写数据。

  • CacheMode.GET:从二级缓存中读取数据,仅在数据更新时对二级缓存写数据。

  • CacheMode.PUT:仅向二级缓存写数据,但不从二级缓存中读数据。

  • CacheMode.REFRESH:仅向二级缓存写数据,但不从二级缓存中读数据。通过 hibernate.cache.use_minimal_puts 的设置,强制二级缓存从数据库中读取数据,刷新缓存内容。

如若需要查看二级缓存或查询缓存区域的内容,你可以使用统计(Statistics) API。


此时,你必须手工打开统计选项。可选的,你可以让 Hibernate 更人工可读的方式维护缓存内容。


查询的结果集也可以被缓存。只有当经常使用同样的参数进行查询时,这才会有些用处。

按照应用程序的事务性处理过程,查询结果的缓存将产生一些负荷。例如,如果缓存针对 Person 的查询结果,在 Person 发生了修改时,Hibernate 将需要跟踪这些结果什么时候失效。因为大多数应用程序不会从缓存查询结果中受益,所以 Hibernate 在缺省情况下将禁用缓存。要使用查询缓存,你首先需要启用查询缓存:

hibernate.cache.use_query_cache true

这个设置创建了两个新的缓存 region:

如上面所提及的,绝大多数的查询并不能从查询缓存中受益,所以 Hibernate 默认是不进行查询缓存的。如若需要进行缓存,请调用 org.hibernate.Query.setCacheable(true)方法。这个调用会让查询在执行过程中时先从缓存中查找结果,并将自己的结果集放到缓存中去。

在前面的章节里我们已经讨论了集合和相关应用程序。在本节我么将探索运行时集合的更多问题。

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

这个分类是区分了不同的表和外键关系类型,但是它没有告诉我们关系模型的所有内容。 要完全理解他们的关系结构和性能特点,我们必须同时考虑“用于 Hibernate 更新或删除集合行数据的主键的结构”。因此得到了如下的分类:

所有的有序集合类(maps,lists,arrays)都拥有一个由 <key><index> 组成的主键。这种情况下集合类的更新是非常高效的 — 主键已经被有效的索引,因此当 Hibernate 试图更新或删除一行时,可以迅速找到该行数据。

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

<idbag> 映射定义了代理键,因此它总是可以很高效的被更新。事实上,<idbag> 拥有着最好的性能表现。

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

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

没有监测和性能参数而进行优化是毫无意义的。Hibernate 为其内部操作提供了一系列的示意图,因此可以从 每个 SessionFactory 抓取其统计数据。

你可以有两种方式访问 SessionFactory 的数据记录,第一种就是自己直接调用 sessionFactory.getStatistics() 方法读取、显示统计数据。

此外,如果你打开 StatisticsService MBean 选项,那么 Hibernate 则可以使用 JMX 技术 发布其数据记录。你可以让应用中所有的 SessionFactory 同时共享一个 MBean,也可以每个 SessionFactory 分配一个 MBean。下面的代码即是其演示代码:

// 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
// 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

你可以通过以下方法打开或关闭 SessionFactory 的监测功能:

你也可以在程序中调用 clear() 方法重置统计数据,调用 logSummary() 在日志中记录(info 级别)其总结。

Hibernate 提供了一系列数据记录,其记录的内容包括从最基本的信息到与具体场景的特殊信息。所有的测量值都可以由 Statistics 接口 API 进行访问,主要分为三类:

例如:你可以检查缓存的命中成功次数,缓存的命中失败次数,实体、集合和查询的使用概率,查询的平均时间等。请注意 Java 中时间的近似精度是毫秒。Hibernate 的数据精度和具体的 JVM 有关,在有些平台上其精度甚至只能精确到 10 秒。

你可以直接使用 getter 方法得到全局数据记录(例如,和具体的实体、集合、缓存区无关的数据),你也可以在具体查询中通过标记实体名、 或 HQL、SQL 语句得到某实体的数据记录。请参考 StatisticsEntityStatisticsCollectionStatisticsSecondLevelCacheStatisticsQueryStatistics 的 API 文档以抓取更多信息。下面的代码则是个简单的例子:

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"  );

如果你想得到所有实体、集合、查询和缓存区的数据,你可以通过以下方法获得实体、集合、查询和缓存区列表:getQueries()getEntityNames()getCollectionRoleNames()getSecondLevelCacheRegionNames()