数据保存:

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.classnew 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.classnew 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.classnew 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.classnew 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.classnew 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.classnew Integer(1));
System.out.println(“User name is ==> “+user.getName());

在尝试再次加载已删除数据对象时,我们调用了另一个session实例。
运行日志输出如下:
可以看到,与前例相同,第二次数据加载时Hibernate依然返回了无效数据。
也就是说,bulk delete/update只是提供了面向高性能批量操作的一种实现途径,但无法保证缓存数据的一致有效性,在实际开发中,必须特别注意这一点,在缓存策略的制定上须特别谨慎。
数据的批量更新与批量删除相关知识点基本相同,就不再赘述。
为此牺牲的所谓设计上的优雅性,未必就那么令人惋惜。毕竟对于应用系统的开发而言,为客户提供一个满足需求并且高效稳定的系统才是第一目标,产品最终能得到用户的欢迎,才是真正的优雅。

最新文章

  1. 数据导出Excel
  2. 彻底解决Eclipse自动补全变量名及变量名后面追加类型名
  3. [Ubuntu][Linux]更改PATH路径
  4. 浅析JNI函数的注册过程
  5. every day english
  6. 浅谈Androidclient项目框架
  7. [iOS UI进阶 - 6.2] 核心动画CoreAnimation 练习代码
  8. linux的nohup disown setsid screen
  9. 安卓天天练练(十)ListView
  10. objective-C学习笔记(五)函数成员:初始化器和析构器
  11. firebug登陆之数据包分析
  12. SAP HANA procudure 创建用户
  13. IVIEW TREE问题总结
  14. 在Ubuntu 18.04中安装pyenv(Python多版本管理工具)
  15. expect脚本同步文件 expect脚本指定host和要同步的文件 构建文件分发系统 批量远程执行命令
  16. Ubuntu部分快捷键的使用简介
  17. Redis 位操作
  18. ContOs 将SpringBoot的jar制作成系统服务
  19. Spring学习(二)—— java的动态代理机制
  20. Jmeter----读取excel表中的数据

热门文章

  1. Oracle 数据库排错之 ORA-00600
  2. Wireshark使用注意事项
  3. 抒发一下这些天用django做web项目的一些体会
  4. iframe详解
  5. &amp;&amp;和&amp;(||和|)区别
  6. 深入理解IEnumerable和IQueryable两接口的区别
  7. Log4j 使用
  8. python time与datetime.date/datetime模块
  9. 深度强化学习资料(视频+PPT+PDF下载)
  10. splay tree旋转操作 hdu 1890