文章出处
在一个拥有单独业务层的应用中业务层必须在返回之前为web层准备好其所需的数据集合这就意味着 业务层应该载入所有表现层/web层所需的数据并将这些已实例化完毕的数据返回通常应用程序应该 为web层所需的每个集合调用Hibernateinitialize() (这个调用必须发生咱session关闭之前) 或者使用带有FETCH 从句或FetchModeJOIN 的Hibernate查询 事先取得所有的数据集合如果你在应用中使用了Command模式代替Session Facade 那么这项任务将会变得简单的多
你也可以通过merge() 或lock() 方法在访问未实例化的集合(或代理)之前 为先前载入的对象绑定一个新的Session 显然Hibernate将不会也不应该自动完成这些任务因为这将引入一个特殊的事务语义
有时候你并不需要完全实例化整个大的集合仅需要了解它的部分信息(例如其大小)或者集合的部分内容
你可以使用集合过滤器得到其集合的大小而不必实例化整个集合
( (Integer) screateFilter( collection select count(*) )list()get() )intValue()
这里的createFilter() 方法也可以被用来有效的抓取集合的部分内容而无需实例化整个集合
screateFilter( lazyCollection )setFirstResult()setMaxResults()list();
使用批量抓取(Using batch fetching)
Hibernate可以充分有效的使用批量抓取也就是说如果仅一个访问代理(或集合)那么Hibernate将不载入其他未实例化的代理 批量抓取是延迟查询抓取的优化方案你可以在两种批量抓取方案之间进行选择在类级别和集合级别
类/实体级别的批量抓取很容易理解假设你在运行时将需要面对下面的问题你在一个Session 中载入了个 Cat 实例每个Cat 实例都拥有一个引用成员owner 其指向Person 而Person 类是代理同时lazy=true 如果你必须遍历整个cats集合对每个元素调用getOwner() 方法Hibernate将会默认的执行次SELECT 查询 得到其owner的代理对象这时你可以通过在映射文件的Person 属性显式声明batchsize 改变其行为
<class name=Person batchsize=></class>
随之Hibernate将只需要执行三次查询分别为
你也可以在集合级别定义批量抓取例如如果每个Person 都拥有一个延迟载入的Cats 集合 现在Sesssion 中载入了个person对象遍历person集合将会引起次SELECT 查询 每次查询都会调用getCats() 方法如果你在Person 的映射定义部分允许对cats 批量抓取 那么Hibernate将可以预先抓取整个集合请看例子
<class name=Person> <set name=cats batchsize=> </set></class>
如果整个的batchsize 是(笔误?)那么Hibernate将会分四次执行SELECT 查询 按照的大小分别载入数据这里的每次载入的数据量还具体依赖于当前Session 中未实例化集合的个数
如果你的模型中有嵌套的树状结构例如典型的帐单-原料结构(billofmaterials pattern)集合的批量抓取是非常有用的(尽管在更多情况下对树进行读取时嵌套集合(nested set)或原料路径(materialized path)(××)是更好的解决方法)
使用子查询抓取(Using subselect fetching)
假若一个延迟集合或单值代理需要抓取Hibernate会使用一个subselect重新运行原来的查询一次性读入所有的实例这和批量抓取的实现方法是一样的不会有破碎的加载
使用延迟属性抓取(Using lazy property fetching)
Hibernate 对单独的属性支持延迟抓取这项优化技术也被称为组抓取(fetch groups)请注意该技术更多的属于市场特性在实际应用中优化行读取比优化列读取更重要但是仅载入类的部分属性在某些特定情况下会有用例如在原有表中拥有几百列数据数据模型无法改动的情况下
可以在映射文件中对特定的属性设置lazy 定义该属性为延迟载入
<class name=Document> <id name=id> <generator class=native/> </id> <property name=name notnull=true length=/> <property name=summary notnull=true length= lazy=true/> <property name=text notnull=true length= lazy=true/></class>
属性的延迟载入要求在其代码构建时加入二进制指示指令(bytecode instrumentation)如果你的持久类代码中未含有这些指令 Hibernate将会忽略这些属性的延迟设置仍然将其直接载入
你可以在Ant的Task中进行如下定义对持久类代码加入二进制指令
<target name=instrument depends=compile> <taskdef name=instrument classname=orghibernatetoolinstrumentInstrumentTask> <classpath path=${jarpath}/> <classpath path=${classesdir}/> <classpath refid=libclasspath/> </taskdef> <instrument verbose=true> <fileset dir=${testclassesdir}/org/hibernate/auction/model> <include name=*class/> </fileset> </instrument></target>
还有一种可以优化的方法它使用HQL或条件查询的投影(projection)特性可以避免读取非必要的列 这一点至少对只读事务是非常有用的它无需在代码构建时二进制指令处理因此是一个更加值得选择的解决方法
有时你需要在HQL中通过抓取所有属性 强行抓取所有内容
二级缓存(The Second Level Cache)
Hibernate的Session 在事务级别进行持久化数据的缓存操作 当然也有可能分别为每个类(或集合)配置集群或JVM级别(SessionFactory级别 )的缓存 你甚至可以为之插入一个集群的缓存注意缓存永远不知道其他应用程序对持久化仓库(数据库)可能进行的修改 (即使可以将缓存数据设定为定期失效)
默认情况下Hibernate使用EHCache进行JVM级别的缓存(目前Hibernate已经废弃了对JCS的支持未来版本中将会去掉它) 你可以通过设置Hibernatecacheprovider_class 属性指定其他的缓存策略 该缓存策略必须实现orghibernatecacheCacheProvider 接口
表 缓存策略提供商(Cache Providers)
Cache | Provider class | Type | Cluster Safe | Query Cache Supported | Hashtable (not intended for production use)org
hibernate
cache
HashtableCacheProvider memoryyesEHCacheorg
hibernate
cache
EhCacheProvider memory
diskyesOSCacheorg
hibernate
cache
OSCacheProvider memory
diskyesSwarmCacheorg
hibernate
cache
SwarmCacheProvider clustered (ip multicast)yes (clustered invalidation)JBoss TreeCacheorg
hibernate
cache
TreeCacheProvider clustered (ip multicast)
transactionalyes (replication)yes (clock sync req
)
缓存映射(Cache mappings)
类或者集合映射的<cache> 元素可以有下列形式
<cache usage=transactional|readwrite|nonstrictreadwrite|readonly ()/>
(
)
usage 说明了缓存的策略: transactional readwrite nonstrictreadwrite 或 readonly
另外(首选?) 你可以在hibernatecfgxml中指定<classcache> 和 <collectioncache> 元素
这里的usage 属性指明了缓存并发策略(cache concurrency strategy)
策略只读缓存(Strategy: read only)
如果你的应用程序只需读取一个持久化类的实例而无需对其修改 那么就可以对其进行只读 缓存这是最简单也是实用性最好的方法甚至在集群中它也能完美地运作
<class name=egImmutable mutable=false> <cache usage=readonly/> </class>
策略:读/写缓存(Strategy: read/write)
如果应用程序需要更新数据那么使用读/写缓存 比较合适 如果应用程序要求序列化事务的隔离级别(serializable transaction isolation level)那么就决不能使用这种缓存策略 如果在JTA环境中使用缓存你必须指定Hibernatetransactionmanager_lookup_class 属性的值 通过它Hibernate才能知道该应用程序中JTA的TransactionManager 的具体策略 在其它环境中你必须保证在Sessionclose() 或Sessiondisconnect() 调用前 整个事务已经结束 如果你想在集群环境中使用此策略你必须保证底层的缓存实现支持锁定(locking)Hibernate内置的缓存策略并不支持锁定功能
<class name=egCat > <cache usage=readwrite/> <set name=kittens > <cache usage=readwrite/> </set></class>
策略:非严格读/写缓存(Strategy: nonstrict read/write)
如果应用程序只偶尔需要更新数据(也就是说两个事务同时更新同一记录的情况很不常见)也不需要十分严格的事务隔离 那么比较适合使用非严格读/写缓存 策略如果在JTA环境中使用该策略 你必须为其指定Hibernatetransactionmanager_lookup_class 属性的值 在其它环境中你必须保证在Sessionclose() 或Sessiondisconnect() 调用前 整个事务已经结束
策略:事务缓存(transactional)
Hibernate的事务缓存 策略提供了全事务的缓存支持 例如对JBoss TreeCache的支持这样的缓存只能用于JTA环境中你必须指定 为其Hibernatetransactionmanager_lookup_class 属性
没有一种缓存提供商能够支持上列的所有缓存并发策略下表中列出了各种提供器及其各自适用的并发策略
表 各种缓存提供商对缓存并发策略的支持情况(Cache Concurrency Strategy Support)
Cache | readonly | nonstrictreadwrite | readwrite | transactional | Hashtable (not intended for production use)yesyesyesEHCacheyesyesyesOSCacheyesyesyesSwarmCacheyesyesJBoss TreeCacheyesyes
管理缓存(Managing the caches)
无论何时当你给save() update() 或 saveOrUpdate() 方法传递一个对象时或使用load() get() list() iterate() 或scroll() 方法获得一个对象时 该对象都将被加入到Session 的内部缓存中
当随后flush()方法被调用时对象的状态会和数据库取得同步 如果你不希望此同步操作发生或者你正处理大量对象需要对有效管理内存时你可以调用evict() 方法从一级缓存中去掉这些对象及其集合
ScrollableResult cats = sesscreateQuery(from Cat as cat)scroll(); //a huge result setwhile ( catsnext() ) { Cat cat = (Cat) catsget(); doSomethingWithACat(cat); sessevict(cat);}
Session还提供了一个contains() 方法用来判断某个实例是否处于当前session的缓存中
如若要把所有的对象从session缓存中彻底清除则需要调用Sessionclear()
对于二级缓存来说在SessionFactory 中定义了许多方法 清除缓存中实例整个类集合实例或者整个集合
sessionFactoryevict(Catclass catId); //evict a particular CatsessionFactoryevict(Catclass); //evict all CatssessionFactoryevictCollection(Catkittens catId); //evict a particular collection of kittenssessionFactoryevictCollection(Catkittens); //evict all kitten collections
CacheMode 参数用于控制具体的Session如何与二级缓存进行交互
CacheModeNORMAL 从二级缓存中读写数据
CacheModeGET 从二级缓存中读取数据仅在数据更新时对二级缓存写数据
CacheModePUT 仅向二级缓存写数据但不从二级缓存中读数据
CacheModeREFRESH 仅向二级缓存写数据但不从二级缓存中读数据通过 Hibernatecacheuse_minimal_puts 的设置强制二级缓存从数据库中读取数据刷新缓存内容
如若需要查看二级缓存或查询缓存区域的内容你可以使用统计(Statistics) API
Map cacheEntries = sessionFactorygetStatistics() getSecondLevelCacheStatistics(regionName) getEntries();
此时你必须手工打开统计选项可选的你可以让Hibernate更人工可读的方式维护缓存内容
Hibernategenerate_statistics truehibernatecacheuse_structured_entries true
查询缓存(The Query Cache)
查询的结果集也可以被缓存只有当经常使用同样的参数进行查询时这才会有些用处 要使用查询缓存首先你必须打开它
Hibernatecacheuse_query_cache true
该设置将会创建两个缓存区域 一个用于保存查询结果集(orghibernatecacheStandardQueryCache ) 另一个则用于保存最近查询的一系列表的时间戳(orghibernatecacheUpdateTimestampsCache ) 请注意在查询缓存中它并不缓存结果集中所包含的实体的确切状态它只缓存这些实体的标识符属性的值以及各值类型的结果 所以查询缓存通常会和二级缓存一起使用
绝大多数的查询并不能从查询缓存中受益所以Hibernate默认是不进行查询缓存的如若需要进行缓存请调用 QuerysetCacheable(true) 方法这个调用会让查询在执行过程中时先从缓存中查找结果 并将自己的结果集放到缓存中去
如果你要对查询缓存的失效政策进行精确的控制你必须调用QuerysetCacheRegion() 方法 为每个查询指定其命名的缓存区域
List blogs = sesscreateQuery(from Blog blog where blogblogger = :blogger) setEntity(blogger blogger) setMaxResults() setCacheable(true) setCacheRegion(frontpages) list();
如果查询需要强行刷新其查询缓存区域那么你应该调用QuerysetCacheMode(CacheModeREFRESH) 方法 这对在其他进程中修改底层数据(例如不通过Hibernate修改数据)或对那些需要选择性更新特定查询结果集的情况特别有用 这是对SessionFactoryevictQueries() 的更为有效的替代方案同样可以清除查询缓存区域
理解集合性能(Understanding Collection performance)
前面我们已经对集合进行了足够的讨论本段中我们将着重讲述集合在运行时的事宜
分类(Taxonomy)
Hibernate定义了三种基本类型的集合
值数据集合
一对多关联
多对多关联
这个分类是区分了不同的表和外键关系类型但是它没有告诉我们关系模型的所有内容 要完全理解他们的关系结构和性能特点我们必须同时考虑用于Hibernate更新或删除集合行数据的主键的结构 因此得到了如下的分类
有序集合类
集合(sets)
包(bags)
所有的有序集合类(maps lists arrays)都拥有一个由<key> 和 <index> 组成的主键 这种情况下集合类的更新是非常高效的——主键已经被有效的索引因此当Hibernate试图更新或删除一行时可以迅速找到该行数据
集合(sets)的主键由<key> 和其他元素字段构成 对于有些元素类型来说这很低效特别是组合元素或者大文本大二进制字段 数据库可能无法有效的对复杂的主键进行索引 另一方面对于一对多多对多关联特别是合成的标识符来说集合也可以达到同样的高效性能( 附注如果你希望SchemaExport 为你的<set> 创建主键 你必须把所有的字段都声明为notnull=true )
<idbag> 映射定义了代理键因此它总是可以很高效的被更新事实上 <idbag> 拥有着最好的性能表现
Bag是最差的因为bag允许重复的元素值也没有索引字段因此不可能定义主键 Hibernate无法判断出重复的行当这种集合被更改时Hibernate将会先完整地移除 (通过一个(in a single DELETE ))整个集合然后再重新创建整个集合 因此Bag是非常低效的
请注意对于一对多关联来说主键很可能并不是数据库表的物理主键 但就算在此情况下上面的分类仍然是有用的(它仍然反映了Hibernate在集合的各数据行中是如何进行定位的)
Lists maps 和sets用于更新效率最高
根据我们上面的讨论显然有序集合类型和大多数set都可以在增加删除修改元素中拥有最好的性能
可论证的是对于多对多关联值数据集合而言有序集合类比集合(set)有一个好处因为Set 的内在结构 如果改变了一个元素Hibernate并不会更新(UPDATE) 这一行 对于Set 来说只有在插入(INSERT) 和删除(DELETE) 操作时改变才有效再次强调这段讨论对一对多关联并不适用
注意到数组无法延迟载入我们可以得出结论list map和idbags是最高效的(非反向)集合类型set则紧随其后 在Hibernate中set应该时最通用的集合类型这时因为set的语义在关系模型中是最自然的
但是在设计良好的Hibernate领域模型中我们通常可以看到更多的集合事实上是带有inverse=true 的一对多的关联对于这些关联更新操作将会在多对一的这一端进行处理因此对于此类情况无需考虑其集合的更新性能
Bag和list是反向集合类中效率最高的
在把bag扔进水沟之前你必须了解在一种情况下bag的性能(包括list)要比set高得多 对于指明了inverse=true 的集合类(比如说标准的双向的一对多关联) 我们可以在未初始化(fetch)包元素的情况下直接向bag或list添加新元素! 这是因为Collectionadd() )或者CollectionaddAll() 方法 对bag或者List总是返回true(这点与与Set不同)因此对于下面的相同代码来说速度会快得多
Parent p = (Parent) sessload(Parentclass id); Child c = new Child(); csetParent(p); pgetChildren()add(c); //no need to fetch the collection! sessflush();
一次性删除(One shot delete)
偶尔的逐个删除集合类中的元素是相当低效的Hibernate并没那么笨 如果你想要把整个集合都删除(比如说调用listclear())Hibernate只需要一个DELETE就搞定了
假设我们在一个长度为的集合类中新增加了一个元素然后再删除两个 Hibernate会安排一条INSERT 语句和两条DELETE 语句(除非集合类是一个bag) 这当然是显而易见的 但是假设我们删除了个数据只剩下个然后新增个则有两种处理方式
逐一的删除这个数据再新增三个
删除整个集合类(只用一句DELETE语句)然后增加个数据
Hibernate还没那么聪明知道第二种选择可能会比较快 (也许让Hibernate不这么聪明也是好事否则可能会引发意外的数据库触发器之类的问题)
幸运的是你可以强制使用第二种策略你需要取消原来的整个集合类(解除其引用) 然后再返回一个新的实例化的集合类只包含需要的元素有些时候这是非常有用的
显然一次性删除并不适用于被映射为inverse=true 的集合
监测性能(Monitoring performance)
没有监测和性能参数而进行优化是毫无意义的Hibernate为其内部操作提供了一系列的示意图因此可以从 每个SessionFactory 抓取其统计数据
监测SessionFactory
你可以有两种方式访问SessionFactory 的数据记录第一种就是自己直接调用 sessionFactorygetStatistics() 方法读取显示统计 数据
此外如果你打开StatisticsService MBean选项那么Hibernate则可以使用JMX技术 发布其数据记录你可以让应用中所有的SessionFactory 同时共享一个MBean也可以每个 SessionFactory分配一个MBean下面的代码即是其演示代码
// MBean service registration for a specific SessionFactoryHashtable tb = new Hashtable();tbput(type statistics);tbput(sessionFactory myFinancialApp);ObjectName on = new ObjectName(hibernate tb); // MBean object nameStatisticsService stats = new StatisticsService(); // MBean implementationstatssetSessionFactory(sessionFactory); // Bind the stats to a SessionFactoryserverregisterMBean(stats on); // Register the Mbean on the server
// MBean service registration for all SessionFactorysHashtable tb = new Hashtable();tbput(type statistics);tbput(sessionFactory all);ObjectName on = new ObjectName(hibernate tb); // MBean object nameStatisticsService stats = new StatisticsService(); // MBean implementationserverregisterMBean(stats on); // Register the MBean on the server
TODO仍需要说明的是在第一个例子中我们直接得到和使用MBean而在第二个例子中在使用MBean之前 我们则需要给出SessionFactory的JNDI名使用hibernateStatsBeansetSessionFactoryJNDIName(my/JNDI/Name) 得到SessionFactory然后将MBean保存于其中
你可以通过以下方法打开或关闭SessionFactory 的监测功能
在配置期间将Hibernategenerate_statistics 设置为true 或false
在运行期间则可以可以通过sfgetStatistics()setStatisticsEnabled(true) 或hibernateStatsBeansetStatisticsEnabled(true)
你也可以在程序中调用clear() 方法重置统计数据调用logSummary() 在日志中记录(info级别)其总结
数据记录(Metrics)
Hibernate提供了一系列数据记录其记录的内容包括从最基本的信息到与具体场景的特殊信息所有的测量值都可以由 Statistics 接口进行访问主要分为三类
使用Session 的普通数据记录例如打开的Session的个数取得的JDBC的连接数等
实体集合查询缓存等内容的统一数据记录
和具体实体集合查询缓存相关的详细数据记录
例如你可以检查缓存的命中成功次数缓存的命中失败次数实体集合和查询的使用概率查询的平均时间等请注意 Java中时间的近似精度是毫秒Hibernate的数据精度和具体的JVM有关在有些平台上其精度甚至只能精确到秒
你可以直接使用getter方法得到全局数据记录(例如和具体的实体集合缓存区无关的数据)你也可以在具体查询中通过标记实体名 或HQLSQL语句得到某实体的数据记录请参考Statistics EntityStatistics CollectionStatistics SecondLevelCacheStatistics 和QueryStatistics 的API文档以抓取更多信息下面的代码则是个简单的例子
Statistics stats = HibernateUtilsessionFactorygetStatistics();double queryCacheHitCount = statsgetQueryCacheHitCount();double queryCacheMissCount = statsgetQueryCacheMissCount();double queryCacheHitRatio = queryCacheHitCount / (queryCacheHitCount + queryCacheMissCount);(Query Hit ratio: + queryCacheHitRatio);EntityStatistics entityStats = statsgetEntityStatistics( CatclassgetName() );long changes = entityStatsgetInsertCount() + entityStatsgetUpdateCount() + entityStatsgetDeleteCount();(CatclassgetName() + changed + changes + times );
如果你想得到所有实体集合查询和缓存区的数据你可以通过以下方法获得实体集合查询和缓存区列表 getQueries() getEntityNames() getCollectionRoleNames() 和 getSecondLevelCacheRegionNames()