Hibernate提高性能

导读:
  20.1. 抓取策略(Fetching strategies)
  抓取策略(fetching strategy)是指:当应用程序须要在(Hibernate实体对象图的)关联关系间进行导航的时候, Hibernate如何获取关联对象的策略。抓取策略能够在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,延迟集合抓取- 直到应用程序对集合进行了一次操做时,集合才被抓取。(对集合而言这是默认行为。)
  Proxy fetching,代理抓取- 对返回单值的关联而言,当其某个方法被调用,而非对其关键字进行get操做时才抓取。
  Lazy attribute fetching,属性延迟加载- 对属性或返回单值的关联而言,当其实例变量被访问的时候进行抓取(须要运行时字节码强化)。这一方法不多是必要的。
  这里有两个正交的概念:关联什么时候被抓取,以及被如何抓取(会采用什么样的SQL语句)。不要混淆它们!咱们使用抓取来改善性能。咱们使用延迟来定义一些契约,对某特定类的某个脱管的实例,知道有哪些数据是可使用的。
  20.1.1. 操做延迟加载的关联
  默认状况下,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读取数据的代码 移到tx.commit()以前。
  除此以外,经过对关联映射指定lazy="false",咱们也可使用非延迟的集合或关联。可是, 对绝大部分集合来讲,更推荐使用延迟方式抓取数据。若是在你的对象模型中定义了太多的非延迟关联,Hibernate最终几乎须要在每一个事务中载入整个数据库到内存中!
  可是,另外一方面,在一些特殊的事务中,咱们也常常须要使用到链接抓取(它自己上就是非延迟的),以代替查询抓取。 下面咱们将会很快明白如何具体的定制Hibernate中的抓取策略。在Hibernate3中,具体选择哪一种抓取策略的机制是和选择 单值关联或集合关联相一致的。
  20.1.2. 调整抓取策略(Tuning fetch strategies)
  查询抓取(默认的)在N+1查询的状况下是极其脆弱的,所以咱们可能会要求在映射文档中定义使用链接抓取:
  fetch="join">  fetch="join">
  
  
   fetch="join">  在映射文档中定义的抓取策略将会有产生如下影响:
  经过get()或load()方法取得数据。
  只有在关联之间进行导航时,才会隐式的取得数据(延迟抓取)。
  条件查询
  一般状况下,咱们并不使用映射文档进行抓取策略的定制。更多的是,保持其默认值,而后在特定的事务中, 使用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次查询的方法是,使用二级缓存。
  20.1.3. 单端关联代理(Single-ended association proxies)
  在Hinerbate中,对集合的延迟抓取的采用了本身的实现方法。可是,对于单端关联的延迟抓取,则须要采用 其余不一样的机制。单端关联的目标实体必须使用代理,Hihernate在运行期二进制级(经过优异的CGLIB库), 为持久对象实现了延迟载入代理。
  默认的,Hibernate3将会为全部的持久对象产生代理(在启动阶段),而后使用他们实现 多对一(many-to-one)关联和一对一(one-to-one)关联的延迟抓取。
  在映射文件中,能够经过设置proxy属性为目标class声明一个接口供代理接口使用。 默认的,Hibernate将会使用该类的一个子类。 注意:被代理的类必须实现一个至少包可见的默认构造函数,咱们建议全部的持久类都应拥有这样的构造函数
  在如此方式定义一个多态类的时候,有许多值得注意的常见性的问题,例如:
  
  ......
  
  .....
  
  
  首先,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接口。 在load()、iterate()方法中就会返回 Cat和DomesticCat的代理对象。 (注意list()并不会返回代理对象。)
  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();
  这里,对象之间的关系也将被延迟载入。这就意味着,你应该将属性声明为Cat,而不是CatImpl。
  可是,在有些方法中是不须要使用代理的。例如:
  equals()方法,若是持久类没有重载equals()方法。
  hashCode()方法,若是持久类没有重载hashCode()方法。
  标志符的getter方法。
  Hibernate将会识别出那些重载了equals()、或hashCode()方法的持久化类。
  20.1.4. 实例化集合和代理(Initializing collections and proxies)
  在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和结束事务将是很是重要的, Servlet过滤器必须如此访问Session,才能保证正确使用Session。 咱们推荐使用ThreadLocal变量保存当前的Session(能够参考第 1.4 节 “与Cat同乐”的例子实现)。
  在一个拥有单独业务层的应用中,业务层必须在返回以前,为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();
  20.1.5. 使用批量抓取(Using batch fetching)
  Hibernate能够充分有效的使用批量抓取,也就是说,若是仅一个访问代理(或集合),那么Hibernate将不载入其余未实例化的代理。 批量抓取是延迟查询抓取的优化方案,你能够在两种批量抓取方案之间进行选择:在类级别和集合级别。
  类/实体级别的批量抓取很容易理解。假设你在运行时将须要面对下面的问题:你在一个Session中载入了25个 Cat实例,每一个Cat实例都拥有一个引用成员owner, 其指向Person,而Person类是代理,同时lazy="true"。 若是你必须遍历整个cats集合,对每一个元素调用getOwner()方法,Hibernate将会默认的执行25次SELECT查询, 获得其owner的代理对象。这时,你能够经过在映射文件的Person属性,显式声明batch-size,改变其行为:
  ...
  随之,Hibernate将只须要执行三次查询,分别为十、十、 5。
  你也能够在集合级别定义批量抓取。例如,若是每一个Person都拥有一个延迟载入的Cats集合, 如今,Sesssion中载入了10个person对象,遍历person集合将会引发10次SELECT查询, 每次查询都会调用getCats()方法。若是你在Person的映射定义部分,容许对cats批量抓取, 那么,Hibernate将能够预先抓取整个集合。请看例子:
  
  
  ...
  
  
  若是整个的batch-size是3(笔误?),那么Hibernate将会分四次执行SELECT查询, 按照三、三、三、1的大小分别载入数据。这里的每次载入的数据量还具体依赖于当前Session中未实例化集合的个数。
  若是你的模型中有嵌套的树状结构,例如典型的账单-原料结构(bill-of-materials pattern),集合的批量抓取是很是有用的。 (尽管在更多状况下对树进行读取时,嵌套集合(nested set)或原料路径(materialized path)() 是更好的解决方法。)
  20.1.6. 使用子查询抓取(Using subselect fetching)
  倘若一个延迟集合或单值代理须要抓取,Hibernate会使用一个subselect从新运行原来的查询,一次性读入全部的实例。这和批量抓取的实现方法是同样的,不会有破碎的加载。
  20.1.7. 使用延迟属性抓取(Using lazy property fetching)
  Hibernate3对单独的属性支持延迟抓取,这项优化技术也被称为组抓取(fetch groups)。 请注意,该技术更多的属于市场特性。在实际应用中,优化行读取比优化列读取更重要。可是,仅载入类的部分属性在某些特定状况下会有用,例如在原有表中拥有几百列数据、数据模型没法改动的状况下。
  能够在映射文件中对特定的属性设置lazy,定义该属性为延迟载入。
  
  
  
  
  
  
  
  
  属性的延迟载入要求在其代码构建时加入二进制指示指令(bytecode instrumentation),若是你的持久类代码中未含有这些指令, Hibernate将会忽略这些属性的延迟设置,仍然将其直接载入。
  你能够在Ant的Task中,进行以下定义,对持久类代码加入“二进制指令。”
  
  
  
  
  
  
  
  
  
  
  
  
  还有一种能够优化的方法,它使用HQL或条件查询的投影(projection)特性,能够避免读取非必要的列, 这一点至少对只读事务是很是有用的。它无需在代码构建时“二进制指令”处理,所以是一个更加值得选择的解决方法。
  有时你须要在HQL中经过抓取全部属性,强行抓取全部内容。
  20.2. 二级缓存(The Second Level Cache)
  Hibernate的Session在事务级别进行持久化数据的缓存操做。 固然,也有可能分别为每一个类(或集合),配置集群、或JVM级别(SessionFactory级别)的缓存。 你甚至能够为之插入一个集群的缓存。注意,缓存永远不知道其余应用程序对持久化仓库(数据库)可能进行的修改 (即便能够将缓存数据设定为按期失效)。
  默认状况下,Hibernate使用EHCache进行JVM级别的缓存(目前,Hibernate已经废弃了对JCS的支持,将来版本中将会去掉它)。 你能够经过设置hibernate.cache.provider_class属性,指定其余的缓存策略, 该缓存策略必须实现org.hibernate.cache.CacheProvider接口。
  表 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 TreeCache org.hibernate.cache.TreeCacheProvider clustered (ip multicast), transactional yes (replication) yes (clock sync req.)
  20.2.1. 缓存映射(Cache mappings)
  类或者集合映射的“元素”能够有下列形式:
  usage="transactional|read-write|nonstrict-read-write|read-only" (1)
