Hibernate深入浅出(九)持久层操作——数据保存&批量操作

数据保存:

1)session.save

session.save方法用于实体对象到数据库的持久化操作。也就是说,session.save方法调用与实体对象所匹配的Insert SQL,将数据插入库表。

结合一个简单实例来进行讨论:

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

示例:

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万个用户数据。那么,对应我们实现了相应的数据批量导入方法:

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内部缓存,如:

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的改进实现:

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的数量:

<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参数对于批量插入操作的实际影响。

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批量删除代码:

Transaction tx = session.beginTransaction();
session.delete(“from TUser”);
tx.commit();

(假设数据库t_user表中有1000条记录)
对于这样的代码,Hibernate会执行以下语句:
Hibernate会首先从数据库查询出所有符合条件的记录,再对此记录进行循环删除,实际上,session.delete(“from TUser”)等价于:

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方法的实现也正是如此,如下:

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批量删除示例:

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代码测试:

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表中的所有记录:

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实际上仍然没有解决缓存同步上的问题,无法保证缓存数据的一致有效性。
看以下示例:

//加载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删除的形式:

//加载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二级缓存,运行以下代码:

//加载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只是提供了面向高性能批量操作的一种实现途径,但无法保证缓存数据的一致有效性,在实际开发中,必须特别注意这一点,在缓存策略的制定上须特别谨慎。
数据的批量更新与批量删除相关知识点基本相同,就不再赘述。
为此牺牲的所谓设计上的优雅性,未必就那么令人惋惜。毕竟对于应用系统的开发而言,为客户提供一个满足需求并且高效稳定的系统才是第一目标,产品最终能得到用户的欢迎,才是真正的优雅。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章