数据保存:
1)session.save
session.save方法用于实体对象到数据库的持久化操作。也就是说,session.save方法调用与实体对象所匹配的Insert SQL,将数据插入库表。
结合一个简单实例来进行讨论:
1
2
3
4
5
|
TUser user = new TUser(); user.setName( "Luna" ); Transaction tx = session.beginTransaction(); session.save(user); tx.commit(); |
首先,我们创建了一个user对象,并启动事务,之后调用session.save方法对对象进行保存。
session.save方法中包含了以下几个主要步骤:
a. 在session内部缓存中寻找待保存对象
内部缓存命中,则认为此数据已经保存(执行过insert操作),实体对象已经处于Persistent状态,直接返回。
此时,即使数据相对之前状态已经发生了变化,也将在稍后的事务提交时,由脏数据检查过程加以判定,并根据判定结果决定是否要执行对应的update操作。
b. 如果实体类实现了Lifecycle接口,则调用待保存对象的onSave方法
c. 如果实体类实现了Validatable接口,则调用其validate方法
d. 调用对应拦截器的Interceptor.onSave方法(如果有的话)
e. 构造Insert SQL,并加以执行
f. 记录插入成功,user.id属性被设定为insert操作返回的新记录id值
g. 将user对象放入内部缓存
这里值得一提的是,save方法并不会把实体对象纳入二级缓存,因为通过save方法保存的实体对象,在事务的剩余部分中被修改几率往往很高,缓存的频繁更新以及随之而来的数据同步问题的代价,已经超过了此数据得到重用的可能收益,得不偿失。
h. 最后,如果存在级联关系,对级联关系进行递归处理。
2)session.update
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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.commt(); |
这里我们通过update方法将一个Detached状态的对象与session重新关联起来,从而使之转变为Persistent状态。
那么update方法中,到底进行了怎样的操作完成这一步骤?
a. 首先,根据待更新实体对象的Key,在当前session的内部缓存中进行查找,如果发现,则认为当前实体对象已经处于Persistent状态,返回。
从这一点我们可以看出,对一个Persistent状态的实体对象调用update语句并不会产生任何作用。
b. 初始化实体对象的状态信息(作为之后脏数据检查的依据),并将其纳入内部缓存。注意这里session.update方法本身并没有发送Update SQL完成数据更新操作,Update SQL将在之后的session.flush方法中执行(Transaction.commit在真正提交数据库事务之前会调用session.flush)。
3)session.saveOrUpdate
幕后原理:
a. 首先在session内部缓存中进行查找,如果发现则直接返回。
b. 执行实体类对应的Interceptor.isUnsaved方法(如果有的话),判断对象是否为未保存状态。
c. 根据unsave-value判断对象是否处于未保存状态。
d. 如果对象未保存(Transient状态),则调用save方法保存对象。
e. 如果对象已保存(Detached状态),调用update方法将对象与session重新关联。
可以看到,saveOrUpdate实际上是save和update方法的组合应用。它本身并没有增加新的功能特性,但为应用层开发提供了一个为相当边界的功能选择。
有了saveOrUpdate方法,处理就相当简单明了,我们无需关心传入的user参数到底是怎样的状态。
数据批量操作:
显然,最简单的方式就是通过迭代调用
session.save/update/saveOrUpdate/delete操作。从逻辑上而言,这样的解决方式并没有什么问题。不过,从性能角度考虑,这样的做法却有待商榷。
1. 数据批量导入
举个简单的例子,我们需要导入10万个用户数据。那么,对应我们实现了相应的数据批量导入方法:
1
2
3
4
5
6
7
8
9
|
public void importUsers() throws HibernateException{ Transaction tx = session.beginTransaction(); for ( int i= 0 ;i< 100000 ;i++){ TUser user = new TUser(); user.setName(“user”+i); session.save(user); } tx.commit(); } |
代码从逻辑上看并没有什么问题。但是运行期可能就会发现,程序运行由于OutOfMemoryError而异常中止。
why?原因在于Hibernate内部缓存的维护机制,每次调用
session.save方法时,当前session都会将此对象纳入自身的内部缓存进行管理。
内部缓存与二级缓存不同,我们可以在二级缓存的配置中指定其最大容量,但内部缓存并没有这样的限制。
随着循环的进行,越来越多的TUser实例被纳入到session内部缓存之中,内存逐渐耗尽,于是产生了OutOfMemoryError。
如何避免这样的问题?
一个解决方案是每隔一段时间清空session内部缓存,如:
1
2
3
4
5
6
7
8
9
10
11
|
Transaction tx = session.beginTransaction(); for ( int i= 0 ;i< 100000 ;i++){ TUser user = new TUser(); user.setName(“user”+i); session.save(user); if (i% 25 == 0 ){ //以每25个数据作为一个处理单元 session.flush(); session.clear(); } } tx.commit(); |
在传统JDBC编程时,对于批量操作,一般用怎样的方式加以优化?
下面的代码是一个典型的基于JDBC的改进实现:
1
2
3
4
5
6
|
PreparedStatement stmt = conn.prepareStatement(“INSERT INTO t_user(name) VALUES(?)”); for ( int i= 0 ;i< 100000 ;i++){ stmt.setString( 1 ,”user”+i); stmt.addBatch(); } int [] counts = stmt.executeBatch(); |
这里我们通过PreparedStatement.executeBacth方法,将数个SQL操作批量提交以获得性能上的提升。
那么Hibernate中是否有对应的批量操作方式呢?
我们可以通过设置hibernate.jdbc.batch_size参数来指定Hibernate每次提交SQL的数量:
1
2
3
4
5
6
7
|
< hibernate-mapping > < session-factory > … < property name=”hibernate.jdbc.batch_size”>25</ property > … </ session-factory > </ hibernate-mapping > |
这样,当我们发起SQL调用的时候,Hibernate会累积到25个SQL之后批量提交,从而实现了与上面JDBC代码类似的效能。
同样的方法,也可以用于Update操作和Delete操作。
下面做个简单的测试,看看hibernate.jdbc.batch_size参数对于批量插入操作的实际影响。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public void importUserList() throws HibernateException{ Transaction tx = session.beginTransaction(); for ( int i= 0 ;i< 100000 ;i++){ TUser user = new TUser(); user.setName(“user”+i); session.save(user); if (i% 25 == 0 ){ //以每25个数据作为一个处理单元 session.flush(); session.clear(); } } tx.commit(); } public void testBatchInsert(){ long startTime = System.currentTimeMillis(); try { this .importUserList(); } catch (HibernateException e){ e.printStackTrace(); } long currentTime = System.currentTimeMillis(); System.out.println(“Batch Insert Time cost in ms => “+(currentTime-startTime)); } |
测试环境:
操作系统:XP sp2
JDK版本:Sun JDK 1.4.2_08
CPU: p4 1.5G Mobile
RAM:512M
数据库:SQLServer2000/Oracle9i
JDBC:jtds JDBC Driver for SQLServer 1.02/Oracle JDBC Driver 9.0.2.0.0
注:Mysql JDBC Driver不支持BatchUpdate方式,因此batch_size的设定对MySQL无效。
对于远程数据库,hibernate.jdbc.batch_size的设定就相当关键。
这里的差距,并不是数据存取机制有什么不同,而是在于网络传输上的损耗,对于数据库与应用均部署在本机的情况而言,数据通讯上的性能损耗较小,因而hibernate.jdbc.batch_size设定的影响相对较弱,而对于远程数据库,网络传输上的损耗就不可不计,因而不同的传输模式(批量传输与单笔传输)将对性能的整体表现产生较大影响。
2. 数据批量删除
批量删除操作在Hibernate2和Hibernate3中有着不同的实现机制,首先来看Hibernate2中的批量删除。
下面是一段典型的Hibernate2批量删除代码:
1
2
3
|
Transaction tx = session.beginTransaction(); session.delete(“from TUser”); tx.commit(); |
(假设数据库t_user表中有1000条记录)
对于这样的代码,Hibernate会执行以下语句:
Hibernate会首先从数据库查询出所有符合条件的记录,再对此记录进行循环删除,实际上,session.delete(“from TUser”)等价于:
1
2
3
4
5
6
7
|
Transaction tx = session.beginTransaction(); List userList = session.find(“from TUser”); int len = userList.size(); for ( int i= 0 ;i<len;i++){ session.delete(userList.get(i)); } tx.commit(); |
实际上,Hibernate内部,Delete方法的实现也正是如此,如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
public int delete(String query, Object[] values, Type[] types) throws HibernateException{ if (log.isTraceEnabled()){ log.trace(“delete: ”+query); if (values.length!= 0 ) log.trace(“parameters: “+ StringHelper.toString(values)); } List list = find(query,values,types); int size = list.size(); for ( int i= 0 ;i<size;i++) delete(list.get(i)); return size; } |
看上去很难以理解的实现方式,为什么Hibernate不单独执行一条Delete SQL”delete t_user where id>5”完成所有的工作呢?
这就是所有ORM框架都必须面对的问题。ORM为了自动维持其内部状态属性,必须知道用户到底对哪些数据进行了操作。它必须首先从数据中获得所有待删除对象,才能根据这些对象,对目前内部缓存和二级缓存中的数据进行整理,以保持内存状态与数据库数据的一致性。
当然,解决办法并不是没有,ORM可以根据调用的Delete SQL对缓存中的数据进行处理,只要是缓存中TUser对象的id值大于5的统统废除,缓存数据废除之后,再执行”delete from t_user where id>5”.但是,如此的需求将导致缓存的管理复杂性大大增加(实际上是实现了一个支持SQL的内存数据库),这样的要求对于一个轻量级的ORM实现而言未免苛刻。
批量删出操作同样会遇到与数据批量导入操作同样的问题:
1) 内存消耗
对于内存消耗问题,无法像之前一样通过session.clear操作解决,因为我们并无法干涉数据的批量加载过程。
变通的方法之一:用session.iterate或者Query.iterate方法逐条获取数据,再执行delete操作。
另外,Hibernate2.16之后的版本提供了基于游标的数据遍历操作,为解决这个问题提供了一个较好的解决方案(前提是所使用的JDBC驱动必须支持游标)。通过游标,我们可以逐条获取数据,从而使得内存处于较为稳定的使用状态。
下面是基于游标的Hibernate批量删除示例:
1
2
3
4
5
6
7
8
9
|
Transaction tx = session.beginTransaction(); String hql = “from TUser”; Query query = session.createQuery(hql); ScrollableResults scRes = query.scroll(); while (scRes.next()){ TUser user = (TUser)scRes.get( 0 ); session.delete(user); } tx.commit(); |
2) 迭代删除操作的执行效率
由于Hibernate批量删除操作过程中,需要反复调用delete SQL,因此同样存在SQL批量发送问题,对于这个问题,我们仍采用调整hibernate.jdbc.batch_size参数解决。
使用JDBC代码测试:
1
2
3
|
String sqlStr = “delete from t_user”; Statement statement = dbconn.createStatement(); statement.execute(sqlStr); |
耗时:390ms。
可以看到,即使是优化过的批量删除功能,性能差距还是相当可观的(近10倍的差距)。因此,在Hibernate2中,对于批量操作而言,适当的时候采用传统的JDBC进行直接的批量数据库操作(此时应特别注意对缓存的影响),可以获得性能上的极大提升,特别是对于批量性能关键的逻辑实现而言。
考虑到以上问题,Hibernate3 HQL语法中引入了bulk delete/update操作,bulk delete/update操作的原理,即通过一条独立的SQL语句完成数据的批量删除/更新操作(类似上例中的JDBC批量删除)。
我们可以通过如下代码删除t_user表中的所有记录:
1
2
3
4
5
6
|
Transaction tx = session.beginTransaction(); String hql = “delete from t_user”; Query query = session.createQuery(hql); int ret = query.executeUpdate(); tx.commit(); System.out.println(“delete records =>”+ret); |
观察运行期日志输出:
可以看到,通过一条干净利落的”delete from t_user”语句,我们即完成数据的批量删除功能,从底层实现来看,这与之前JDBC示例中的实现方式并没有什么不同,性能表现也大致相似。
那么,我们之前曾谈及的批量删除与缓存管理上的矛盾,在Hibernate3中是否仍然存在?
这也正是必须特别注意的一点,Hibernate3的bulk delete/update实际上仍然没有解决缓存同步上的问题,无法保证缓存数据的一致有效性。
看以下示例:
1
2
3
4
5
6
7
8
9
10
|
//加载id=1的用户记录 TUser user = (TUser)session.load(TUser. class , new Integer( 1 )); System.out.println(“User name is ==> “+user.getName()); //删除id=1的用户记录 Transaction tx = session.beginTransaction(); session.delete(user); tx.commit(); //尝试再次加载 user = (TUser)session.load(TUser. class , new Integer( 1 )); System.out.println(“User name is ==> “+user.getName()); |
尝试运行以上代码,在尝试再次加载已删除的TUser对象时,Hibernate将抛出ObjectDeletedException,表明此对象已删除,加载失败。
将以上代码修改为通过bulk delete/update删除的形式:
1
2
3
4
5
6
7
8
9
10
11
12
|
//加载id=1的用户记录 TUser user = (TUser)session.load(TUser. class , new Integer( 1 )); System.out.println(“User name is ==> “+user.getName()); //通过bulk delete/update删除id=1的用户记录 Transaction tx = session.beginTransaction(); String hql = “delete from t_user where id= 1 ”; Query query = session.createQuery(hql); query.executeUpdate(); tx.commit(); //尝试再次加载 user = (TUser)session.load(TUser. class , new Integer( 1 )); System.out.println(“User name is ==> “+user.getName()); |
输出日志如下:
可以看到,第二次加载操作成功,由于缓存同步上的问题,我们得到了一个已经被删除的过期数据对象。
通过前面的讨论,我们知道,Hibernate中维护了两级缓存。
上面的代码中,我们通过同一个session实例反复进行数据加载,第二次查询操作将从内部缓存中直接查找数据返回。
那么,在不同session实例之间的协调情况如何,二级缓存中的数据有效性是否能得到保证?
打开Hibernate二级缓存,运行以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
//加载id=1的用户记录 TUser user = (TUser)session.load(TUser. class , new Integer( 1 )); System.out.println(“User name is ==> “+user.getName()); //加载id=1的用户记录已被放入二级缓存 //通过bulk delete/update删除id=1的用户记录 Transaction tx = session.beginTransaction(); String hql = “delete from t_user where id= 1 ”; Query query = session.createQuery(hql); query.executeUpdate(); tx.commit(); //通过另一个session实例再次尝试加载 user = (TUser)anotherSession.load(TUser. class , new Integer( 1 )); System.out.println(“User name is ==> “+user.getName()); |
在尝试再次加载已删除数据对象时,我们调用了另一个session实例。
运行日志输出如下:
可以看到,与前例相同,第二次数据加载时Hibernate依然返回了无效数据。
也就是说,bulk delete/update只是提供了面向高性能批量操作的一种实现途径,但无法保证缓存数据的一致有效性,在实际开发中,必须特别注意这一点,在缓存策略的制定上须特别谨慎。
数据的批量更新与批量删除相关知识点基本相同,就不再赘述。
为此牺牲的所谓设计上的优雅性,未必就那么令人惋惜。毕竟对于应用系统的开发而言,为客户提供一个满足需求并且高效稳定的系统才是第一目标,产品最终能得到用户的欢迎,才是真正的优雅。