/>  usage="transactional|read-write|nonstrict-read-write|read-only" (1)
  />
  (1) usage说明了缓存的策略: transactional、 read-write、 nonstrict-read-write或 read-only。
  另外(首选?), 你能够在hibernate.cfg.xml中指定和 元素。
  这里的usage属性指明了缓存并发策略(cache concurrency strategy)。
  20.2.2. 策略:只读缓存(Strategy: read only)
  若是你的应用程序只需读取一个持久化类的实例,而无需对其修改, 那么就能够对其进行只读缓存。这是最简单,也是实用性最好的方法。甚至在集群中,它也能完美地运做。
  
  
  ....
  
  20.2.3. 策略:读/写缓存(Strategy: read/write)
  若是应用程序须要更新数据,那么使用读/写缓存比较合适。 若是应用程序要求“序列化事务”的隔离级别(serializable transaction isolation level),那么就决不能使用这种缓存策略。 若是在JTA环境中使用缓存,你必须指定hibernate.transaction.manager_lookup_class属性的值, 经过它,Hibernate才能知道该应用程序中JTA的TransactionManager的具体策略。 在其它环境中,你必须保证在Session.close()、或Session.disconnect()调用前, 整个事务已经结束。 若是你想在集群环境中使用此策略,你必须保证底层的缓存实现支持锁定(locking)。Hibernate内置的缓存策略并不支持锁定功能。
  
  
  ....
  
  
  ....
  
  
  20.2.4. 策略:非严格读/写缓存(Strategy: nonstrict read/write)
  若是应用程序只偶尔须要更新数据(也就是说,两个事务同时更新同一记录的状况很不常见),也不须要十分严格的事务隔离, 那么比较适合使用非严格读/写缓存策略。若是在JTA环境中使用该策略, 你必须为其指定hibernate.transaction.manager_lookup_class属性的值, 在其它环境中,你必须保证在Session.close()、或Session.disconnect()调用前, 整个事务已经结束。
  20.2.5. 策略:事务缓存(transactional)
  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 TreeCache yes yes
  20.3. 管理缓存(Managing the caches)
  不管什么时候,当你给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
  20.4. 查询缓存(The Query Cache)
  查询的结果集也能够被缓存。只有当常用一样的参数进行查询时,这才会有些用处。 要使用查询缓存,首先你必须打开它:
  hibernate.cache.use_query_cache true
  该设置将会建立两个缓存区域 - 一个用于保存查询结果集(org.hibernate.cache.StandardQueryCache); 另外一个则用于保存最近查询的一系列表的时间戳(org.hibernate.cache.UpdateTimestampsCache)。 请注意:在查询缓存中,它并不缓存结果集中所包含的实体的确切状态;它只缓存这些实体的标识符属性的值、以及各值类型的结果。 因此查询缓存一般会和二级缓存一块儿使用。
  绝大多数的查询并不能从查询缓存中受益,因此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();
  若是查询须要强行刷新其查询缓存区域,那么你应该调用Query.setCacheMode(CacheMode.REFRESH)方法。 这对在其余进程中修改底层数据(例如,不经过Hibernate修改数据),或对那些须要选择性更新特定查询结果集的状况特别有用。 这是对SessionFactory.evictQueries()的更为有效的替代方案,一样能够清除查询缓存区域。
  20.5. 理解集合性能(Understanding Collection performance)
  前面咱们已经对集合进行了足够的讨论。本段中,咱们将着重讲述集合在运行时的事宜。
  20.5.1. 分类(Taxonomy)
  Hibernate定义了三种基本类型的集合:
  值数据集合
  一对多关联
  多对多关联
  这个分类是区分了不一样的表和外键关系类型,可是它没有告诉咱们关系模型的全部内容。 要彻底理解他们的关系结构和性能特色,咱们必须同时考虑“用于Hibernate更新或删除集合行数据的主键的结构”。 所以获得了以下的分类:
  有序集合类
  集合(sets)
  包(bags)
  全部的有序集合类(maps, lists, arrays)都拥有一个由和 组成的主键。 这种状况下集合类的更新是很是高效的——主键已经被有效的索引,所以当Hibernate试图更新或删除一行时,能够迅速找到该行数据。
  集合(sets)的主键由和其余元素字段构成。 对于有些元素类型来讲,这很低效,特别是组合元素或者大文本、大二进制字段; 数据库可能没法有效的对复杂的主键进行索引。 另外一方面,对于一对多、多对多关联,特别是合成的标识符来讲,集合也能够达到一样的高效性能。( 附注:若是你但愿SchemaExport为你的建立主键, 你必须把全部的字段都声明为not-null="true"。)
  映射定义了代理键,所以它老是能够很高效的被更新。事实上, 拥有着最好的性能表现。
  Bag是最差的。由于bag容许重复的元素值,也没有索引字段,所以不可能定义主键。 Hibernate没法判断出重复的行。当这种集合被更改时,Hibernate将会先完整地移除 (经过一个(in a single DELETE))整个集合,而后再从新建立整个集合。 所以Bag是很是低效的。
  请注意:对于一对多关联来讲,“主键”极可能并非数据库表的物理主键。 但就算在此状况下,上面的分类仍然是有用的。(它仍然反映了Hibernate在集合的各数据行中是如何进行“定位”的。)
  20.5.2. Lists, maps 和sets用于更新效率最高
  根据咱们上面的讨论,显然有序集合类型和大多数set均可以在增长、删除、修改元素中拥有最好的性能。
  可论证的是对于多对多关联、值数据集合而言,有序集合类比集合(set)有一个好处。由于Set的内在结构, 若是“改变”了一个元素,Hibernate并不会更新(UPDATE)这一行。 对于Set来讲,只有在插入(INSERT)和删除(DELETE)操做时“改变”才有效。再次强调:这段讨论对“一对多关联”并不适用。
  注意到数组没法延迟载入,咱们能够得出结论,list, map和idbags是最高效的(非反向)集合类型,set则紧随其后。 在Hibernate中,set应该时最通用的集合类型,这时由于“set”的语义在关系模型中是最天然的。
  可是,在设计良好的Hibernate领域模型中,咱们一般能够看到更多的集合事实上是带有inverse="true"的一对多的关联。对于这些关联,更新操做将会在多对一的这一端进行处理。所以对于此类状况,无需考虑其集合的更新性能。
  20.5.3. Bag和list是反向集合类中效率最高的
  在把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();
  20.5.4. 一次性删除(One shot delete)
  偶尔的,逐个删除集合类中的元素是至关低效的。Hibernate并没那么笨, 若是你想要把整个集合都删除(好比说调用list.clear()),Hibernate只须要一个DELETE就搞定了。
  假设咱们在一个长度为20的集合类中新增长了一个元素,而后再删除两个。 Hibernate会安排一条INSERT语句和两条DELETE语句(除非集合类是一个bag)。 这固然是显而易见的。
  可是,假设咱们删除了18个数据,只剩下2个,而后新增3个。则有两种处理方式:
  逐一的删除这18个数据,再新增三个;
  删除整个集合类(只用一句DELETE语句),而后增长5个数据。
  Hibernate还没那么聪明,知道第二种选择可能会比较快。 (也许让Hibernate不这么聪明也是好事,不然可能会引起意外的“数据库触发器”之类的问题。)
  幸运的是,你能够强制使用第二种策略。你须要取消原来的整个集合类(解除其引用), 而后再返回一个新的实例化的集合类,只包含须要的元素。有些时候这是很是有用的。
  显然,一次性删除并不适用于被映射为inverse="true"的集合。
  20.6. 监测性能(Monitoring performance)
  没有监测和性能参数而进行优化是毫无心义的。Hibernate为其内部操做提供了一系列的示意图,所以能够从 每一个SessionFactory抓取其统计数据。
  20.6.1. 监测SessionFactory
  你能够有两种方式访问SessionFactory的数据记录,第一种就是本身直接调用 sessionFactory.getStatistics()方法读取、显示统计数据。
  此外,若是你打开StatisticsServiceMBean选项,那么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
  TODO:仍须要说明的是:在第一个例子中,咱们直接获得和使用MBean;而在第二个例子中,在使用MBean以前 咱们则须要给出SessionFactory的JNDI名,使用hibernateStatsBean.setSessionFactoryJNDIName("my/JNDI/Name")获得SessionFactory,而后将MBean保存于其中。
  你能够经过如下方法打开或关闭SessionFactory的监测功能:
  在配置期间,将hibernate.generate_statistics设置为true或false;
  在运行期间,则能够能够经过sf.getStatistics().setStatisticsEnabled(true)或hibernateStatsBean.setStatisticsEnabled(true)
  你也能够在程序中调用clear()方法重置统计数据,调用logSummary()在日志中记录(info级别)其总结。
  20.6.2. 数据记录(Metrics)
  Hibernate提供了一系列数据记录,其记录的内容包括从最基本的信息到与具体场景的特殊信息。全部的测量值均可以由 Statistics接口进行访问,主要分为三类:
  使用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()。
  Hibernate程序性能优化的考虑要点
  MENGCHUCHEN
  本文依照HIBERNATE帮助文档,一些网络书籍及项目经验整理而成,只提供要点和思路,具体作法能够留言探讨,或是找一些更详细更有针对性的资料。
  初用HIBERNATE的人也许都遇到过性能问题,实现同一功能,用HIBERNATE与用JDBC性能相差十几倍很正常,若是不及早调整,极可能影响整个项目的进度。
  大致上,对于HIBERNATE性能调优的主要考虑点以下:
  数据库设计调整
  HQL优化
  API的正确使用(如根据不一样的业务类型选用不一样的集合及查询API)
  主配置参数(日志,查询缓存,fetch_size, batch_size等)
  映射文件优化(ID生成策略,二级缓存,延迟加载,关联优化)
  一级缓存的管理
  针对二级缓存,还有许多特有的策略
  事务控制策略。
  一、 数据库设计
  a) 下降关联的复杂性
  b) 尽可能不使用联合主键
  c) ID的生成机制,不一样的数据库所提供的机制并不彻底同样
  d) 适当的冗余数据,不过度追求高范式
  二、 HQL优化
  HQL若是抛开它同HIBERNATE自己一些缓存机制的关联,HQL的优化技巧同普通的SQL优化技巧同样,能够很容易在网上找到一些经验之谈。
  三、 主配置
  a) 查询缓存,同下面讲的缓存不太同样,它是针对HQL语句的缓存,即彻底同样的语句再次执行时能够利用缓存数据。可是,查询缓存在一个交易系统(数据变动频繁,查询条件相同的机率并不大)中可能会起副作用:它会白白耗费大量的系统资源但却难以派上用场。
  b) fetch_size,同JDBC的相关参数做用相似,参数并非越大越好,而应根据业务特征去设置
  c) batch_size同上。
  d) 生产系统中,切记要关掉SQL语句打印。
  四、 缓存
  a) 数据库级缓存:这级缓存是最高效和安全的,但不一样的数据库可管理的层次并不同,好比,在ORACLE中,能够在建表时指定将整个表置于缓存当中。
  b) SESSION缓存:在一个HIBERNATE SESSION有效,这级缓存的可干预性不强,大多于HIBERNATE自动管理,但它提供清除缓存的方法,这在大批量增长/更新操做是有效的。好比,同时增长十万条记录,按常规方式进行,极可能会发现OutofMemeroy的异常,这时可能须要手动清除这一级缓存:Session.evict以及Session.clear
  c) 应用缓存:在一个SESSIONFACTORY中有效,所以也是优化的重中之重,所以,各种策略也考虑的较多,在将数据放入这一级缓存以前,须要考虑一些前提条件:
  i. 数据不会被第三方修改(好比,是否有另外一个应用也在修改这些数据?)
  ii. 数据不会太大
  iii. 数据不会频繁更新(不然使用CACHE可能拔苗助长)
  iv. 数据会被频繁查询
  v. 数据不是关键数据(如涉及钱,安全等方面的问题)。
  缓存有几种形式,能够在映射文件中配置:read-only(只读,适用于不多变动的静态数据/历史数据),nonstrict-read-write,read-write(比较广泛的形式,效率通常),transactional(JTA中,且支持的缓存产品较少)
  d) 分布式缓存:同c)的配置同样,只是缓存产品的选用不一样,在目前的HIBERNATE中可供选择的很少,oscache, jboss cache,目前的大多数项目,对它们的用于集群的使用(特别是关键交易系统)都持保守态度。在集群环境中,只利用数据库级的缓存是最安全的。
  五、 延迟加载
  a) 实体延迟加载:经过使用动态代理实现
  b) 集合延迟加载:经过实现自有的SET/LIST,HIBERNATE提供了这方面的支持
  c) 属性延迟加载:
  六、 方法选用
  a) 完成一样一件事,HIBERNATE提供了可供选择的一些方式,但具体使用什么方式,可能用性能/代码都会有影响。显示,一次返回十万条记录(List/Set/Bag/Map等)进行处理,极可能致使内存不够的问题,而若是用基于游标(ScrollableResults)或Iterator的结果集,则不存在这样的问题。
  b) Session的load/get方法,前者会使用二级缓存,然后者则不使用。
  c) Query和list/iterator,若是去仔细研究一下它们,你可能会发现不少有意思的状况,两者主要区别(若是使用了Spring,在HibernateTemplate中对应find,iterator方法):
  i. list只能利用查询缓存(但在交易系统中查询缓存做用不大),没法利用二级缓存中的单个实体,但list查出的对象会写入二级缓存,但它通常只生成较少的执行SQL语句,不少状况就是一条(无关联)。
  ii. iterator则能够利用二级缓存,对于一条查询语句,它会先从数据库中找出全部符合条件的记录的ID,再经过ID去缓存找,对于缓存中没有的记录,再构造语句从数据库中查出,所以很容易知道,若是缓存中没有任何符合条件的记录,使用iterator会产生N+1条SQL语句(N为符合条件的记录数)
  iii. 经过iterator,配合缓存管理API,在海量数据查询中能够很好的解决内存问题,如:
  while(it.hasNext()){
  YouObject object = (YouObject)it.next();
  session.evict(youObject);
  sessionFactory.evice(YouObject.class, youObject.getId());
  }
  若是用list方法,极可能就出OutofMemory错误了。
  iv. 经过上面的说明,我想你应该知道如何去使用这两个方法了。
  七、 集合的选用
  在HIBERNATE 3.1文档的“19.5. Understanding Collection performance”中有详细的说明。
  八、 事务控制
  事务方面对性能有影响的主要包括:事务方式的选用,事务隔离级别以及锁的选用
  a) 事务方式选用:若是不涉及多个事务管理器事务的话,不须要使用JTA,只有JDBC的事务控制就能够。
  b) 事务隔离级别:参见标准的SQL事务隔离级别
  c) 锁的选用:悲观锁(通常由具体的事务管理器实现),对于长事务效率低,但安全。乐观锁(通常在应用级别实现),如在HIBERNATE中能够定义VERSION字段,显然,若是有多个应用操做数据,且这些应用不是用同一种乐观锁机制,则乐观锁会失效。所以,针对不一样的数据应有不一样的策略,同前面许多状况同样,不少时候咱们是在效率与安全/准确性上找一个平衡点,不管如何,优化都不是一个纯技术的问题,你应该对你的应用和业务特征有足够的了解。
  九、 批量操做
  即便是使用JDBC,在进行大批数据更新时,BATCH与不使用BATCH有效率上也有很大的差异。咱们能够经过设置batch_size来让其支持批量操做。
  举个例子,要批量删除某表中的对象,如“delete Account”,打出来的语句,会发现HIBERNATE找出了全部ACCOUNT的ID,再进行删除,这主要是为了维护二级缓存,这样效率确定高不了,在后续的版本中增长了bulk delete/update,但这也没法解决缓存的维护问题。也就是说,因为有了二级缓存的维护问题,HIBERNATE的批量操做效率并不尽如人意!
  从前面许多要点能够看出,不少时候咱们是在效率与安全/准确性上找一个平衡点,不管如何,优化都不是一个纯技术的问题,你应该对你的应用和业务特征有足够的了解,通常的,优化方案应在架构设计期就基本肯定,不然可能致使不必的返工,导致项目延期,而做为架构师和项目经理,还要面对开发人员可能的抱怨,必竟,咱们对用户需求更改的控制力不大,但技术/架构风险是应该在初期意识到并制定好相关的对策。
  还有一点要注意,应用层的缓存只是锦上添花,永远不要把它当救命稻草,应用的根基(数据库设计,算法,高效的操做语句,恰当API的选择等)才是最重要的。

web

相关文章
相关标签/搜索