Hibernate 学习笔记(7)

Hibernate提升性能主要内容

Hibernate抓取策略

二级缓存(Second Level)

管理Hibernate缓存

查询缓存(Query Cache)

理解集合性能

如何监测Hibernate性能?

什么是抓取策略?

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

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

1. 连接抓取(Join fetching) - Hibernate通过 在SELECT语句使用OUTER JOIN(外连接)来 获得对象的关联实例或者关联集合。 

2. 查询抓取(Select fetching) - 另外发送一条 SELECT 语句抓取当前对象的关联实体或集合。除非你显式的指定lazy="false"禁止 延迟抓取(lazy fetching),否则只有当你真正访问关联关系的时候,才会执行第二条select语句。 

3. 子查询抓取(Subselect fetching) - 另外发送一条SELECT 语句抓取在前面查询到(或者抓取到)的所有实体对象的关联集合。除非你显式的指定lazy="false" 禁止延迟抓取(lazy fetching),否则只有当你真正访问关联关系的时候,才会执行第二条select语句。 

4. 批量抓取(Batch fetching) - 对查询抓取的优化方案, 通过指定一个主键或外键列表,Hibernate使用单条SELECT语句获取一批对象实例或集合。 

Hibernate会区分下列各种情况: 

1. Immediate fetching立即抓取 - 当宿主被加载时,关联、集合或属性被立即抓取。 

2. Lazy collection fetching延迟集合抓取- 直到应用程序对集合进行了一次操作时,集合才被抓取。(对集合而言这是默认行为。) 

3. "Extra-lazy" collection fetching,"Extra-lazy"集合抓取 -对集合类中的每个元素而言,都是直到需要时才去访问数据库。除非绝对必要,Hibernate不会试图去把整个集合都抓取到内存里来(适用于非常大的集合)。 

4. Proxy fetching代理抓取 - 对返回单值的关联而言,当其某个方法被调用,而非对其关键字进行get操作时才抓取。 

5. "No-proxy" fetching,非代理抓取 - 对返回单值的关联而言,当实例变量被访问的时候进行抓取。与上面的代理抓取相比,这种方法没有那么“延迟”得厉害(就算只访问标识符,也会导致关联抓取)但是更加透明,因为对应用程序来说,不再看到proxy。这种方法需要在编译期间进行字节码增强操作,因此很少需要用到。 

6. 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读取数据的代码 移到tx.commit()之前。 

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

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

调整抓取策略(Tuning fetch strategies) 

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

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

1)通过get()或load()方法取得数据。 

2)只有在关联之间进行导航时,才会隐式的取得数据。 

3)条件查询 

4)使用了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次查询的方法是,使用二级缓存。

单端关联代理(Single-ended association proxies) 

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

<class name="CatImpl" proxy="Cat"> 

...... 

<subclass name="DomesticCatImpl" proxy="DomesticCat"> 

..... 

     </subclass> 

</class> 

这里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()方法的持久化类。 

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

实例化集合和代理(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和结束事务将是非常重要的, 请参见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(); 

使用批量抓取(Using batch fetching) 

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

使用子查询抓取(Using subselect fetching) 

假若一个延迟集合或单值代理需要抓取,Hibernate会使用一个subselect重新运行原来的查询,一次性读入所有的实例。这和批量抓取的实现方法是一样的,不会有破碎的加载。

使用延迟属性抓取(Using lazy property fetching) 

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

管理缓存(Managing the caches)-Session缓存 

无论何时,当你给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 

理解集合性能

分类(Taxonomy) 

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

值数据集合 

一对多关联 

多对多关联 

这个分类是区分了不同的表和外键关系类型,但是它没有告诉我们关系模型的所有内容。 要完全理解他们的关系结构和性能特点,我们必须同时考虑“用于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在集合的各数据行中是如何进行“定位”的。) 

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添加新元素! 这是因为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(); 

一次性删除(One shot delete) 

偶尔的,逐个删除集合类中的元素是相当低效的。Hibernate并没那么笨, 如果你想要把整个集合都删除(比如说调用list.clear()),Hibernate只需要一个DELETE就搞定了。 

假设我们在一个长度为20的集合类中新增加了一个元素,然后再删除两个。 Hibernate会安排一条INSERT语句和两条DELETE语句(除非集合类是一个bag)。 这当然是显而易见的。 

但是,假设我们删除了18个数据,只剩下2个,然后新增3个。则有两种处理方式: 

逐一的删除这18个数据,再新增三个; 

