Hibernate的高级操作

3,507 views

Published on

0 Comments
1 Like
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
3,507
On SlideShare
0
From Embeds
0
Number of Embeds
3
Actions
Shares
0
Downloads
43
Comments
0
Likes
1
Embeds 0
No embeds

No notes for slide

Hibernate的高级操作

  1. 1. 第5章 Hibernate 高级特性 在前面的内容中,我们探讨了 Hibernate 的基础使用技术。通过对这些基础技术的把握,我们即 可开始进行基于 Hibernate 的持久层开发。然而,在这些应用技术之后,存在着怎样的运行机制,及 其内部实现方式对应用层可能产生怎样的影响,则是我们下面需要关注的问题。 本章主要分两大部分进行介绍: 1. Hibernate 持久化实现 介绍 Hibernate 对象持久化操作的实现机理。其中包括以下内容: 实体对象生命周期 实体对象识别 数据缓存 事务管理 持久层操作 2. Hibernate 回调与拦截机制 介绍了 Hibernate 中提供的事件捕获和处理机制。其中包括以下内容: Lifecyle 与 Validatable 接口 Hibernate Interceptor 5.1 Hibernate 持久化实现 5.1.1 实体对象生命周期 实体对象的 3 种状态 实体对象的生命周期,是 Hibernate 应用中的一个关键概念。对生命周期的理解和把握,不仅对 Hibernate 的正确应用颇有裨益,而且对 Hibernate 实现原理的探索也极具意义。下面的内容中,我们 就围绕这个主题进行讨论。
  2. 2. 这里的实体对象,特指 Hibernate O/R 映射关系中的域对象(即 O/R 中的“O”。 ) 实体对象生命周期中的 3 种状态(考虑到中英术语意译上可能的语义丢失,下面的内容中,我们 将直接引用英文术语进行描述): 1. Transient(自由状态) 所谓 Transient,即实体对象在内存中的自由存在,它与数据库中的记录无关。如: public void methodA{ TUser user = new TUser(); user.setName("Emma"); } 这里的 user 对象,与数据库中的记录没有任何关联。 2. Persistent(持久状态) 何谓 Persistent? 即实体对象处于由 Hibernate 框架所管理的状态。这种状态下,实体对象的引用 被纳入 Hibernate 实体容器中加以管理。 处于 Persistent 状态的对象,其变更将由 Hibernate 固化到数据库中。 看下面的例子: TUser user = new TUser(); TUser anotherUser = new TUser(); user.setName("Emma"); anotherUser.setName("Kevin"); //此时user和anotherUser都处于Transient状态 Transaction tx = session.beginTransaction(); session.save(user); //此时的user对象已经由Hibernate纳入实体管理容器,处于Persistent状态 //而anotherUser仍然处于Transient状态 tx.commit(); //事务提交之后,库表中已经插入一条用户"Emma "的记录 //对于anotherUser则无任何操作 Transaction tx2 = session.beginTransaction(); user.setName("Emma_1"); //Persistent anotherUser.setName("Kevin_1");//Transient tx2.commit(); //虽然这个事务中我们没有显式调用Session.save方法保存user对象 //但是由于处于Persistent状态的对象将自动被固化到数据库中,因此user //对象的变化也将被同步到数据库中 //也就是说数据库中"Emma"的用户记录已经被更新为"Emma_1" //此时anotherUser仍然是个普通Java对象,处于Transient状态,它不受 //Hibernate框架管理,因此其属性的更改也不会对数据库产生任何影响 可以看到,处于Transient状态的实体对象,可以通过Session.save方法转换为Persistent状态。而同 样,如果一个实体对象是由Hibernate加载(如通过Session.load方法获得),那么,它也处于Persistent 状态。如: //由Hibernate返回的Persistent对象 TUser user = (TUser)session.load(TUser.class,new Integer(1)); //Session.load方法中,在返回对象之前,Hibernate就已经将此对象纳入其 //实体容器中,这里的user对象即处于Persistent状态 Persistent对象对应了数据库中的一条记录,可以看作是数据库记录的对象化操作接口,其状态的 变更将对数据库中的记录产生影响。 简而言之,如果一个实体对象与某个Session实例发生了关联,并处于对应Session的有效期内,那
  3. 3. 么它就处于Persistent状态。 3. Detached(游离状态) 处于 Persistent 状态的对象,其对应的 Session 实例关闭之后,那么,此对象就处于“Detached” 状态。 Session 实例可以看作是 Persistent 对象的宿主,一旦此宿主失效,那么其从属的 Persistent 对象即 进入 Detached 状态。 我们来看以下实例: TUser user = new TUser(); user.setName("Emma"); //此时user处于Transient状态 Transaction tx = session.beginTransaction(); session.save(user); //此时的user对象已经由Hibernate纳入管理容器,处于Persistent状态 tx.commit(); session.close(); //user对象此时状态为Detached,因为与其关联的session已经关闭 上面的例子中,user 对象从 Persistent 状态转变为 Detached 状态。那么,这里的 Detached 状态与 Transient 状态有什么区别? 区别就在于 Detached 对象可以再次与某个 Session 实例相关联而成为 Persistent 对象: TUser user = new TUser(); user.setName("Emma"); //此时user处于Transient状态 Transaction tx = session.beginTransaction(); session.save(user); //此时的user对象已经由Hibernate纳入管理容器,处于Persistent状态 tx.commit(); session.close(); //user对象此时状态为Detached,因为与其关联的session已经关闭 Transaction tx2 = session2.beginTransaction(); session2.update(user); //此时处于Detached状态的user对象再次借助session2由Hibernate纳入管 //理容器,恢复Persistent状态 user.setName("Emma_1"); //由于user对象再次处于Persistent状态,因此其属性变更将自动由 //Hibernate固化到数据库中 tx2.commit(); 可以看到,这里我们通过 Session.update 方法将 Detached 对象再次与 Hibernate 持久层容器相关联, 因而 user 对象又转变为 Persistent 状态。 一个很自然的问题。 这个 Detached 状态的 user 对象,与最初的 Transient 状态的 user 对象到底有何区别? 既然 Detached 状态的 user 对象已经与 Hibernate 实体容器无关,那么这两者还有什么差异? 回顾上面的代码。 通过如下代码,我们创建了 Transient 状态的 user 对象: TUser user = new TUser(); user.setName("Emma"); 而借助 session.save 方法,我们将其转变为 Persistent 状态。
  4. 4. session.save(user); 关键就在这里,在 session.save 方法执行过程中,user 对象的内容已经发生了改变。 在创建 Transient 对象时,我们为 user 对象设定了一个 name 属性。此时 user 对象所包含的数据 信息也仅限于此,它与数据库中的记录并不存在对应关系。 而 Session.save 执行之后,Hibernate 对 user 对象进行了持久化,并为其赋予了主键值。在这里, 也就是 user.id 属性(回顾之前的示例场景,TUser 类中,id 属性被设定为自增型主键),由于 id 属性 是主键,可以惟一确定库表中的一条记录,那么,这个 user 对象自然就可以与库表中具备相同 id 值 的记录相关联。 这就是前后两个状态中,user 对象之间的基本差异,Transient 状态的 user 对象与库表中的数据缺 乏对应关系,而 Detached 状态的 user 对象,却在库表中存在相对应的记录(由主键惟一确定),只不 过由于 Detached 对象脱离了 Session 这个数据操作平台,其状态的变化无法更新到库表中的对应记录。 就目前这个示例,简而言之,Transient 状态中的实体对象,无主键信息,而 Detached 状态的实 体对象,包含了其对应数据库记录的主键值。 我们也可以人工制造一个 Detached 状态对象: Tuser user = new Tuser(); user.setName("Emma"); //硬编码为其指定主键值(假设库表中存在id=1的记录) user.setId(new Integer(1)); //此时user对象成为一个“人造detached对象” Transaction tx= session.beginTransaction(); session.update(user); //Session根据其主键值,将其转变为Persistent状态 user.setAge(new Integer(20)); tx.commit(); //观察数据库中的记录,发现Age字段的值已经发生变化。 session.close(); 对于这里的简单示例,我们可以通过编码将一个 Transient 状态的对象手动的与库表记录形成关 联,使其转变为一个 Detached 状态的对象,此时,我们手工构造的这个 Detached 对象与通过 Session 构造的 Detached 对象并没有什么区别。 不过,考虑到实际情况可能并非这么简单,Hibernate 在判定对象处于 Detached 状态还是 Transient 状态时,有着更加复杂的机制。 判定一个对象是否处于 Transient 状态的条件: 1. 首先,对象的 id 属性(如果此属性存在的话)是否为 null。 对于上面的示例,Hibernate 即根据此条件进行判定。 2. 如果指定了 id 属性的 unsaved-value(请参见稍后对 unsaved-value 的讨论),那么 id 属性是 否等于 unsaved-value。 3. 如果配备了 Version 属性(参见稍后的“事务管理-乐观锁”部分描述),version 属性是否 为 null。 4. 如果配备了 Version 属性,且为 vesion 指定了 unsaved-value,version 属性值是否等于 unsave-value。 5. 如 果 存 在 Interceptor ( 参 见 稍 后 的 “ Hibernate 回 调 与 拦 截 机 制 ” 部 分 内 容 ) 那 么 , Interceptor.isUnsaved 方法是否返回 true。
  5. 5. 相对与 Persistent 状态与 Detached 状态的转变。实体对象从 Persistent 状态转变为 Transient 状态, 一般由 session.delete 方法完成: Tuser user = new Tuser(); user.setName("Emma"); Transaction tx= session.beginTransaction(); session.save(user);//Transient => Persisent tx.commit(); Transaction tx2= session.beginTransaction(); session.delete(user); tx2.commit();//Persistent => Transient System.out.println(user.getId());//打印user.id属性值 通过 session.delete 方法, Persistent 状态的 user 对象转变为 Transient 状态。 代码最后我们打印出了其 id 值,可以看到,这个 id 值非 null,这是否意味着 user 对象是处于 Detached 状态? 这里再次重复一下 Detached 状态与 Transient 状态之间的差异,Transient 状态的实体对象与库表 中的记录无关,我们无法根据 Transient 对象中的信息在库表中寻找到对应的记录,而 Detached 状态 的对象,虽然与 Session 实例脱离,但我们根据其中的信息,能够寻找到库表中对应的数据记录。 而这里,这个 id 所对应的库表记录已经删除,此时的 user 对象与库表中的记录已经不存在对应 关系,因此,它处于 Transient 状态。 VO 与 PO 有时候,为了方便,我们也将处于 Transient 和 Detached 状态的对象统称为值对象(VO 即 Value ,而将处于 Persistent 状态的对象称为持久对象(PO 即 Persistence Object) Object) 。 这是站在“实体对象是否被纳入 Hibernate 实体管理容器”的立场加以区分的,非管理的实体对 象统称 VO,而被管理的实体对象称为 PO。 再从 VO 和 PO 的角度重复一下上面的描述: VO 和 PO 的主要区别在于: 1. VO是相对独立的实体对象,处于非管理状态。 2. PO是由Hibernate纳入其实体管理容器(Entity Map)的对象,它代表了与数据库中某条记录对 应的Hibernate实体,PO的变化在事务提交时将反映到实际数据库中。 3. 如果一个PO与其对应的Session实例分离,那么此时,它又会变成一个VO。 由 PO、VO 的概念,又引申出一些系统层次设计方面的问题。如在传统的 MVC 架构中,位于 Model 层的 PO,是否允许被传递到其他层面。由于 PO 的更新最终将被映射到实际数据库中,如果 PO 在其他层面(如 View 层)发生了变动,那么可能会对 Model 层造成意想不到的破坏。 因此,一般而言,应该避免直接将 PO 传递到系统中的其他层面,一种解决办法是,通过构造一 个新的 VO,通过属性复制使其具备与 PO 相同的属性值,并以其为传输媒质(实际上,这个 VO 被 用作 Data Transfer Object,即所谓的 DTO),将此 VO 传递给其他层面以实现必须的数据传送。 属 性 复 制 可 以 通 过 Apache Jakarta Commons Beanutils ( http://jakarta.apache.org/ commons/beanutils/)组件提供的属性批量复制功能,避免繁复的 get/set 操作。
  6. 6. 下面的例子中,我们把 user 对象的所有属性复制到 anotherUser 对象中: TUser user = new TUser(); TUser anotherUser = new TUser(); user.setName("Emma"); user.setAge(new Integer(1)); try { BeanUtils.copyProperties(anotherUser,user); System.out.println("UserName => " +anotherUser.getName() ); System.out.println("User Age => " + anotherUser.getAge() ); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } 5.1.2 实体对象识别 实体身份识别(Data Identity) 在 Java 语言中,对象之间的比较主要通过以下两种方式: 1. 引用比较(==) 引用比较的作用是判断两个变量是否引用了同一个对象实例。如 TUser user1 = new TUser(); TUser user2 =user1; if (user1 == user2){ …… } 2. 内容比较 String string1="string1"; String string2="string2"; if (string1.equals(string2)){ …… } 内容比较的目的是为了判定两个对象所包含的数据是否相同。 以上两种方式是 Java 语言中对象比较的基本方式,基于这种机制,我们可以很方便地分辨对象 之间的差异。而这里,面对持久层逻辑,我们必须面对新的问题:如何判定两个实体对象是否相等。 假设出现这样的情况: TUser user1=(TUser)session.load(TUser.class,new Integer(1)); user1.setAge(new Integer(21)); TUser user2=(TUser)session2.load(TUser.class,new Integer(1)); 上面的示例中,user1 和 user2 这两个对象是否相等? 从 Java 语言规范的角度而言,这两个对象无论是引用,还是具体内容都不相同。但是,站在持
  7. 7. 久层角度而言,这两个对象却都代表着数据库中的同一条记录(t_user 表中 id 为 1 的记录) ,具备等 价的含义。这种等价关系,是由于持久层逻辑的出现而引入的,而这也同时引出了下面我们所要探讨 的主题:实体对象的身份识别。 如何确定一个实体对象的身份?站在数据库的角度,我们认为,在一个库表结构中,主键可以惟 一确定一条记录,那么,对于拥有同样主键值的实体对象,则认为他们等同。 如上面的例子中,id 是 t_user 表的主键,对于两个 TUser 对象,只要其主键值相同,我们则认为 他们等同。 对于 Hibernate 而言,这个规则也成立。net.sf.hibernate.engine.Key 类(Hibernate 3 中对应类为 org.hibernate.engine.Key)封装了 Hibernate 用于区分两个实体对象的识别信息。 图 5-1 是一个 Key 对象的运行期内存快照。 图 5-1 Key 对象的运行期内存快照 可以看到, 中主要维持了 3 个属性, Key 实体类,实体类名和实体 ID。通过实体类名和 ID,Hibernate 即可确定这个实体在数据库中的对应库表和记录,从而将其与其他对应不同记录的实体对象区分开 来。 另外,Key 在 Hibernate 缓存中也扮演着数据标识的角色,Hibernate 将根据 Key 在缓存中寻找是 否有对应的数据存在。 于此同时,在持久层之外,对象是否等价在业务逻辑层可能还有另外的含义,往往存在一些特定 的数据实体判定规则。 如,对于 t_user 表中两条不同的记录。其 name 字段相同,那么我们就认为,这两条记录实际上 对应着同一个人,从这个角度上来看,这两个数据实体等价。此时我们用作判定的条件,既非对象引 用、对象内容,也非类名和 ID,而是特定领域中的逻辑规则。 这样的逻辑规则如何体现在我们的实体对象之间?比较自然的方法是通过覆盖 Object.equals 方 法来实现。 Equals 和 hashCode 方法 Java Collection 将通过 hashCode/equals 方法判定两个对象是否相等。 我们知道,Set 集合类型不允许集合中出现两个相同对象。如: Set set = new HashSet(); TUser user1 = new TUser(); TUser user2 = new TUser(); user1.setId(new Integer(1)); user2.setId(new Integer(1)); set.add(user1); set.add(user2); System.out.println("Items in set =>"+set.size());
  8. 8. 观察屏幕输出:Items in set =>2; 这里的 TUser 对象并没有覆盖 Object.equals/hashCode 方法,因此 Collection 将调用 TUser 的父类 (也就是 Object)的 equals/hashCode 方法判断这两个对象是否相等: public boolean equals(Object obj) { return (this == obj); } Object.equals 方法只是简单对比了两个对象的引用是否相等,显然,这里 user1==user2 并不成立, 于是 Collection 判定这两个对象互不相等,将其分别纳入集合中。 现在,我们修改一下 TUser 类,使之覆盖 Object.equals 和 Object.hashCode 方法: public class TUser implements Serializable { …… public boolean equals(Object object) { TUser usr = (TUser) object; return this.getId().equals(usr.getId()); } public int hashCode() { return this.getId().intValue(); } } 再次运行之前的测试代码,我们看到输出:Items in set =>1; Set 集合认为 user1 和 user2 是两个相同的对象,因此,只在集合中维持了一个实例 user1。 Collection 在判断两个对象是否相等的时候,会首先调用对象的 hashCode 方法,如果 hashCode 相同的话,随即调用其 equals 方法,如果两次判断均为真,则认为对比的两个对象相等。 其实上面的现象,在 Java 日常开发中我们也可能经常碰到,感兴趣的读者可以看看 java.lang.Integer 类的 hashCode 和 equals 方法实现,再尝试一下在 Set 中重复添加 intValue=1 的 Integer 对象观察结果。 对于我们上面这个改造后的 TUser 类而言,无论其实例之间其他属性取值有怎样的差异,只要其 id 相等,则 Set 集合中只会维持相同 id 的一个实例。 那么,这对 Hibernate 又意味着什么? 如果我们在 MiddleGen 的 OR 映射选项里选择了 Generate Equals/HashCode,那么通过 hbm2java 生成最后的代码时,我们会发现代码中添加了如下的 Equals/hashCode 方法,以 TAddress 类为例: public boolean equals(Object other) { if ( (this == other ) ) return true; if ( !(other instanceof TAddress) ) return false; TAddress castOther = (TAddress) other; return new EqualsBuilder() .append(this.getId(), castOther.getId()) .isEquals(); } public int hashCode() { return new HashCodeBuilder() .append(getId()) .toHashCode(); } 这两个方法的原理其实跟之前我们手工编码的实现原理类似:如果 id 相同,则认为对比的两个 对象相同。这实际上也符合我们之前讨论的持久层的实体对象身份识别原则。不过,这里却产生了另 外一个问题。 再尝试运行以下代码: TUser user = (TUser)session.load(TUser.class,new Integer(2));
  9. 9. TAddress addr1 = new TAddress(); addr1.setAddress("Shanghai"); TAddress addr2 = new TAddress(); addr2.setAddress("Guangdong"); user.getAddresses().add(addr1);//addr1.id==null user.getAddresses().add(addr2);//addr2.id==null System.out.println("Items in set =>"+user.getAddresses().size()); 从代码逻辑上来看,其目的显然是想为 user 对象添加两个关联的地址对象。 但是,运行此代码,我们却发现输出是:Items in set =>1 也 就 是 说 , addr2 并 没 有 真 正 被 加 入 到 user.addresses 集 合 中 去 。 这 样 , 我 们 在 稍 后 调 用 session.save(user)对象时,也只会向数据库插入一条地址数据。 为什么出现这样的现象?原因就在于主键值的生成机制,对于这里的 TAddress 对象,我们采用 了“identity”的 id 生成方式,其 id 只有在 Session.save 方法执行之后才会被设置。 我们在向 user.addresses 集合中添加对象时,Session.save 方法尚未执行,因此,id==null,参照上 面 equals/hashCode 的实现机制,只要 id 相同(现在 id 都等于 null) ,则认为这两个对象相同,因此只 在集合中维持了 addr1,而没有将 addr2 添加进去。 那么,我们是否应该在实体类中覆盖 equals/hashCode 方法?如果应该覆盖的话,采取怎样的 equals/hashCode 实现比较合适? 首先,我们来看在不覆盖 equals/hashCode 方法的情况下,可能出现什么问题。 分析以下代码: TUser user = (TUser)session.load(TUser.class,new Integer(1)); System.out.println("Address count=> "+user.getAddresses().size()); Iterator it = user.getAddresses().iterator(); TAddress addr = (TAddress)it.next(); //////////////////////////////////////////////////////////// TUser user2 = (TUser)session2.load(TUser.class,new Integer(1)); user2.getAddresses().add(addr); System.out.println("Address count=> "+user2.getAddresses().size()); Transaction tx = session2.beginTransaction(); session2.save(user2); tx.commit(); 代码用反斜杠分隔为两部分,第一部分我们加载了一个 id=1 的 User 对象,并获得了它所关联的 一个 TAddress 对象 addr(假设此 user 有 3 个关联 address 对象) 。 第 2 部分,我们通过另外一个 Session 再次加载了 id=1 的 user 对象,并将前面的 addr 对象加入 其中。 注意,当 id=1 的 user 对象加载的时候,其 addresses 属性已经包含了所关联的 3 条记录,这样, 由于 addr 对象的引用与 user.addresses 中的 3 个对象都不相同,addr 成功加入。 这就导致了一个问题,user.addresses 中包含了两个针对同一库表记录的实体。 这样,执行 session2.save 方法时,我们将得到一个 NonUniqueObjectException 异常。 观察屏幕输出: Address count=> 3
  10. 10. Address count=> 4 net.sf.hibernate.NonUniqueObjectException: …… …… 这就是在不覆盖 equals/hashCode 方法的情况下我们所面对的问题:实体对象的跨 Session 识别。 假设上面的代码中只使用了一个 session 实例,那么第二次加载 user 对象及其关联的 address 时, addresses 集合中的 3 个 address 对象实际上与第一次加载的 3 个完全一样(Session 在第一次加载时将 这些数据在内部进行了缓存,第二次直接返回这些缓存中的实例引用)。这样在添加 addr 时,集合会 将其判定为已存在元素从而维持不变。这样就不会出现 NonUniqueObjectException 异常。 我们的实际开发中,这种情况可能比较少见,如果确定系统中不会出现类似的冲突,那么我们可 以不必覆盖 equals/hashCode 方法。 但是,如果系统中可能出现类似的冲突,该如何面对? 一个方法是实现所谓的值比对,即在 equals/hashCode 方法中,对实体类的所有属性值进行比对, 如果两个实体类的属性值都一致,那么判定为相等,否则反之。 如: public boolean equals(Object object) { if (!(object instanceof TAddress)) { return false; } TAddress rhs = (TAddress) object; return new EqualsBuilder() .appendSuper(super.equals(object)) .append(this.userId, rhs.userId) .append(this.type, rhs.type) .append(this.idx, rhs.idx) .append(this.address, rhs.address) .append(this.tel, rhs.tel) .append(this.zipcode, rhs.zipcode) .append(this.id, rhs.id) .isEquals(); } public int hashCode() { return new HashCodeBuilder(-599736627, 1187168773) .appendSuper(super.hashCode()) .append(this.userId) .append(this.type) .append(this.idx) .append(this.address) .append(this.tel) .append(this.zipcode) .append(this.id) .toHashCode(); } 上面的 equals/hashCode 方法将实体类的所有属性都纳入了运算,实现了“值比对”机制。 为每个类都编写如上的代码无疑是件苦差使,好在现在已经有了许多辅助工具来帮我们自动完成 上 面 的 工 作 , 如 Intellij IDEA 中 已 经 内 置 了 equals/hashCode 方 法 的 自 动 生 成 功 能 (Code->Generate->equals&hashCode 菜单),而 Eclipse 中也有对应的免费插件可以使用: a) Commonclipse(http://commonclipse.sf.net) 上面的 equals/hashCode 方法就是由 Commonclipse 插件自动生成。 b) Commons4E(http://commons4e.berlios.de/) 另外注意,使用“值比对”方法只需针对实体类的属性进行处理,而不要涉及实体类所关联的集
  11. 11. 合类的比对,否则在多对多关系中很容易引发一些其他的问题。 值比对的缺点在于检查过于严格,属性稍有差异,对象即被判定为不等,在某些情况下,这样的 策略并不适用。 之前我们曾经讨论过业务逻辑层中的对象判定问题,除了“值比对”,还有另外一种基于业务逻 辑的对象判定方式“业务关键信息判定”。 业务关键信息判定实际上是值比对的一个子集,也就是说,在进行实体属性比对的时候,我们只 对一些业务关键属性进行判断,如之前讨论业务逻辑层的对象判定时所提及的例子:如果两个 user 对象的 name 属性相等,则判定为等同。如: public boolean equals(Object object) { if (!(object instanceof TUser)) { return false; } TUser rhs = (TUser) object; return new EqualsBuilder() .appendSuper(super.equals(object)) .append(this.name, rhs.name) .isEquals(); } public int hashCode() { return this.name.hashCode(); } 这种方式需要针对业务逻辑进行判定,因此需要特别小心。如对上面的例子,我们在实施此判定 策略之前必须保证以下前提:t_user 表中不可能出现 name 相同的记录(name 为逻辑主键) 。 脏数据检查 何谓脏数据(Dirty Data)? 这里的“脏”可能有些误导,脏数据并非废弃或者无用的数据,而是指一个数据对象所携带的信 息发生了改变之后的状态。 如我们从数据库中读取了一个 TUser 对象: Transaction tx = session.beginTransaction(); TUser user = (TUser)session.load(TUser.class,new Integer(1)); //此时user对象处于由数据库读出的原始状态 user.setAge(30); //此时user对象所携带的信息发生了变化,成为所谓的“脏数据” tx.commit(); 事务提交时,Hibernate 会对 session 中的 PO 进行检测,判断那些发生了变化,并将发生变化的 数据更新到数据库中。 这里就存在一个问题,Hibernate 如何判断一个数据对象是否发生了改变,或者说,Hibernate 如 何进行脏数据识别? 脏数据检查的一般策略大致有下面两种: 1. 数据对象监控 数据对象监控的实现方式,大体上是通过拦截器对数据对象的设值方法(setter)进行拦截, 拦截器的实现可以借助 Dynamic Proxy1或者 CGlib 实现。一旦数据对象的设置方法被调用(通 1 参见第1部分中关于Dynamic Proxy模式的描述。
  12. 12. 常这也就意味着数据对象的内容发生变化),则将其标志为“待更新”状态,之后在数据库 操作时将其更新到对应的库表。 2. 数据版本比对 在持久层框架中维持数据对象的最近读取版本,当数据提交时将提交数据与此版本进行比 对,如果发生变化则将其同步到数据库相应的库表。 Hibernate 采取的是第二种检查策略。 结合一个实例,我们来探讨一下 Hibernate 脏数据检查的具体实现: TUser user = (TUser) session.load(TUser.class, new Integer(1)); Transaction tx = session.beginTransaction(); user.setName("Kevin"); tx.commit(); 1. 首先,我们通过 Hibernate 加载 id=1 的 user 对象: TUser user = (TUser) session.load(TUser.class, new Integer(1)); 假设此时 user.name 属性值为 “Emma”。 2. 启动事务 Transaction tx = session.beginTransaction(); 3. 调用 user 的设值方法,将其 name 属性修改为“Kevin” user.setName("Kevin"); 4. 事务提交,好戏开场 tx.commit(); Transaction.Commit 方法随即调用 Session.flush: public void commit() throws HibernateException { …… if ( session.getFlushMode()!=FlushMode.NEVER ) session.flush(); …… } Session.flush()方法中,会完成两个主要任务: public void flush() throws HibernateException { …… flushEverything();//刷新所有数据 execute();//执行数据库SQL完成持久化动作 …… } flushEverything 会首先完成一些预处理工作(如调用对应的 interceptor、协同级联关系等) ;之后, 即调用 flushEntities 方法对当前 Session 中的实体对象进行刷新,而这个过程,也是脏数据判定的关键。 在继续下面的过程探讨之前,我们首先来看一个内部数据结构“EntityEntry”: static final class EntityEntry implements Serializable { LockMode lockMode; //当前加锁模式 Status status;// 当前状态[Loaded,Deleted,Loading,Saving…] Serializable id; Object[] loadedState; //实体最近一次与数据库的同步版本 Object[] deletedState; //实体最近一次删除时的版本 boolean existsInDatabase; Object version; //版本号[用于乐观锁,请参见事务管理部分]
  13. 13. // ClassPersister是针对实体类的持久化封装,通过它我们可以获得实体类 //属性对应的数据库字段类型等信息,或者执行对应的持久化操作(如insert、 //update) transient ClassPersister persister; String className; boolean isBeingReplicated; EntityEntry( Status status, Object[] loadedState, Serializable id, Object version, LockMode lockMode, boolean existsInDatabase, ClassPersister persister, boolean disableVersionIncrement ) { this.status = status; this.loadedState = loadedState; this.id = id; this.existsInDatabase = existsInDatabase; this.version = version; this.lockMode = lockMode; this.isBeingReplicated = disableVersionIncrement; this.persister = persister; if (persister!=null) className = persister.getClassName(); } } EntityEntry 是从属于 SessionImpl(SessionImpl 是 Session 接口的实现)的一个内部类,每个 EntityEntry 对应一个实体类实例,保存了该实体类的状态信息,如其最近一次与数据库同步时的版本 (loadedState)等。 为了更加形象化地理解,下面给出了本例在运行期间,user 对象对应的 EntityEntry 内存快照(图 5-2)。 图 5-2 user 对象对应的 EntityEntry 内存快照 可以看到, EntityEntry 中包含了对应实体对象的所有状态信息,特别是在其 loadedState 属性中, 保存了实体对象最近一次与数据库同步的版本副本。 前面说过,Hibernate 实现脏数据检查机制是基于数据版本比对机制,而这也就是 Hibernate 实现 脏数据判定的原始依据。 在 Session 中,保存了所有与当前 Session 实例相关联的实体对象的当前实例和原始状态信息(即
  14. 14. EntityEntry)。这两者以“key-value”的形式,保存在 SessionImpl.entityEntries 数据结构中。 SessionImpl.entityEntries 是一个 Map 型的数据结构,其中每个项目(Entry)都包含了当前与 Session 关联的一个实体对象实例及其原始信息。以实体对象为 Key,而以对应的 EntityEntry 为 Value。 Session.flushEntities 方法的工作,就是遍历 entityEntries,并将其中的实体对象与其原始版本进行 比对,判断对象状态是否更改。 private void flushEntities() throws HibernateException { //将Map型的entityEntries转换为List,用于循环遍历 List list = IdentityMap.concurrentEntries(entityEntries); int size = list.size(); for ( int i=0; i<size; i++ ) { Map.Entry me = (Map.Entry) list.get(i); //取出Entry.Value中的EntityEntry EntityEntry entry = (EntityEntry) me.getValue(); Status status = entry.status; //判断实体当前状态 if (status!=LOADING && status!=GONE) //取出Entry.Key中保存的当前实体对象,连同EntityEntry //交由flushEntity方法进行刷新处理 flushEntity( me.getKey(), entry ); } } flushEntity 方法的工作相对琐碎,首先它会检查当前实体对象的 id 是否发生了变动,如果 id 改 变,即判定为异常(即当前实体对象与 EntityEntry 对应关系非法) 。 随即调用 Interceptor(如果有的话)并执行相关的拦截方法。 之后再结合 TypeFactory,Type 等辅助类,将当前实体对象的属性与 EntityEntry 中的原始实体状 态进行比对,判断是否发生了变化,如果发生了变化,是否需要执行数据库更新。 如果以上条件都满足的话,则向当前的更新任务队列中添加一个新的更新任务 (ScheduledUpdate) 。 此更新任务队列将在 Session.flush 方法中稍后的 execute 过程被翻译成对应的 update sql 交由数据 库执行。 之后,Transaction 调用当前 Session 所对应的 JDBC Connection 的 commit 方法,将当前事务提交。 至此,user 对象的更新过程完成。 其间的过程,以 UML 序列图表示大致如图 5-3 所示。 unsaved-value 数据保存时,Hibernate 将根据这个值来判断对象是否需要保存。 所谓显式保存,是指代码中明确调用 session 的 save、update、saveOrupdate 方法对对象进行持久 化。如: session.save(user);
  15. 15. 图 5-3 User 对象更新过程的 UML 序列图 而在某些情况下,如映射关系中,Hibernate 根据级联(Cascade)关系对联接类进行保存。此时 代码中没有针对级联对象的显示保存语句,需要 Hibernate 根据对象当前状态判断是否需要保存到数 据库。此时,Hibernate 即将根据 unsaved-value 进行判定。 首先 Hibernate 会取出目标对象的 id。 之后,将此值与 unsaved-value 进行比对,如果相等,则认为目标对象尚未保存,否则,认为对 象已经保存,无需再进行保存操作。 如:user 对象是之前由 Hibernate 从数据库中获取,同时,此 user 对象的若干个关联对象 address 也被加载,此时我们向 user 对象新增一个 address 对象,此时调用 session.save(user),Hibernate 会根 据 unsaved-value 判断 user 对象的数个 address 关联对象中,哪些需要执行 save 操作,而哪些不需要。 对于我们新加入的 address 对象而言,由于其 id(Integer 型)尚未赋值,因此为 null,与我们设 定的 unsaved-value(null)相同,因此 Hibernate 视其为一个未保存对象,将为其生成 insert 语句并执 行。 这里可能会产生一个疑问,如果“原有”关联对象发生变动(如 user 的某个“原有”的 address 对象的属性发生了变化,所谓“原有”即此 address 对象已经与 user 相关联,而不是我们在此过程中 为之新增的),此时 id 值是从数据库中读出的,并没有发生改变,自然与 unsaved-value(null)也不 一样,那么 Hibernate 是不是就不进行保存操作? 上面关于 PO、 的讨论中曾经涉及到数据保存的问题, VO 实际上,这里的“保存” 实际上是 , “insert” 的概念,只是针对新关联对象的加入,而非数据库中原有关联对象的“update”。所谓新关联对象,一 般情况下可以理解为未与 Session 发生关联的 VO。而“原有”关联对象,则是 PO。如上面关于 PO、
  16. 16. VO 的讨论中所述: 对于save操作而言,如果对象已经与Session相关联(即已经被加入Session的实体容器 中),则无需进行具体的操作。因为之后的Session.flush过程中,Hibernate会对此实体容器 中的对象进行遍历,查找出发生变化的实体,生成并执行相应的update语句。 5.1.3 数据缓存 数据缓存概述 在特定硬件基础上(假设系统不存在设计上的缺漏和糟糕低效的 SQL 语句)缓存(Cache)往往 是提升系统性能的关键因素。 而对于 ORM 实现而言,缓存则显得尤其重要,它是持久层性能提升的关键。相对 JDBC 数据存 取,ORM 实现往往需要借助更加复杂的机制,以实现内部状态的管理、OR 关系的映射等。 这些额外的开销使得 ORM 数据访问效率相对降低。如何弥补这里产生的性能差距?数据缓存是 其中一个关键策略。 缓存是数据库数据在内存中的临时容器,它包含了库表数据在内存中的临时拷贝,位于数据库与 数据访问层之间(图 5-4)。 Data Access Layer Data Cache Database 图 5-4 缓存 ORM 在进行数据读取时,会根据其缓存管理策略,首先在缓存中查询,如果在缓存中发现所需 数据(缓存命中),则直接以此数据作为查询结果加以利用,从而避免了数据库调用的性能开销。 相对内存操作而言,数据库调用是一个代价高昂的过程,对于典型企业级应用结构,数据库往往 与应用服务器位于不同的物理服务器,这也就意味着每次数据库访问都是一次远程调用,Socket 的创 建与销毁,数据的打包拆包,数据库执行查询指令、网络传输上的延时,这些消耗都给系统整体性能 造成了严重影响。 此时,本地内存中数据缓存的存在价值就显得特别突出。特别是对于查询操作相对频繁 (read-mostly)的系统而言(如论坛系统,新闻发布系统等),良好的缓存管理机制以及合理的缓存 应用模式往往是性能提升的关键。 下面,我们就将围绕数据缓存的一般实施策略,及其应用模式进行探讨。 数据缓存策略 持久层设计中,往往需要考虑到几个不同层次中的数据缓存策略。这些层次的划分标准针对不同
  17. 17. 的情况有所差异,一般而言,ORM 的数据缓存应包含如下几个层次: 1. 事务级缓存(Transaction Layer Cache) 2. 应用级/进程级缓存(Application/Process Layer Cache) 3. 分布式缓存(Cluster Layer Cache) 事务级缓存 在当前事务范围内的数据缓存策略。 这里的事务可能是一个数据库事务,也可能是某个应用级事务。对于 Hibernate 而言,事务级缓 存是基于 Session 生命周期实现的,每个 Session 会在内部维持一个数据缓存,此缓存随着 Session 的 创建(销毁)而存在(消亡),因此也称为 Session Level Cache(也称为内部缓存) 。 应用级缓存 在某个应用中,或者应用中某个独立数据访问子集中的共享缓存。 此缓存可由多个事务(数据库事务或者应用级事务)共享。事务之间的缓存共享策略与应用的事 务隔离机制密切相关。 Hibernate 中, 在 应用级缓存在 SessionFactory 层实现,所有由此 SessionFactory 创建的 Session 实例共享此缓存,因此也称为 SessionFactory Level Cache。 多实例并发运行的环境(如多机负载均衡环境中)中,我们必须特别小心缓存机制可能带来的负 面效应。 假设实例 A 和实例 B 共享同一数据库,并行运行,A 和 B 各自维持自己的缓存,如果缺乏同步 机制,A 在某个操作中对数据库进行了更新,而 B 并没有获得相应的更新通知,其缓存中的数据还是 数据库修改之前的版本,那么 B 在之后的读取操作中,可能就以此过期数据作为数据源,从而导致数 据同步错误,这样的错误对于关键业务数据而言是无法承受的(如账务系统) 。 在这种情况下,应用级缓存无法使用。为了解决这个问题,我们引入了分布式缓存。 分布式缓存 在多个应用实例,多个 JVM 之间共享的缓存模式。 分布式缓存由多个应用级缓存实例组成集群,通过某种远程机制(如 RMI 或 JMS)实现各个缓 存实例间的数据同步,任何一个实例的数据修改操作,将导致整个集群间的数据状态同步。 分布式缓存解决了多实例并发运行过程中的数据同步问题。 但是,除非对于并发读取性能要求较高,且读取操作在持久层操作中占绝大部分比重的情况,分 布式缓存的实际效果尚需考证。 由于多个实例间的数据同步机制,每个缓存实例发生的变动都会复制到其余所有节点中(对于 Repplication 式缓存而言),这样的远程同步开销不可忽视。 笔者曾经主持构建的一个大型金融业务系统中,在模拟测试阶段发现,分布式缓存的频繁同步甚 至导致了网络中的数据阻塞。 考虑到主流企业级数据库均已经具备了数据库级的缓存机制,此时,分布式缓存的性能优势在一 些情况下并不明显,并且还可能引入其他的问题,因此,分布式缓存的使用还有待商榷。当我们决定 在系统中引入分布式缓存前,必须经过仔细的压力测试和性能分析,以免出现不必要的尴尬。 另外,需要再次强调的是,如果当前应用与其他应用共享数据库,也就是说,在当前应用运行过 程中,其他应用可能同时更新数据库,那么缓存策略的制定就需要格外小心。这种情况下,采取一些
  18. 18. 保守策略(避免缓存机制的使用)可能更加稳妥。 5.1.4 Hibernate 数据缓存 Hibernate 数据缓存(Cache)分为两个层次,以 Hibernate 语义加以区分,可分为: 1. 内部缓存(Session Level,也称为一级缓存) 2. 二级缓存(SessionFactory Level,也称为二级缓存) Hibernate 中,缓存将在以下情况中发挥作用: 1. 通过 id [主键] 加载数据时 这包括了根据 id 查询数据的 Session.load 方法,以及 Session.iterate 等批量查询方法 (Session.iterate 进行查询时,也是根据 id 在缓存中查找数据,类似一个 Session.load 循环,具体请参见“持久化 操作”部分的讨论)。 2. 延迟加载 缓存的应用是一个非常复杂的论题,在下面的内容中,我们将主要围绕 Hibernate 中数据缓存的 概念及其运行机制进行讨论。而缓存的具体使用,以及结合缓存的数据访问策略和技巧, “持 将在 久化操作”部分结合对应的数据访问方法进行探讨。 内部缓存 内部缓存在 Hibernate 中又称为一级缓存,属于应用事务级缓存。 在之前的“脏数据检查”部分的讨论中,实际上我们已经涉及了内部缓存的实现原理。 Session 在内部维护了一个 Map 数据类型,此数据类型中保持了所有的与当前 Session 相关联的数 据对象,如果观察 SessionImpl 类源码,我们可以看到: private final Map entitiesByKey; //key=Key, value=Object private final Map proxiesByKey; //key=Key, value=HibernateProxy private transient Map entityEntries; //key=Object, value=Entry private transient Map arrayHolders; //key=array, value=ArrayHolder private transient Map collectionEntries; //key=PersistentCollection, value=CollectionEntry private final Map collectionsByKey; //key=CollectionKey, value=PersistentCollection 这些 Map 数据结构中维护了当前 Session 中所有相关 PO 的状态。 如果我们需要通过 Session 加载某个数据对象,Session 首先会根据所要加载的数据类和 id,在 entitiesByKey 中寻找是否已有此数据的缓存实例,如果存在且其状态判定为有效,则以此数据实例作 为结果返回。 同样,如果 Session 从数据库中加载了数据,也会将其纳入此 Map 结构加以管理。 这也就是内部缓存的实现,非常简单。另外,根据代码可以看出,这些 Map 数据结构为 Session 的私有数据,伴随 Session 实例的创建而创建,消亡而消亡。因此,有时也称此缓存为 Session Level Cache。 内部缓存正常情况下由 Hibernate 自动维护,如果需要手动干预,我们可以通过以下方法完成: 1. Session.evict
  19. 19. 将某个特定对象从内部缓存中清除。 2. Session.clear 清空内部缓存。 二级缓存 在 Hibernate 中,二级缓存涵盖了应用级缓存和分布式缓存领域。 二级缓存将由从属于本 SessionFactory 的所有 Session 实例共享,因此有时称为 SessionFactory Leve Cache。 Session 在进行数据查询操作时,会首先在自身内部的一级缓存中进行查找,如果一级缓存未能 命中,则将在二级缓存中查询,如果二级缓存命中,则以此数据作为结果返回。 在引入二级缓存时,我们首先必须考虑以下问题: 1. 数据库是否与其他应用共享 2. 应用是否需要部署在集群环境中 对于第一种情况,往往也就意味着我们不得不放弃二级缓存的使用(我们也可以对数据库的共享 情况进行细化,比如某个表由本应用独占,那么也可以对此表引用二级缓存机制) 。 对于第二种情况,我们就必须考虑是否需要引入分布式缓存机制,以及引入分布式缓存带来的实 际性能变化。 其次,我们应该对哪些数据应用二级缓存? 显然,对数据库中所有的数据都实施缓存是最简单的方法,大多数情况下,这可能也是实际开发 中最常采用的模式(节省了开发人员的大量脑细胞☺)。 但是在某些情况下,这样的方式反而会对性能造成影响,如对于以下情况:一个电信话务系统, 客户可以通过这套系统查询自己的历史通话记录。 这个案例中,对于每个客户,库表中可能都有成千上万条数据,而不同客户之间,基本不可能共 享数据(客户只能查询自身的通话记录),如果对此表施以缓存管理,那么可以想象,内存会迅速被 几乎不可能再被重用的数据充斥,系统性能急剧下降。 因此,在考虑缓存机制应用策略的时候,我们必须对当前系统的数据逻辑进行考察,以确定最佳 的解决方案。 如果数据满足以下条件,则可将其纳入缓存管理。 1. 数据不会被第三方应用修改 2. 数据大小(Data Size)在可接受的范围之内 3. 数据更新频率较低 4. 同一数据可能会被系统频繁引用 5. 非关键数据(关键数据,如金融账户数据) Hibernate 本身并未提供二级缓存的产品化实现(只是提供了一个基于 Hashtable 的简单缓存以供 调试),而是为众多的第三方缓存组件提供了接入接口,我们可以根据实际情况选择不同的缓存实现 版本,具体请参见稍后的“第三方缓存实现”部分内容的描述。 第三方缓存实现
  20. 20. 基于 Java 的缓存实现,最简单的方式莫过于对集合类数据类型进行封装。Hibernate 提供了基于 Hashtable 的缓存实现机制,不过,由于其性能和功能上的局限,仅供开发调试中使用。 同时,Hibernate 还提供了面向第三方缓存实现的接口,如: 1. JCS 2. EHCache 3. OSCache 4. JBoss Cache 5. SwarmCache Hibernate 早期版本中采用了 JCS Java Caching System ——Apache Turbine 项目中的一个子项目) ( 作为默认的二级缓存实现。由于 JCS 的发展停顿2,以及其内在的一些问题(在某些情况下,可能导 致内存泄漏以及死锁),新版 Hibernate 已经将 JCS 去除,并以 EHCache 作为其默认的二级 Cache 实 现。 相对 JCS 而言,EHCache 更加稳定,并具备更好的缓存调度性能,其缺陷是目前还无法做到分布 式缓存。 如果我们的系统需要在多台设备上部署,并共享同一个数据库(典型的,如多机负载均衡),则 必须使用支持分布式缓存的 Cache 实现(如 JBossCache)以避免出现不同系统实例之间缓存不一致, 而导致数据同步错误的情况。 Hibernate 对缓存进行了良好封装,透明化的缓存机制使得我们在上层结构的实现中无需面对繁琐 的缓存维护细节。 目前 Hibernate 支持的缓存实现在表 5-1 中列出,注意 Hibernate3 中 provider_class 包名需要修改 为 org.hibernate.cache。 表 5-1 Hibernate 支持的缓存实现 名 称 provider_class 分布式支持 查询缓冲 HashTable net.sf.hibernate.cache.HashtableCacheProvider N Y EHCache net.sf.ehcache.hibernate.Provider N Y OSCache net.sf.hibernate.cache.OSCacheProvider N Y SwarmCache net.sf.hibernate.cache.SwarmCacheProvider Y N JBossCache net.sf.hibernate.cache.TreeCacheProvider Y Y SwarmCache 和 JBossCache 均提供了分布式缓存实现(Cache 集群)。 其中 SwarmCache 提供的是 invalidation 方式的分布式缓存,即当集群中的某个节点更新了缓存中 的数据,即通知集群中的其他节点将此数据废除,之后各个节点需要用到这个数据的时候,会重新从 数据库中读入并填充到缓存中。 而 JBossCache 提供的是 Repplication 式的缓存,即如果集群中某个节点的数据发生改变,此节点 会将发生改变的数据的最新版本复制到集群中的每个节点中以保持所有节点状态一致。 Hibernate 中启用二级缓存,需要在 hibernate.cfg.xml 中配置以下参数(以 EHCache 为例) : <hibernate-configuration> <session-factory> 2 本书截稿之前,Apache组织将JCS项目提升到主项目层次,相信不久之后会有一个更加成熟可靠的版本出现。
  21. 21. …… <property name="hibernate.cache.provider_class"> net.sf.ehcache.hibernate.Provider </property> …… </session-factory> </hibernate-configuration> 另外还需要针对 Cache 实现本身进行配置,下面是一个 EHCache 配置文件示例:ehcache.xml: <ehcache> <diskStore path="java.io.tmpdir"/> <defaultCache maxElementsInMemory="10000" //Cache中最大允许保存的数据对象数量 eternal="false" //Cache中数据是否为常量 timeToIdleSeconds="120" //缓存数据钝化时间 timeToLiveSeconds="120" //缓存数据的生存时间 overflowToDisk="true" //内存不足时,是否启用磁盘缓存 /> </ehcache> (其中“//”开始的注释是笔者追加,实际配置文件中不应出现) 之后,需要在我们的映射文件中指定各个映射实体(以及 collection)的缓存同步策略: <class name=" org.hibernate.sample.TUser" .... > <cache usage="read-write"/> .... <set name="addresses" .... > <cache usage="read-only"/> .... </set> </class> 缓存同步策略可应用于实体类和集合属性。 下面,我们继续围绕缓存同步策略进行探讨。 缓存同步策略 缓存同步策略决定了数据对象在缓存中的存取规则。 为了使得缓存调度遵循正确的应用级事务隔离机制,我们必须为每个实体类指定相应的缓存同步 策略。 Hibernate 提供以下 4 种内置的缓存同步策略: read-only 只读。对于不会发生改变的数据,可使用只读型缓存。 nonstrict-read-write 如果程序对并发访问下的数据同步要求不是非常严格,且数据更新操作频率较低(几个 小时或者更长时间更新一次),可以采用本选项,获得较好的性能。 read-write 严格可读写缓存。基于时间戳判定机制,实现了“read committed”事务隔离等级3。可 用于对数据同步要求严格的情况,但不支持分布式缓存。这也是实际应用中使用最多的同步 策略。 3 参见稍后“事务管理”部分中对于事务隔离等级的描述。
  22. 22. transactional 事务型缓存,必须运行在 JTA 事务环境中。 在事务型缓存中,缓存的相关操作也被添加到事务之中(此时的缓存,类似一个内存数据库), 如果由于某种原因导致事务失败,我们可以连同缓冲池中的数据一同回滚到事务开始之前的状态。 事务型缓存实现了“Repeatable read”事务隔离等级,有效保障了数据的合法性,适用于对关键 数据的缓存。 注意:目前 Hibernate 内置的 Cache 中,只有 JBossCache 支持事务性的 Cache 实现。 不同的缓存实现,可支持的缓存同步策略也各不相同(表 5-2)。 表 5-2 不同缓存实现所对应的同步策略 名 称 read-only read-write nonstrict-read-write transactional HashTable Y Y Y EHCache Y Y Y OSCache Y Y Y SwarmCache Y Y JBossCache Y Y 5.1.5 事务管理 事务管理概述 “事务”是一个逻辑工作单元,它包括一系列的操作。事务包含 4 个基本特性,也就是我们常说 的 ACID,其中包括: 1. Atomic(原子性,这里的“原子”即代表事务中的各个操作不可分割) 事务中包含的操作被看作一个逻辑单元,这个逻辑单元中的操作要么全部成功,要么全部失 败。 如:A 通过某网上银行系统给 B 转账,此时会执行两个数据更新操作,减少 A 的余额,增 加 B 的余额。这两个操作形成一个事务,在此事务内的这两个更新操作必须符合“要么全部成 功,要么全部失败”的事务原子性原则。单纯 A 余额的减少或者 B 余额的增加都会造成账务系 统的混乱。 2. Consistency(一致性) 一致性意味着,只有合法的数据可以被写入数据库,如果数据有任何违例(比如数据与字段 类型不符),则事务应该将其回滚到最初状态。 3. Isolation(隔离性) 事务允许多个用户对同一个数据的并发访问,而不破坏数据的正确性和完整性。同时,并行 事务的修改必须与其他并行事务的修改相互独立。 按照比较严格的隔离逻辑来讲,一个事务看到的数据要么是另外一个事务修改这些事务之前 的状态,要么是第二个事务已经修改完成的数据,但是这个事务不能看到其他事务正在修改的数 据。 针对不同的情况,事务的隔离级别要求也各有差异,下面一节中,我们将具体探讨事务隔离
  23. 23. 等级的相关内容。 4. Durability(持久性) 事务结束后,事务处理的结果必须能够得到固化(保存在可掉电存储器上) 。 数据库事务管理隔离等级 事务隔离指的是,数据库(或其他事务系统)通过某种机制,在并行的多个事务之间进行分隔, 使每个事务在其执行过程中保持独立(如同当前只有此事务单独运行)。 本节内容主要围绕数据库事务隔离等级进行探讨。 Hibernate 中的事务隔离依赖于底层数据库提供的事务隔离机制,因此,对数据库事务隔离机制的 理解在基于 Hibernate 实现的持久层中同样适用。 首先我们来看数据操作过程中可能出现的 3 种不确定情况: 脏读取(Dirty Reads) 一个事务读取了另一个并行事务未提交的数据。 不可重复读取(Non-repeatable Reads) 一个事务再次读取之前曾读取过的数据时,发现该数据已经被另一个已提交的事务修 改。 虚读(Phantom Reads) 一个事务重新执行一个查询,返回一套符合查询条件的记录,但这些记录中包含了因 为其他最近提交的事务而产生的新记录。 为了避免上面 3 种情况的出现。标准 SQL 规范中,定义了如下 4 个事务隔离等级: Read Uncommitted 最低等级的事务隔离,它仅仅保证了读取过程中不会读取到非法数据。这种隔离等级 下,上述 3 种不确定情况均有可能发生。 此事务等级对于大多数逻辑严格的应用系统而言是难以接受的,脏读取的出现将为系 统的并发逻辑带来极大的隐患。 Read Committed 此级别的事务隔离保证了一个事务不会读到另一个并行事务已修改但未提交的数据, 也就是说,此等级的事务级别避免了“脏读取”。 当一个事务运行在这个隔离级别时, 一个 SELECT 查询只能看到查询开始之前提交 的数据,而永远无法看到未提交的数据,或者是在查询执行时其他并行的事务提交做的改 变。 此事务隔离等级是大多数主流数据库的默认事务等级,同时也适用于大多数系统。 Repeatable Read 此级别的事务隔离避免了“脏读取”和“不可重复读取”现象的出现。这也意味着,一 个事务不可能更新已经由另一个事务读取但未提交(回滚)的数据。 一般而言,此级事务应用并不广泛,它并不能完全保证数据的合法性(可能出现虚读), 同时也带来了更多的性能损失,如果当前数据库由应用所独享,那么我们可以考虑通过“乐 观锁”达到同样的目的(参见稍后关于“锁”机制的探讨)。
  24. 24. Serializable 最高等级的事务隔离,也提供了最严格的隔离机制。 上面 3 种不确定情况都将被规避。 这个级别将模拟事务的串行执行,逻辑上如同所有事务都处于一个执行队列,依次串行执 行,而非并行执行。 此事务隔离等级在提供了最严密的隔离机制的同时,无疑也带来了高昂的性能开销。 因此使用必须谨慎。生产系统中很少有使用此级事务隔离等级的案例。如果确实需要,我 们可以通过一些其他的策略加以实现(如“悲观锁”机制,参见稍后关于“锁”机制的探 讨)。 这 4 种事务隔离等级总结如下(表 5-3): 表 5-3 事务隔离等级 隔离等级 脏读取 不可重复读取 虚读 Read Uncommitted 可能 可能 可能 Read Committed 不可能 可能 可能 Repeatable Read 不可能 不可能 可能 Serializable 不可能 不可能 不可能 这 4 种事务隔离等级的严密程度由前往后依次递增,同时,其性能也依次下降。因此,无论实际 情况如何,都使用最高级事务隔离的做法并不可取。我们必须根据应用的具体情况进行取舍,以获得 数据合法性与系统性能上的最佳平衡。 Hibernate 事务管理概述 Hibernate 是 JDBC 的轻量级封装,本身并不具备事务管理能力。在事务管理层,Hibernate 将其 委托给底层的 JDBC 或者 JTA,以实现事务的管理和调度。 Hibernate 的默认事务处理机制基于 JDBC Transaction。我们也可以通过配置文件设定采用 JTA 作 为事务管理实现: <hibernate-configuration> <session-factory> …… <property name="hibernate.transaction.factory_class"> net.sf.hibernate.transaction.JTATransactionFactory <!--net.sf.hibernate.transaction.JDBCTransactionFactory--> </property> …… </session-factory> </hibernate-configuration> 基于 JDBC 的事务管理 将事务管理委托给 JDBC 进行处理无疑是最简单的实现方式,Hibernate 对于 JDBC 事务的封装也 非常简单。 我们来看下面这段代码: session = sessionFactory.openSession(); Transaction tx = session.beginTransaction(); …… tx.commit(); 从 JDBC 层面而言,上面的代码实际上对应着:
  25. 25. Connection dbconn = getConnection(); dbconn.setAutoCommit(false); …… dbconn.commit(); 就是这么简单,Hibernate 并没有做更多的事情(实际上也没法做更多的事情),只是将这样的 JDBC 代码进行了封装而已。 这里要注意的是,在 sessionFactory.openSession()中,Hibernate 会初始化数据库连接,与此同时, 将其 AutoCommit 设为关闭状态(false)。而其后,在 Session.beginTransaction 方法中,Hibernate 会 再 次 确 认 Connection 的 AutoCommit 属 性 被 设 为 关 闭 状 态 ( 为 了 防 止 用 户 代 码 对 session 的 Connection.AutoCommit 属性进行修改)。 这也就是说,我们一开始从 SessionFactory 获得的 session,其自动提交属性就已经被关闭 (AutoCommit=false),下面的代码将不会对事务性数据库产生任何效果(非事务性数据库除外,如 Mysql ISAM): session = sessionFactory.openSession(); session.save(user); session.close(); 这实际上相当于 JDBC Connection 的 AutoCommit 属性被设为 false,执行了若干 JDBC 操作之后, 没有调用 commit 操作即将 Connection 关闭。 如果要使代码真正作用到数据库,我们必须显式地调用 Transaction 指令: session = sessionFactory.openSession(); Transaction tx = session.beginTransaction(); session.save(user); tx.commit(); session.close(); 基于 JTA 的事务管理 JTA 提供了跨 Session 的事务管理能力。这一点是与 JDBC Transaction 最大的差异。 JDBC 事务由 Connection 管理,也就是说,事务管理实际上是在 JDBC Connection 中实现。事务 周期限于 Connection 的生命周期之类。同样,对于基于 JDBC Transaction 的 Hibernate 事务管理机制 而言,事务管理在 Session 所依托的 JDBC Connection 中实现,事务周期限于 Session 的生命周期。 JTA 事务管理则由 JTA 容器实现,JTA 容器对当前加入事务的众多 Connection 进行调度,实现 其事务性要求。JTA 的事务周期可横跨多个 JDBC Connection 生命周期。同样对于基于 JTA 事务的 Hibernate 而言,JTA 事务横跨可横跨多个 Session。 图 5-5 形象地说明了这个问题: JDBC Connection Transaction Transaction Start Commit JDBC Transaction JTA Container Transaction Start JDBC Connection 1 JDBC Connection 1 …… JDBC Connection 1 Transaction Commit JTA Transaction
  26. 26. 图 5-5 基于 JDBC 的事务管理与基于 JTA 的事务管理 图中描述的是 JDBC Connection 与事务之间的关系,而 Hibernate Session 在这里与 JDBC Connection 具备同等的逻辑含义。 从图 5-5 中我们可以看出,JTA 事务是由 JTA Container 维护的,事务的生命周期由 JTA Container 维护,而与具体的 Connection 无关。 这里需要注意的是,参与 JTA 事务的 Connection 需避免对事务管理进行干涉。这也就是说,如 果采用 JTA Transaction,我们不应该再调用 Hibernate 的 Transaction 功能。 上面基于 JDBC Transaction 的正确代码,这里就会产生问题: public class ClassA{ public void saveUser(User user){ Session session = sessionFactory.openSession(); Transaction tx = session.beginTransaction(); session.save(user); tx.commit(); session.close(); } } public class ClassB{ public void saveOrder(Order order){ Session session = sessionFactory.openSession(); Transaction tx = session.beginTransaction(); session.save(order); tx.commit(); session.close(); } } public class ClassC{ public void save(){ …… UserTransaction tx = (UserTransaction)( new InitialContext().lookup(“……”) ); ClassA.save(user); ClassB.save(order); tx.commit(); …… } } 这里有两个类 ClassA 和 ClassB,分别提供了两个方法:saveUser 和 saveOrder,用于保存用户信 息和订单信息。在 ClassC 中,我们顺序调用了 ClassA.saveUser 方法和 ClassB.saveOrder 方法,同时 引入了 JTA 中的 UserTransaction 以实现 ClassC.save 方法中的事务性。 问题出现了,ClassA 和 ClassB 中分别都调用了 Hibernate 的 Transaction 功能。 Hibernate 的 JTA 在 封装中,Session.beginTransaction 同样也执行了 InitialContext.lookup 方法获取 UserTransaction 实例, Transaction.commit 方法同样也调用了 UserTransaction.commit 方法。实际上,这就形成了两个嵌套式 的 JTA Transaction:ClassC 声明了一个事务,而在 ClassC 事务周期内,ClassA 和 ClassB 也企图声明 自己的事务,这将导致运行期错误。 因此,如果决定采用 JTA Transaction,应避免再重复调用 Hibernate 的 Transaction 功能,上面 ClassA 和 ClassB 的代码修改如下: public class ClassA{
  27. 27. public void save(TUser user){ Session session = sessionFactory.openSession(); session.save(user); session.close(); } …… } public class ClassB{ public void save (Order order){ Session session = sessionFactory.openSession(); session.save(order); session.close(); } …… } 上面代码中的 ClassC.save 方法,同时修改如下: public class ClassC{ public void save(){ …… Session session = sessionFactory.openSession(); Transaction tx = session.beginTransaction(); classA.save(user); classB.save(order); tx.commit(); …… } } 实际上,这是利用 Hibernate 来完成启动和提交 UserTransaction 的功能,但这样的做法比原本直 接通过 InitialContext 获取 UserTransaction 的做法消耗了更多的资源,得不偿失。 在 EJB 中使用 JTA Transaction 无疑最为简便,我们只需要将 save 方法配置为 JTA 事务支持即可, 无需显式声明任何事务,下面是一个 Session Bean 的 save 方法,它的事务属性被声明为“Required”, EJB 容器将自动维护此方法执行过程中的事务: /** * @ejb.interface-method * view-type="remote" * * @ejb.transaction type = "Required" **/ public void save(){ //EJB环境中,通过部署配置即可实现事务声明,而无需显式调用事务 classA.save(user); classB.save(log); }//方法结束时,如果没有异常发生,则事务由EJB容器自动提交。 锁(locking) 业务逻辑的实现过程中,往往需要保证数据访问的排他性。如在金融系统的日终结算处理中,我 们希望针对某个截止点的数据进行处理,而不希望在结算进行过程中(可能是几秒钟,也可能是几个 小时),数据再发生变化。 此时,我们就需要通过一些机制来保证这些数据在某个操作过程中不会被外界修改,这样的机制, 在这里,也就是所谓的“锁”,即给我们选定的目标数据上锁,使其无法被其他程序修改。 Hibernate 支持两种锁机制:即通常所说的“悲观锁(Pessimistic Locking) 和 ” “乐观锁(Optimistic Locking)”。
  28. 28. 悲观锁(Pessimistic Locking) 悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的 事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现, 往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则, 即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。 一个典型的,依赖数据库实现的悲观锁调用: select * from account where name="Erica " for update 通过 for update 子句,这条 SQL 锁定了 account 表中所有符合检索条件(name=“Erica”)的记录。 本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。 Hibernate 的悲观锁,也是基于数据库的锁机制实现。 下面的代码实现了对查询记录的加锁: String hqlStr = "from TUser as user where user.name='Erica'"; Query query = session.createQuery(hqlStr); query.setLockMode("user",LockMode.UPGRADE); //加锁 List userList = query.list();//执行查询,获取数据 query.setLockMode 对查询语句中,特定别名所对应的记录进行加锁(我们通过“from TUser as user”为 TUser 类指定了一个别名“user”),这里也就是对返回的所有 user 记录进行加锁。 观察运行期 Hibernate 生成的 SQL 语句: select tuser0_.id as id, tuser0_.name as name, tuser0_.group_id as group_id, tuser0_.user_type as user_type, tuser0_.sex as sex from t_user tuser0_ where (tuser0_.name='Erica' ) for update 可以看到 Hibernate 通过使用数据库的 for update 子句实现了悲观锁机制。 Hibernate 的加锁模式有: LockMode.NONE : 无锁机制 LockMode.WRITE :Hibernate 在 Insert 和 Update 记录的时候会自动获取 LockMode.READ : Hibernate 在读取记录的时候会自动获取 以上这 3 种锁机制一般由 Hibernate 内部使用,如 Hibernate 为了保证 Update 过程中对象不会被 外界修改,会在 save 方法实现中自动为目标对象加上 WRITE 锁。这些都是 Hibernate 内部对数据的 锁定机制,与数据库无关。 LockMode.UPGRADE :利用数据库的 for update 子句加锁 LockMode. UPGRADE_NOWAIT :Oracle 的特定实现,利用 Oracle 的 for update nowait 子句实现加锁 上面这两种锁机制是我们在应用层较为常用的,依赖数据库的悲观锁机制。 加锁一般通过以下方法实现: Criteria.setLockMode Query.setLockMode Session.lock 注意,只有在查询开始之前(也就是 Hibernate 生成 SQL 之前)设定加锁,才会真正通过数据库
  29. 29. 的锁机制进行加锁处理,否则,数据已经通过不包含 for update 子句的 Select SQL 加载进来,所谓数 据库加锁也就无从谈起。 乐观锁(Optimistic Locking) 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁 机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事 务而言,这样的开销往往无法承受。 如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如 更改用户账户余额),如果在其全程都采用悲观锁机制,也就意味着整个操作过程中(从操作员读出 数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),数据库记录 始终处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果。 乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实 现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为 数据库表增加一个“version”字段来实现。 读取出数据时,将此版本号一同读出,之后更新时,对此版本号加 1。此时,将提交数据的版本 数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本 号,则予以更新,否则认为是过期数据。 对于上面修改用户账户信息的例子而言,假设数据库中账户信息表中有一个 version 字段,当前 值为 1;而当前账户余额字段(balance)为$100。 1. 操作员 A 此时将其读出(version=1),并从其账户余额中扣除$50($100-$50)。 2. 在操作员 A 操作的过程中,操作员 B 也读入此用户信息(version=1),并从其账户余额中扣 除$20($100-$20)。 3. 操 作 员 A 完 成 了 修 改 工 作 , 将 数 据 版 本 号 加 1 ( version=2 ) , 连 同 账 户 扣 除 后 余 额 (balance=$50),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被 更新,数据库记录 version 更新为 2。 4. 操作员 B 完成了操作,也将版本号加 1(version=2)试图向数据库提交数据(balance=$80), 但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2,数据库记录当前版本也为 2,不满足“提交版本必须大于记录当前版本才能执行更新”的乐观锁策略,因此,操作员 B 的 提交被驳回。这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员 A 的操 作结果的可能。 从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销(操作员 A 和操作员 B 操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系统整体性能表现。 需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上 例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户余额更新操作不受我们系统的控 制,因此可能会造成非法数据被更新到数据库中。 在系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策 略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对 外公开)。 Hibernate 在其数据访问引擎中内置了乐观锁实现。如果不用考虑外部系统对数据库的更新操作, 利用 Hibernate 提供的透明化乐观锁实现,将大大提升我们的生产力。
  30. 30. Hibernate 中可以通过 class 描述符的 optimistic-lock 属性结合 version 描述符指定。 现在,我们为之前示例中的 TUser 加上乐观锁机制。 1. 首先为 TUser 的 class 描述符添加 optimistic-lock 属性: <hibernate-mapping> <class name="org.hibernate.sample.TUser" table="t_user" dynamic-update="true" dynamic-insert="true" optimistic-lock="version" > …… </class> </hibernate-mapping> optimistic-lock 属性有如下可选取值: none 无乐观锁。 version 通过版本机制实现乐观锁。 dirty 通过检查发生变动过的属性实现乐观锁。 all 通过检查所有属性实现乐观锁。 其中通过 version 实现的乐观锁机制是 Hibernate 官方推荐的乐观锁实现,同时也是 Hibernate 中, 目前惟一在实体对象脱离 Session 发生修改的情况下依然有效的锁机制。因此,一般情况下,我们都 选择 version 方式作为 Hibernate 乐观锁实现机制。 2. 添加一个 Version 属性描述符 <hibernate-mapping> <class name="org.hibernate.sample.TUser" table="t_user" dynamic-update="true" dynamic-insert="true" optimistic-lock="version" > <id name="id" column="id" type="java.lang.Integer" > <generator class="native"> </generator> </id> <version column="version" name="version" type="java.lang.Integer" /> …… </class> </hibernate-mapping>
  31. 31. 注意,version 节点必须出现在 ID 节点之后。 这里我们声明了一个 version 属性,用于存放用户的版本信息,保存在 T_User 表的 version 字段 中。 此时如果我们尝试编写一段代码,更新 TUser 表中记录的数据,如: Criteria criteria = session.createCriteria(TUser.class); criteria.add(Expression.eq("name","Erica")); List userList = criteria.list(); TUser user =(TUser)userList.get(0); Transaction tx = session.beginTransaction(); user.setUserType(1); //更新UserType字段 tx.commit(); 每次对 TUser 进行更新的时候,我们可以发现,数据库中的 version 都在递增。 而如果我们尝试在 tx.commit 之前,启动另外一个 Session,对名为 Erica 的用户进行操作,以模 拟并发更新时的情形: Session session= getSession(); Criteria criteria = session.createCriteria(TUser.class); criteria.add(Expression.eq("name","Erica")); Session session2 = getSession(); Criteria criteria2 = session2.createCriteria(TUser.class); criteria2.add(Expression.eq("name","Erica")); List userList = criteria.list(); List userList2 = criteria2.list(); TUser user =(TUser)userList.get(0); TUser user2 =(TUser)userList2.get(0); Transaction tx = session.beginTransaction(); Transaction tx2 = session2.beginTransaction(); user2.setUserType(99); tx2.commit(); user.setUserType(1); tx.commit(); 执行以上代码,代码将在 tx.commit()处抛出 StaleObjectStateException 异常,并指出版本检查失 败,当前事务正在试图提交一个过期数据。通过捕捉这个异常,我们就可以在乐观锁校验失败时进行 相应处理。 5.1.6 持久层操作 在前面“脏数据检查”部分中,我们已经对 Hibernate 数据更新的实现过程进行了部分探讨。 而 Hibernate 持久层操作的内容,则远不止于此。下面,我们就围绕 Hibernate 中常用持久层操作 实现原理进行探讨。了解持久层操作的实现原理,对我们实现高性能的 Hibernate 持久层将别具意义。 数据加载 Session.get/load
  32. 32. Session.load/get 方法均可以根据指定的实体类和 id 从数据库读取记录,并返回与之对应的实体对 象。 其区别在于: 1. 如 果 未 能 发 现 符 合 条 件 的 记 录 , get 方 法 返 回 null , 而 load 方 法 会 抛 出 一 个 ObjectNotFoundException。 2. Load 方法可返回实体的代理类实例,而 get 方法永远直接返回实体类。关于代理的内容请参 见稍后关于“延迟加载”部分内容。 3. load 方法可以充分利用内部缓存和二级缓存中的现有数据,而 get 方法则仅仅在内部缓存中 进行数据查找,如没有发现对应数据,将越过二级缓存,直接调用 SQL 完成数据读取。 首先来看一个最简单的数据加载过程: TUser user = (TUser)session.load(TUser.class,new Integer(1)); 上面的代码,我们根据实体类型(TUser.class),数据 id(1)加载对应的 user 实体。根据实体类 型和数据 id,Hibernate 即可判定需要读取的库表并定位数据记录。 Session 在加载实体对象时,将经过哪些过程? 1. 首先,通过之前的讨论我们知道,Hibernate 中维持了两级缓存。第一级缓存由 Session 实例 维护,其中保持了 Session 当前所有关联实体的数据,也称为内部缓存。而第二级缓存则存 在于 SessionFactory 层次,由当前所有由本 SessionFactory 构造的 Session 实例共享。 出于性能考虑,避免无谓的数据库访问,Session 在调用数据库查询功能之前,会先在缓存 中进行查询。首先在第一级缓存中,通过实体类型和 id 进行查找,如果第一级缓存查找命 中,且数据状态合法,则直接返回。 2. 之后,Session 会在当前“NonExists”记录中进行查找,如果“NonExists”记录中存在同样 的查询条件,则返回 null。 “NonExists”记录了当前 Session 实例在之前所有查询操作中,未能查询到有效数据的查询 条件(相当于一个查询黑名单列表)。如此一来,如果 Session 中一个无效的查询条件重复出 现,即可迅速做出判断,从而获得最佳的性能表现。 3. 对于 load 方法而言,如果内部缓存中未发现有效数据,则查询第二级缓存,如果第二级缓 存命中,则返回。 4. 如在缓存中未发现有效数据,则发起数据库查询操作(Select SQL),如经过查询未发现对应 记录,则将此次查询的信息在“NonExists”中加以记录,并返回 null。 5. 根据映射配置和 Select SQL 得到的 ResultSet,创建对应的数据对象。 6. 将其数据对象纳入当前 Session 实体管理容器(一级缓存) 。 7. 执行 Interceptor.onLoad 方法(如果有对应的 Interceptor)。 8. 将数据对象纳入二级缓存。 9. 如果数据对象实现了 LifeCycle 接口,则调用数据对象的 onLoad 方法。 10. 返回数据对象。 Session.find/iterate 查询性能往往是系统性能表现的一个重要方面。相对数据库更新、删除操作而言,查询机制的优
  33. 33. 劣很大程度上决定了系统的整体性能。 同样,这个领域,往往也存在最大的性能调整空间。对于同样的查询结果,不同的实现机制其性 能差距可能超出大多数人的想象(出现几百倍的性能差距并不奇怪) 。 因此,在开始具体应用开发之前,了解这方面的实现原理和机制是非常必要的,而这,也是我们 下面即将讨论的主题。 Hibernate 2 中,Session 接口提供了以下方法以完成数据的批量查询功能(相对于 Session.load 的 单一数据加载而言): public List find(…); public Iterator iterate(…); Hibernate 查询接口 Query、Criteria 的查询功能,其内部也正是基于这两个方法实现,因此,对 Session.find/iterate 方法的讨论,涵盖了 Hibernate 中数据批量查询的主要领域,值得引起特别的关注。 另外,值得注意的是:Hibernate3 中,上述方法已经从 Session 接口中废除,统一由 Query 接口 提供。find、iterate 分别对应于 Query.list 和 Query.iterate 方法,对应关系如表 5-4 所示。 表 5-4 Hibernate 2 与 Hibernaet 3 中方法的对应关系 Hibernate 2 Hibernate 3 Session.find() session.createQuery().list() Session.iterate() session.createQuery().iterate() 从实现机制而言,这两个版本之间并没有什么差异。 在下面的内容中,为了保持语义一致性,我们以 Hibernate 2 作为基准版本进行描述。 find/iterate方法均可根据指定条件查询并返回符合查询条件的实体对象集。如: //Session.find String hql = "from TUser where age > ?"; List userList = session.find(hql,new Integer(18),Hibernate.INTEGER); int len = userList.size(); for (int i=0;i<len;i++){ TUser user = (TUser)userList.get(i); System.out.println("User Name:"+user.getName()); } 运行上面的代码,得到屏幕输出: Hibernate: select tuser0_.id as id, tuser0_.name as name, tuser0_.age as age, tuser0_.version as version from t_user tuser0_ where (age>? ) User Name:Emma User Name:Sammi 运行下面的代码: //Session.iterate String hql = "from TUser where age > ?"; Iterator it = session.iterate( hql, new Integer(18), Hibernate.INTEGER ); while (it.hasNext()){ TUser user = (TUser)it.next(); System.out.println("User Name:"+user.getName()); }
  34. 34. 得到屏幕输出: Hibernate: select tuser0_.id as x0_0_ from t_user tuser0_ where (age>? ) Hibernate: select tuser0_.id as id0_, tuser0_.name as name0_, tuser0_.age as age0_, tuser0_.version as version0_ from t_user tuser0_ where tuser0_.id=? Hibernate: select tuser0_.id as id0_, tuser0_.name as name0_, tuser0_.age as age0_, tuser0_.version as version0_ from t_user tuser0_ where tuser0_.id=? User Name:Emma User Name:Sammi 可以看到,Session.find/iterate方法实现了相同的功能— 根据查询条件从数据库获取符合条件 — 的记录,并返回对应的实体集。 从表象上来看,这两个方法达到了同样的目的,只是返回的集合类型不同,find方法返回List,iterate 返回Iterator。这两个方法的区别是否仅限于集合操作的方式差异? 对比上面的输出日志,相信大家都会产生一些疑惑,这两个方法调用的SQL并不一致,那么是否 其实现机制上也有所不同? 显然,find方法通过一条Select SQL实现了查询操作,而iterate方法,则执行了3次Select SQL,第 一次获取了所有符合条件的记录的id,之后,再根据各个id从库表中读取对应的记录,这是一个典型 的N+1次查询问题。 对于这里的例子,库表中有两条符合查询提交的记录,就需要执行2+1=3条Select语句。iterate方 法导致的N+1次查询相对list方法的一次查询,无疑性能较为低下。如果符合条件数据有100 000万条, 那么就要执行100000+1条Select SQL,可想是怎样的性能噩梦。 既然如此,为何Hibernate还要提供iterator方法,而不是仅仅提供高效的find方法? 这个问题与Hibernate缓存机制密切相关。 尝试运行以下代码: String hql = "from TUser where age > ?"; List userList = session.find(hql,new Integer(18),Hibernate.INTEGER); int len = userList.size(); for (int i=0;i<len;i++){ TUser user = (TUser)userList.get(i); System.out.println("User Name:"+user.getName()); } System.out.println("nStart query by iterate ……n"); Iterator it = session.iterate(hql,new Integer(18),Hibernate.INTEGER); while (it.hasNext()){ TUser user = (TUser)it.next(); System.out.println("User Name:"+user.getName()); } 可以看到,这段代码实际上是将之前的find和iterate方法联用,根据同一条件进行查询。屏幕输出 如下: Hibernate: select tuser0_.id as id, tuser0_.name as name, tuser0_.age as age, tuser0_.version as version from t_user tuser0_ where (age>? ) User Name:Emma User Name:Sammi Start query by iterate…… Hibernate: select tuser0_.id as x0_0_ from t_user tuser0_ where (age>? ) User Name:Emma User Name:Sammi 注意“Start query by iterate…”之后的输出,这部分是由iterate方法执行所引发的操作日志。
  35. 35. 这里,Hibernate只执行了一次SQL,即从库表中取出所有满足条件的记录id。前面iterate方法运行 过程中根据id查询记录的两条SQL语句并没有执行。 这其中的差异就在于Hibernate缓存机制。 find方法将执行Select SQL从数据库中获得所有符合条件的记录并构造相应的实体对象,实体对象 构建完毕之后,就将其纳入缓存。 这样,之后iterate方法执行时,它首先执行一条Select SQL以获得所有符合查询条件的数据id,随 即,iterate方法首先在本地缓存中根据id查找对应的实体对象是否存在(类似Session.load方法),如果缓 存中已经存在对应的数据,则直接以此数据对象作为查询结果,如果没找到,再执行相应的Select语 句获得对应的库表记录(iterate方法如果执行了数据库读取操作并构建了完整的数据对象,也会将其 查询结果纳入缓存)。 find方法将读取的数据纳入缓存,为之后的iterate方法提供了现成的可用数据,于是出现了上面这 种情况。 再执行以下代码: String hql = "from TUser where age > ?"; List userList = session.find(hql,new Integer(18),Hibernate.INTEGER); int len = userList.size(); for (int i=0;i<len;i++){ TUser user = (TUser)userList.get(i); System.out.println("User Name:"+user.getName()); } System.out.println("nStart 2nd list……n"); userList = session.find(hql,new Integer(18),Hibernate.INTEGER); len = userList.size(); for (int i=0;i<len;i++){ TUser user = (TUser)userList.get(i); System.out.println("User Name:"+user.getName()); } 观察日志: Hibernate: select tuser0_.id as id, tuser0_.name as name, tuser0_.age as age, tuser0_.version as version from t_user tuser0_ where (age>? ) User Name:Emma User Name:Sammi Start 2nd list…… Hibernate: select tuser0_.id as id, tuser0_.name as name, tuser0_.age as age, tuser0_.version as version from t_user tuser0_ where (age>? ) User Name:Emma User Name:Sammi 两次 find 方法的重复执行并没有减少 SQL 的执行数量,这里缓存机制似乎并没有产生效果。 道理很简单,我们进行 find 数据查询时,即使缓存中已经有一些符合条件的实体对象存在,我们 也无法保证这些数据就是库表中所有符合条件的数据。假设第一次查询条件是 age>25,随即缓存中就 包括了所有 age>25 的 user 数据;第二次查询条件为 age>20,此时缓存中虽然包含了满足 age>25 的 数据,但这些并不是满足条件 age>20 的全部数据。 因此,find 方法还是需要执行一次 Select SQL 以保证查询结果的完整性(iterate 方法通过首先查 询获取所有符合条件记录的 id,以此保证查询结果的完整性) 。 因此,find 方法实际上无法利用缓存,它对缓存只写不读。而 iterate 方法则可以充分发挥缓存带
  36. 36. 来的优势,如果目标数据只读或者读取相对较为频繁,通过这种机制可以大大减少性能上的损耗。 这是基于充分利用缓存以提升性能上的考量。 同时,另外一方面,还有内存使用上的考虑。 假设我们需要对海量数据进行操作,那么,find 方法将一次获得所有的记录并将其读入内存。假 设有 10 万条符合查询条件的记录,那么,这 10 万条数据会被一次性读入,无疑这将带来极大的内存 消耗,此时很可能会触发 OutOfMemoryError,从而导致系统异常。 此时,解决方案之一就是结合 iterate 方法和 evict 方法逐条对记录进行处理,将内存消耗保持在 可以接受的范围之内,如: String hql = "from TUser where age > ?"; Iterator it = session.iterate(hql,new Integer(18),Hibernate.INTEGER); while (it.hasNext()){ TUser user = (TUser)it.next(); //将对象从一级缓存中移除 session.evict(user); //二级缓存可以设定最大数据缓存数量,达到峰值时会自动对缓存中的较老数据 //进行废除,但是我们这里还是通过编码指定将对象从二级缓存中移除,这有助 //保持缓存的数据有效性 sessionFactory.evict( TUser.class, user.getId()); System.out.println("User Name:"+user.getName()); } 注意上面代码中的下画线部分,我们通过 Session/SessionFactory.evict 方法将数据对象强制从缓存 中移除,如果遗漏了这步操作,那么 user 对象实际上还是会被放入缓存中,那么当循环结束时,所有 符合条件的记录依然会充斥着缓存,这与 find 方法导致的结果相同。通过不断的读取,不断的释放, 我们就可以将可用内存数量维持在比较稳定的范围之内。 实际应用开发中,上面的方案也只能解决部分问题,由于 JVM 的异步内存回收机制,无效对象 会不断在内存中积累等待回收,如果数据量较大,必然频繁激发 JVM 的内存回收机制,导致系统性 能急剧下降。因此,实际开发中,对于大批量数据处理,还是推荐采用 SQL 或存储过程实现,以获 得较高的性能,并保证系统平滑运行。 Query Cache 前面讨论 Session.find 方法时,曾经有这样的分析: 我们进行 find 数据查询时,即使缓存中已经有一些符合条件的实体对象存在,我们也无法保证这 些数据就是库表中所有符合条件的数据。假设第一次查询条件是 age>25,随即缓存中就包括了所有 age>25 的 user 数据;第二次查询条件为 age>20,此时缓存中虽然包含了满足 age>25 的数据,但这些 并不是满足条件 age>20 的全部数据。 是的,对于这样的情况,我们不得不发起一次 Select SQL 以保证获取所有符合条件的记录。 但是,如果之前曾经有完全相同的查询条件出现,如已经发生过 age>20 的查询,那么第二次发 起 age>20 的查询时,我们是否可以利用前一个查询所产生的缓存数据? Query Cache 正是为了解决这个问题而诞生的。 Query Cache 中保存了之前查询操作执行过的 Select SQL,以及由此查询产生的查询结果集(包 括查询对象的类型和 id) 。 之后发生查询请求的时候,Hibernate 会首先根据查询的 SQL 从 Query Cache 中检索,如果此 SQL 曾经执行过,则取出对应这个 SQL 的检索结果集,再根据这个结果集中的对象类型及其 id,从缓存

×