Hibernate.orgCommunity Documentation
当应用程序需要在(Hibernate实体对象图的)关联关系间进行导航的时候,Hibernate 使用 抓取策略(fetching strategy) 获取关联对象。抓取策略可以在 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,延迟集合抓取:直到应用程序对集合进行了一次操作时,集合才被抓取(对集合而言这是默认行为)。
"Extra-lazy" collection fetching,"Extra-lazy" 集合抓取:对集合类中的每个元素而言,都是直到需要时才去访问数据库。除非绝对必要,Hibernate 不会试图去把整个集合都抓取到内存里来(适用于非常大的集合)。
Proxy fetching,代理抓取:对返回单值的关联而言,当其某个方法被调用,而非对其关键字进行 get 操作时才抓取。
"No-proxy" fetching,非代理抓取:对返回单值的关联而言,当实例变量被访问的时候进行抓取。与上面的代理抓取相比,这种方法没有那么“延迟”得厉害(就算只访问标识符,也会导致关联抓取)但是更加透明,因为对应用程序来说,不再看到 proxy。这种方法需要在编译期间进行字节码增强操作,因此很少需要用到。
Lazy attribute fetching,属性延迟加载:对属性或返回单值的关联而言,当其实例变量被访问的时候进行抓取。需要编译期字节码强化,因此这一方法很少是必要的。
这里有两个正交的概念:关联何时被抓取,以及被如何抓取(会采用什么样的 SQL 语句)。注意不要混淆它们。我们使用抓取
来改善性能。我们使用延迟
来定义一些契约,对某特定类的某个脱管的实例,知道有哪些数据是可以使用的。
默认情况下,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 读取数据的代码移到事务提交之前。
除此之外,通过对关联映射指定 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"/>
在映射文档中定义的抓取
策略将会对以下列表条目产生影响:
通过 get()
或 load()
方法取得数据。
只有在关联之间进行导航时,才会隐式的取得数据。
条件查询
使用了 subselect
抓取的 HQL 查询
不管你使用哪种抓取策略,定义为非延迟的类图会被保证一定装载入内存。注意这可能意味着在一条 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
实现 Cat
而 DomesticCatImpl
实现 DomesticCat
接口。例如:
<class name="CatImpl" proxy="Cat">
......
<subclass name="DomesticCatImpl" proxy="DomesticCat">
.....
</subclass>
</class
>
然后,load()
和 iterate()
永远也不会返回 Cat
和 DomesticCat
实例的代理。
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();
list()
通常不返回代理。
这里,对象之间的关系也将被延迟载入。这就意味着,你应该将属性声明为 Cat
,而不是 CatImpl
。
有些方法中是不需要代理初始化的:
equals()
方法,如果持久类没有重载 equals()
方法。
hashCode()
:如果持久类没有重载 hashCode()
方法。
标志符的 getter 方法。
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 状态经常会是一个问题。有两种方法可以解决此问题:
在一个基于 Web 的应用中,可以利用 servlet 过滤器(filter),在用户请求(request)结束、页面生成 结束时关闭 Session
(这里使用了在展示层保持打开 Session 模式(Open Session in View)),当然,这将依赖于应用框架中异常需要被正确的处理。在返回界面给用户之前,乃至在生成界面过程中发生异常的情况下,正确关闭 Session
和结束事务将是非常重要的, 请参见 Hibernate wiki 上的 "Open Session in View" 模式,你可以找到示例。
在一个拥有单独业务层的应用中,业务层必须在返回之前,为 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();
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)可能是更好的解决方法。)
假若一个延迟集合或单值代理需要抓取,Hibernate 会使用一个 subselect 重新运行原来的查询,一次性读入所有的实例。这和批量抓取的实现方法是一样的,不会有破碎的加载。
影响抓取加载相关对象的策略的另外一个方法是通过抓取配置(fetch profile),它是和 org.hibernate.SessionFactory
相关的配置但对 org.hibernate.Session
启用。一旦在 org.hibernate.Session
上启用,抓取配置将对这个 org.hibernate.Session
生效直至它被显性地禁用。
这是什么意思呢?让我们通过一个例子进行解释。假设我们有下列映射:
<hibernate-mapping>
<class name="Customer">
...
<set name="orders" inverse="true">
<key column="cust_id"/>
<one-to-many class="Order"/>
</set>
</class>
<class name="Order">
...
</class>
</hibernate-mapping
>
现在,当你获得某个特定客户的引用时,这个客户的订单将处于 lazy 状态,表示我们暂不会从数据库里加载这些订单。这样通常没有问题。假设在某种情况下,加载客户及其订单会更高效。其中一个办法当然是使用通过 HQL 或 Criteria 查询“动态抓取”的策略。另外一个方法就是使用抓取配置(Fetch Profile)。你可以在映射里加入下面的内容:
<hibernate-mapping>
...
<fetch-profile name="customer-with-orders">
<fetch entity="Customer" association="orders" style="join"/>
</fetch-profile>
</hibernate-mapping
>
甚至:
<hibernate-mapping>
<class name="Customer">
...
<fetch-profile name="customer-with-orders">
<fetch association="orders" style="join"/>
</fetch-profile>
</class>
...
</hibernate-mapping
>
下面的代码将实际上加载客户以及订单:
Session session = ...;
session.enableFetchProfile( "customer-with-orders" ); // name matches from mapping
Customer customer = (Customer) session.get( Customer.class, customerId );
目前只有 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 级别
)的缓存。你甚至可以为之插入一个集群的缓存。注意,缓存永远不知道其他应用程序对持久化仓库(数据库)可能进行的修改 (即使可以将缓存数据设定为定期失效)。
通过在 hibernate.cache.provider_class
属性中指定 org.hibernate.cache.CacheProvider
的某个实现的类名,你可以选择让 Hibernate 使用哪个缓存实现。Hibernate 打包一些开源缓存实现,提供对它们的内置支持(见下表)。除此之外,你也可以实现你自己的实现,将它们插入到系统中。注意,在 3.2 版本之前,默认使用 EhCache 作为缓存实现,但从 3.2 起就不再这样了。
表 20.1. 缓存策略提供商(Cache Providers)
Cache | Provider class | Type | Cluster Safe | Query Cache Supported |
---|---|---|---|---|
Hashtable (not intended for production use) | org.hibernate.cache.HashtableCacheProvider | memory | yes | |
EHCache | org.hibernate.cache.EhCacheProvider | memory,disk | yes | |
OSCache | org.hibernate.cache.OSCacheProvider | memory,disk | yes | |
SwarmCache | org.hibernate.cache.SwarmCacheProvider | clustered (ip multicast) | yes (clustered invalidation) | |
JBoss Cache 1.x | org.hibernate.cache.TreeCacheProvider | clustered (ip multicast), transactional | yes (replication) | yes (clock sync req.) |
JBoss Cache 2 | org.hibernate.cache.jbc.JBossCacheRegionFactory | clustered (ip multicast), transactional | yes (replication or invalidation) | yes (clock sync req.) |
类或者集合映射的“<cache>
元素”可以有下列形式:
ml_tag_symbols"><cache
ml_plain"> usage="transactional|read-write|nonstrict-read-write|read-only"
ml_plain"> region="RegionName"
include="all|non-lazy"
/>
| |
| |
|
另外(首选?),你可以在hibernate.cfg.xml
中指定 <class-cache>
和 <collection-cache>
元素。
这里的 usage
属性指明了缓存并发策略(cache concurrency strategy)。
如果你的应用程序只需读取一个持久化类的实例,而无需对其修改, 那么就可以对其进行只读
缓存。这是最简单,也是实用性最好的方法。甚至在集群中,它也能完美地运作。
<class name="eg.Immutable" mutable="false">
<cache usage="read-only"/>
....
</class
>
如果应用程序需要更新数据,那么使用读/写缓存
比较合适。 如果应用程序要求“序列化事务”的隔离级别(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
>
如果应用程序只偶尔需要更新数据(也就是说,两个事务同时更新同一记录的情况很不常见),也不需要十分严格的事务隔离,那么比较适合使用非严格读/写缓存
策略。如果在 JTA 环境中使用该策略,你必须为其指定 hibernate.transaction.manager_lookup_class
属性的值,在其它环境中,你必须保证在Session.close()
、或 Session.disconnect()
调用前,整个事务已经结束。
Hibernate 的事务缓存
策略提供了全事务的缓存支持,例如对 JBoss TreeCache 的支持。这样的缓存只能用于 JTA 环境中,你必须指定为其 hibernate.transaction.manager_lookup_class
属性。
没有一种缓存提供商能够支持上列的所有缓存并发策略。下表中列出了各种提供器、及其各自适用的并发策略。
没有一种缓存提供商能够支持上列的所有缓存并发策略。下表中列出了各种提供器、及其各自适用的并发策略。
表 20.2. 各种缓存提供商对缓存并发策略的支持情况(Cache Concurrency Strategy Support)
Cache | read-only | nonstrict-read-write | read-write | transactional |
---|---|---|---|---|
Hashtable (not intended for production use) | yes | yes | yes | |
EHCache | yes | yes | yes | |
OSCache | yes | yes | yes | |
SwarmCache | yes | yes | ||
JBoss Cache 1.x | yes | yes | ||
JBoss Cache 2 | yes | yes |
无论何时,当你给 save()
、update()
或 saveOrUpdate()
方法传递一个对象时,或使用 load()
、get()
、list()
、iterate()
或 scroll()
方法获得一个对象时,该对象都将被加入到 Session
的内部缓存中。
当随后 flush() 方法被调用时,对象的状态会和数据库取得同步。如果你不希望此同步操作发生,或者你正处理大量对象、需要对有效管理内存时,你可以调用 evict()
方法,从一级缓存中去掉这些对象及其集合。
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);
}
Session 还提供了一个 contains()
方法,用来判断某个实例是否处于当前 session 的缓存中。
如若要把所有的对象从 session 缓存中彻底清除,则需要调用 Session.clear()
。
对于二级缓存来说,在 SessionFactory
中定义了许多方法,清除缓存中实例、整个类、集合实例或者整个集合。
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
CacheMode
参数用于控制具体的 Session 如何与二级缓存进行交互。
CacheMode.NORMAL
:从二级缓存中读、写数据。
CacheMode.GET
:从二级缓存中读取数据,仅在数据更新时对二级缓存写数据。
CacheMode.PUT
:仅向二级缓存写数据,但不从二级缓存中读数据。
CacheMode.REFRESH
:仅向二级缓存写数据,但不从二级缓存中读数据。通过 hibernate.cache.use_minimal_puts
的设置,强制二级缓存从数据库中读取数据,刷新缓存内容。
如若需要查看二级缓存或查询缓存区域的内容,你可以使用统计(Statistics)
API。
Map cacheEntries = sessionFactory.getStatistics()
.getSecondLevelCacheStatistics(regionName)
.getEntries();
此时,你必须手工打开统计选项。可选的,你可以让 Hibernate 更人工可读的方式维护缓存内容。
hibernate.generate_statistics true hibernate.cache.use_structured_entries true
查询的结果集也可以被缓存。只有当经常使用同样的参数进行查询时,这才会有些用处。
按照应用程序的事务性处理过程,查询结果的缓存将产生一些负荷。例如,如果缓存针对 Person 的查询结果,在 Person 发生了修改时,Hibernate 将需要跟踪这些结果什么时候失效。因为大多数应用程序不会从缓存查询结果中受益,所以 Hibernate 在缺省情况下将禁用缓存。要使用查询缓存,你首先需要启用查询缓存:
hibernate.cache.use_query_cache true
这个设置创建了两个新的缓存 region:
org.hibernate.cache.StandardQueryCache
,保存缓存的查询结果
org.hibernate.cache.UpdateTimestampsCache
,保存对可查询表的最近更新的时间戳。它们用于检验查询结果。
如果你配置底层缓存实现来使用过期(expiry)或超时(timeout),把用于 UpdateTimestampsCache 的底层缓存 region 的缓存超时时间设置为比任何查询缓存的超时时间更长的值是非常重要的。事实上,我们推荐根本不把 UpdateTimestampsCache region 用于 expiry。请注意,特别是 LRU 缓存 expiry 策略总是不合适的。
如上面所提及的,绝大多数的查询并不能从查询缓存中受益,所以 Hibernate 默认是不进行查询缓存的。如若需要进行缓存,请调用 org.hibernate.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();
如果查询需要强行刷新其查询缓存区域,那么你应该调用 org.hibernate.Query.setCacheMode(CacheMode.REFRESH)
方法。 这对在其他进程中修改底层数据(例如,不通过Hibernate修改数据),或对那些需要选择性更新特定查询结果集的情况特别有用。这是对 org.hibernate.SessionFactory.evictQueries()
的更为有效的替代方案,同样可以清除查询缓存区域。
在前面的章节里我们已经讨论了集合和相关应用程序。在本节我么将探索运行时集合的更多问题。
Hibernate 定义了三种基本类型的集合:
值数据集合
一对多关联(One-to-many Associations)
多对多关联
这个分类是区分了不同的表和外键关系类型,但是它没有告诉我们关系模型的所有内容。 要完全理解他们的关系结构和性能特点,我们必须同时考虑“用于 Hibernate 更新或删除集合行数据的主键的结构”。因此得到了如下的分类:
有序集合类
集合(sets)
包(bags)
所有的有序集合类(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 在集合的各数据行中是如何进行“定位”的。)
根据我们上面的讨论,显然有序集合类型和大多数 set 都可以在增加、删除、修改元素中拥有最好的性能。
可论证的是对于多对多关联、值数据集合而言,有序集合类比集合(set)有一个好处。因为 Set
的内在结构, 如果“改变”了一个元素,Hibernate 并不会更新(UPDATE)
这一行。对于 Set
来说,只有在插入(INSERT)
和删除(DELETE)
操作时“改变”才有效。再次强调:这段讨论对“一对多关联”并不适用。
注意到数组无法延迟载入,我们可以得出结论,list,map 和 idbags 是最高效的(非反向)集合类型,set 则紧随其后。 在 Hibernate 中,set 应该时最通用的集合类型,这时因为“set”的语义在关系模型中是最自然的。
但是,在设计良好的 Hibernate 领域模型中,我们通常可以看到更多的集合事实上是带有 inverse="true"
的一对多的关联。对于这些关联,更新操作将会在多对一的这一端进行处理。因此对于此类情况,无需考虑其集合的更新性能。
在把 bag 扔进水沟之前,你必须了解,在一种情况下,bag 的性能(包括list)要比 set 高得多:对于指明了 inverse="true"
的集合类(比如说,标准的双向的一对多关联),我们可以在未初始化(fetch)包元素的情况下直接向 bag 或 list 添加新元素! 这是因为 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();
偶尔的,逐个删除集合类中的元素是相当低效的。Hibernate 并没那么笨,如果你想要把整个集合都删除(比如说调用 list.clear()),Hibernate 只需要一个 DELETE 就搞定了。
假设我们在一个长度为20的集合类中新增加了一个元素,然后再删除两个。Hibernate 会安排一条 INSERT
语句和两条 DELETE
语句(除非集合类是一个 bag)。这当然是令人满意的。
但是,假设我们删除了 18 个数据,只剩下 2 个,然后新增 3 个。则有两种处理方式:
逐一的删除这 18 个数据,再新增三个;
删除整个集合类(只用一句 DELETE 语句),然后逐一添加 5 个数据。
Hibernate 还没那么聪明,知道第二种选择可能会比较快。(也许让 Hibernate 不这么聪明也是好事,否则可能会引发意外的“数据库触发器”之类的问题。)
幸运的是,你可以强制使用第二种策略。你需要取消原来的整个集合类(解除其引用),然后再返回一个新的实例化的集合类,只包含需要的元素。有些时候这是非常有用的。
显然,一次性删除并不适用于被映射为 inverse="true"
的集合。
没有监测和性能参数而进行优化是毫无意义的。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
的监测功能:
在配置期间,将 hibernate.generate_statistics
设置为 true
或 false
;
在运行期间,则可以可以通过 sf.getStatistics().setStatisticsEnabled(true)
或 hibernateStatsBean.setStatisticsEnabled(true)
你也可以在程序中调用 clear()
方法重置统计数据,调用 logSummary()
在日志中记录(info 级别)其总结。
Hibernate 提供了一系列数据记录,其记录的内容包括从最基本的信息到与具体场景的特殊信息。所有的测量值都可以由 Statistics
接口 API 进行访问,主要分为三类:
使用 Session
的普通数据记录,例如打开的 Session 的个数、取得的 JDBC 的连接数等;
实体、集合、查询、缓存等内容的统一数据记录。
和具体实体、集合、查询、缓存相关的详细数据记录
例如:你可以检查缓存的命中成功次数,缓存的命中失败次数,实体、集合和查询的使用概率,查询的平均时间等。请注意 Java 中时间的近似精度是毫秒。Hibernate 的数据精度和具体的 JVM 有关,在有些平台上其精度甚至只能精确到 10 秒。
你可以直接使用 getter 方法得到全局数据记录(例如,和具体的实体、集合、缓存区无关的数据),你也可以在具体查询中通过标记实体名、 或 HQL、SQL 语句得到某实体的数据记录。请参考 Statistics
、EntityStatistics
、CollectionStatistics
、SecondLevelCacheStatistics
和 QueryStatistics
的 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()
。
版权 © 2004 Red Hat, Inc.