删除整个集合类(只用一句DELETE语句),然后增加5个数据。 

Hibernate还没那么聪明,知道第二种选择可能会比较快。 (也许让Hibernate不这么聪明也是好事,否则可能会引发意外的“数据库触发器”之类的问题。) 

幸运的是,你可以强制使用第二种策略。你需要取消原来的整个集合类(解除其引用), 然后再返回一个新的实例化的集合类,只包含需要的元素。有些时候这是非常有用的。 

显然,一次性删除并不适用于被映射为inverse="true"的集合。

监测性能(Monitoring performance)

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

监测SessionFactory 

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

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

例1:

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

例2:

// 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级别)其总结。

数据记录(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执行持久化的过程中,应用程序无法参与其中。因为所有的数据持久化操作,对用户都是透明的,所以用户无法加入自己的动作。  

通过事件框架,Hibernate允许应用程序能响应特定的内部事件,从而实现某些通用的功能,或者允许对Hibernate功能进行扩展。

Hibernate的事件框架由以下两部分组成。

拦截器机制:对于特定动作拦截,回调应用中的特定动作。

事件系统:重写Hibernate的事件监听器。 

拦截器

Interceptor接口提供了从会话(session)回调(callback)应用程序(application)的机制, 这种回调机制可以允许应用程序在持久化对象被保存、更新、删除或是加载之前,检查并(或)修改其 属性。

一个可能的用途,就是用来跟踪审核(auditing)信息。例如:下面的这个拦截器,会在一个实现了 Auditable接口的对象被创建时自动地设置createTimestamp属性,并在实现了 Auditable接口的对象被更新时,同步更新lastUpdateTimestamp属性。 

你可以直接实现Interceptor接口,也可以(最好)继承自EmptyInterceptor。

拦截器可以有两种:Session范围内的,和SessionFactory范围内的。

使用Session范围内的拦截器

当使用某个重载的SessionFactory.openSession()使用Interceptor作为参数调用打开一个session的时候,就指定了Session范围内的拦截器。 

 Session session = sf.openSession( new AuditInterceptor() ); 

使用SessionFactory范围内的拦截器

SessionFactory范围内的拦截器要通过Configuration中注册,而这必须在创建SessionFactory之前。在这种情况下,给出的拦截器会被这个SessionFactory所打开的所有session使用了;除非session打开时明确指明了使用的拦截器。

SessionFactory范围内的拦截器,必须是线程安全的,因为多个session可能并发使用这个拦截器,要因此小心不要保存与session相关的状态。 

new Configuration().setInterceptor( new AuditInterceptor() ); 

事件系统

如果需要响应持久层的某些特殊事件,你也可以使用Hibernate3的事件框架。 该事件系统可以用来替代拦截器,也可以作为拦截器的补充来使用。 

基本上,Session接口的每个方法都有相对应的事件。比如 LoadEvent,FlushEvent,等等(查阅XML配置文件 的DTD,以及org.hibernate.event包来获得所有已定义的事件的列表)。

当某个方 法被调用时,Hibernate Session会生成一个相对应的事件并激活所有配置好的事件监听器。

系统预设的监听器实现的处理过程就是被监听的方法要做的(被监听的方法所做的其实仅仅是激活监听器, “实际”的工作是由监听器完成的)。

不过,你可以自由地选择实现 一个自己定制的监听器(比如,实现并注册用来处理处理LoadEvent的LoadEventListener接口), 来负责处理所有的调用Session的load()方法的请求。

监听器应该被看作是单例(singleton)对象,也就是说,所有同类型的事件的处理共享同一个监听器实例,因此监听器 不应该保存任何状态(也就是不应该使用成员变量)。 

用户定制的监听器应该实现与所要处理的事件相对应的接口,或者从一个合适的基类继承(甚至是从Hibernate自带的默认事件监听器类继承, 为了方便你这样做,这些类都被声明成non-final的了)。

用户定制的监听器可以通过编程使用Configuration对象 来注册,也可以在Hibernate的XML格式的配置文件中进行声明(不支持在Properties格式的配置文件声明监听器)。 

下面是一个用户定制的加载事件(load event)的监听器:

public class MyLoadListener implements LoadEventListener {

    // this is the single method defined by the LoadEventListener interface

    public void onLoad(LoadEvent event, LoadEventListener.LoadType loadType)

            throws HibernateException {

        if ( !MySecurity.isAuthorized( event.getEntityClassName(), event.getEntityId() ) ) {

            throw MySecurityException("Unauthorized access");

        }

    }

}

你还需要修改一处配置,来告诉Hibernate,除了默认的监听器,还要附加选定的监听器。 

<hibernate-configuration>

    <session-factory>

        ...

        <event type="load">

            <listener class="com.eg.MyLoadListener"/>

            <listener class="org.hibernate.event.def.DefaultLoadEventListener"/>

        </event>

    </session-factory>

</hibernate-configuration>

看看用另一种方式,通过编程的方式来注册它。 

Configuration cfg = new Configuration();

LoadEventListener[] stack = { new MyLoadListener(), new DefaultLoadEventListener() };

cfg.EventListeners().setLoadEventListeners(stack);

通过在XML配置文件声明而注册的监听器不能共享实例。如果在多个<listener/>节点中使用了相同的类的名字,则每一个引用都将会产生一个独立的实例。

如果你需要在多个监听器类型之间共享 监听器的实例,则你必须使用编程的方式来进行注册。 

为什么我们实现了特定监听器的接口,在注册的时候还要明确指出我们要注册哪个事件的监听器呢? 这是因为一个类可能实现多个监听器的接口。在注册的时候明确指定要监听的事件,可以让启用或者禁用对某个事件的监听的配置工作简单些。 

Hibernate的声明式安全机制

通常,Hibernate应用程序的声明式安全机制由会话外观层(session facade)所管理。 现在,Hibernate3允许某些特定的行为由JACC进行许可管理,由JAAS进行授权管理。 本功能是一个建立在事件框架之上的可选的功能。 

首先,你必须要配置适当的事件监听器(event listener),来激活使用JAAS管理授权的功能。 

<listener type="pre-delete" class="org.hibernate.secure.JACCPreDeleteEventListener"/>

<listener type="pre-update" class="org.hibernate.secure.JACCPreUpdateEventListener"/>

<listener type="pre-insert" class="org.hibernate.secure.JACCPreInsertEventListener"/>

<listener type="pre-load" class="org.hibernate.secure.JACCPreLoadEventListener"/>

注意,<listener type="..." class="..."/>只是<event type="..."><listener class="..."/></event>的简写,对每一个事件类型都必须严格的有一个监听器与之对应。 

接下来,仍然在hibernate.cfg.xml文件中,绑定角色的权限: 

<grant role="admin" entity-name="User" actions="insert,update,read"/>

<grant role="su" entity-name="User" actions="*"/>

这些角色的名字就是你的JACC provider所定义的角色的名字。

Hibernate的事件框架(拦截器机制与事件系统)

通常,载Hibernate执行持久化的过程中,应用程序无法参与其中。因为所有的数据持久化操作,对用户都是透明的,所以用户无法加入自己的动作。 

   通过事件框架,Hibernate允许应用程序能响应特定的内部事件,从而实现某些通用的功能,或者允许对Hibernate功能进行扩展。

   Hibernate的事件框架由以下两部分组成。

      ●拦截器机制:对于特定动作拦截,回调应用中的特定动作。

   ●事件系统:重写Hibernate的事件监听器。

   1.拦截器:

      通过Inteceptor接口,可以从Session中回调应用程序的特定方法,这种回调机制可让应用程序在持久化对象被保存,更新,删除或加载之前,对其进行检查并修改其属性。

      通过Inteceptor接口,可以在数据进入数据库之前,对数据进行最后的检查,如果数据不符合要求,则可以修改,从而避免非法数据进入数据库。当然,通常无须这样做,只有在某些特殊的场合下,才考虑使用拦截器完成检查功能。

      使用拦截器时按如下步骤进行:

         (1)定义实现Interceptor接口的拦截器类。

         (2)通过Session起用拦截器,或者通过Configuration启用全局拦截器。

      下面是一个拦截器的简单示例的代码:

public class MyInterceptor extends EmptyInterceptor {

    public void onDelete(Object entity,Serializable id,Object[] state,

        String[] propertyNames,Type[] types) {

         System.out.println("该方法在删除数据时调用");

   }

}

   拦截器的使用有两种方法:

      ●通过SessionFactory的openSession(Interceptor in)方法打开一个带局部拦截器的Session。

   ●同过Configuration的setInterceptor(Interceptor in)方法设置全局拦截器。

      下面是使用拦截器的示例的部分代码:

public class HibernateUtil {

   public static final SessionFactory sessionFactory;

   static{

      try{

         Configuration configuration = new Configuration().configure();

         sessionFactory = configuration.buildSessionFactory();

      }catch (Throwable ex) {

         System.err.println("初始化sessionFactory失败。" + ex);

         throw new ExceptionInInitializerError(ex);

      }

   }

   public static final ThreadLocal session = new ThreadLocal();

   public static Session currentSession(Interceptor it) throws HibernateException {

      Session s = (Session) session.get();

      if(s == null) {

         s = sessionFactory.openSession(it);

         session.set(s);

      }

      return s;

   }

   public static void closeSession() throws HibernateException {

       Session s = (Session) session.get();

       if (s != null)

         s.close();

       session.set(null);

   }

}

   2.事件系统:

      Hibernate3事件系统是功能更强大的事件框架,该事件系统可以替代拦截器,也可以作为拦截器的补充来使用。

      基本上,Session接口的每个方法都有对应的事件。比如LoadEvent,FlushEvent等。事实上,当Session调用某方法时,Hibernate Session会生成对应的事件,并激活对应的事件监听器。在系统默认监听器实现的处理过程中,完成了对所有的数据持久化的操作,包括插入和修改等操作。如果用户定义了自己的监听器,则意味着用户必须完成对象的持久化操作。

      例如,可以在系统中实现并注册LoadEventListener,该监听器负责处理所有调用Session的load()的方法的请求。

      由于监听器是单态模式对象,即所有的同类型的事件处理共享同一个监听器实例,因此,此监听器不应该保存任何状态,即不应该使用成员变量。

      使用事件系统时按如下步骤进行:

         (1)实现自己的事件监听器类。

         (2)注册自定义事件监听器,代替系统默认的事件监听器。

      实现用户的自定义监听器有如下三个方法:

      ●实现对应的监听器接口:这是不可思议的,实现接口必须实现接口内的所有方法,关键是必须实现Hibernate对应的持久化操作,即数据库访问,这意味着程序员完全取代了Hibernate的底层操作。

   ●继承事件适配器:可以选择地实现需要关注的方法,但依然需要替代Hibernate完成数据库访问。这也不太现实。

   ●继承系统默认的事件监听器:扩展特定的方法。

      实际上,前两种方法很少使用。因为Hibernate的持久化操作也是通过这些监听器实现的,如果用户取代了这些监听器,则应该自己实现持久化操作,这意味着用户放弃了Hibernate的持久化操作,而改为自己完成Hibernate的核心操作。

      通常我们采用第三种方法实现自己的事件监听器。因为Hibernate默认的事件监听器被声明成non-final,从而方便用户继承。

      下面是用户自定义监听器的示例:

//自定义LoadListener,继承默认的DefaultLoadEventListener实现类

public class MyLoadListener extends DefaultLoadEventListener {

    //在LoadEventListener接口仅仅定义了这个方法

    public Object onLoad(LoadEvent event,LoadEventListener.LoadType loadType)

        throws HibernateException {

        Object o = super.onLoad(event,loadType);

        System.out.println("自定义的load事件");

        System.out.println(event.getEntityClassName() + "---------"

             + event.getEntityId());

        return o;

    }

}

      注册用户自定义监听器也有以下两种方法。

         ●编程式:通过使用Configuration对象编程注册。

     ●声明式:在Hibernate的XML格式的配置文件中进行声明,使用Properties格式的配置文件将无法配置自定义监听器。

      通过编程式方式使用自定义监听器时,只需将HibernateUtil中的静态块儿用如下的静态块儿替代即可:

      static{

         try{

            Configuration cfg = new Configuration();

            cfg.getSes

sionEventListenerConfig().setLoadEventListener(

               new MyLoadListener());

            sessionFactory = cfg.configure().buildSessionFactory();

   

         } catch(Throwable ex) {

            System.err.println("初始化sessionFactory失败。" + ex);

            throw new ExceptionInInitializerError(ex);

         } 

      }

      使用声明式方式使用自定义监听器时,可在Hibernate配置文件中的<session-factory>元素中增加:

         <listener type="load" class="MyLoadListener">

子元素即可。

      使用配置文件注册事件监听器虽然方便,但也有不利之处:通过配置文件注册的监视器不能共享实例;如果有多个<listener/>元素中使用了相同的类,则每个引用都将产生一个新拦截器实例;如果需要在多个事件之间共享监听器的实例,则必须使用编程式方式来注册事件监听器。

      虽然监听器类实现了特定监听器的接口,但在注册时还要明确指出注册的事件。因为一个类可能实现多个监听器的接口,所以,在注册时明确指定要监听的事件,可以使得启用或者禁用某个事件的监听配置工作更加简单。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值