9.4 批量处理数据
通常,在一个Session对象的缓存中只存放数量有限的持久化对象,等到Session对象处理事务完毕,还要关闭Session对象,从而及时释放Session的缓存占用的内存。
批量处理数据是指在一个事务中处理大量数据。以下程序在一个事务中批量更新CUSTOMERS表中年龄大于零的所有记录的AGE字段:
Transaction tx = session.beginTransaction();
Iterator customers=
session.createQuery("from Customer c where c.age>0").list().iterator();
while(customers.hasNext()){
Customer customer=(Customer)customers.next();
customer.setAge(customer.getAge()+1);
}
tx.commit();
session.close();
如果CUSTOMERS表中有1万条年龄大于零的记录,那么Hibernate会一下子加载1万个Customer对象到内存。当执行tx.commit()方法时,会清理缓存,Hibernate执行1万条更新CUSTOMERS表的update语句:
update CUSTOMERS set AGE=? …. where ID=i;
update CUSTOMERS set AGE=? …. where ID=j;
……
update CUSTOMERS set AGE=? …. where ID=k;
以上批量更新方式有两个缺点:
占用大量内存,必须把1万个Customer对象先加载到内存,然后一一更新它们。
执行的update语句的数目太多,每个update语句只能更新一个Customer对象,必须通过1万条update语句才能更新1万个Customer对象,频繁地访问数据库,会大大降低应用的性能。
一般说来,应该尽可能避免在应用层进行批量操作,而应该在数据库层直接进行批量操作,例如直接在数据库中执行用于批量更新或删除的SQL语句,如果批量操作的逻辑比较复杂,则可以通过直接在数据库中运行的存储过程来完成批量操作。
并不是所有的数据库系统都支持存储过程。例如目前的MySQL就不支持存储过程,因此不能通过存储过程来进行批量更新或批量删除。
当然,在应用层也可以进行批量操作,主要有以下方式:
(1)通过Session来进行批量操作。
(2)通过StatelessSession来进行批量操作。
(3)通过HQL来进行批量操作。
(4)直接通过JDBC API来进行批量操作。
9.4.1 通过Session来进行批量操作
Session的save()以及update()方法都会把处理的对象存放在自己的缓存中。如果通过一个Session对象来处理大量持久化对象,应该及时从缓存中清空已经处理完毕并且不会再访问的对象。具体的做法是在处理完一个对象或小批量对象后,立刻调用flush()方法清理缓存,然后再调用clear()方法清空缓存。
通过Session来进行批量操作会受到以下约束:
(1)需要在Hibernate的配置文件中设置JDBC单次批量处理的数目,合理的取值通常为10到50之间,例如:
hibernate.jdbc.batch_size=20
在按照本节介绍的方法进行批量操作时,应该保证每次向数据库发送的批量SQL语句数目与这个batch_size属性一致。
(2)如果对象采用"identity"标识符生成器,则Hibernate无法在JDBC层进行批量插入操作。
(3)进行批量操作时,建议关闭Hibernate的第二级缓存。本书的姊妹篇《精通Hibernate:高级篇》对第二级缓存做了详细介绍。Session的缓存为Hibernate的第一级缓存,通常它是事务范围内的缓存,也就是说,每个事务都有单独的第一级缓存。SessionFactory的外置缓存为Hibernate的第二级缓存,它是应用范围内的缓存,也就是说,所有事务都共享同一个第二级缓存。在任何情况下,Hibernate的第一级缓存总是可用的。而默认情况下,Hibernate的第二级缓存是关闭的,此外也可以在Hibernate的配置文件中通过如下方式显式关闭第二级缓存:
hibernate.cache.use_second_level_cache=false
1.批量插入数据
以下代码一共向数据库中插入十万条CUSTOMERS记录,单次批量插入20条CUSTOMERS记录:
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
for ( int i=0; i<100000; i++ ) {
Customer customer = new Customer(.....);
session.save(customer);
if ( i % 20 == 0 ) { //单次批量操作的数目为20
session.flush(); //清理缓存,执行批量插入20条记录的SQL insert语句
session.clear(); //清空缓存中的Customer对象
}
}
tx.commit();
session.close();
在以上程序中,每次执行session.flush()方法,就会向数据库中批量插入20条记录。接下来session.clear()方法把20个刚保存的Customer对象从缓存中清空。
为了保证以上程序顺利运行,需要遵守以下约束。
在Hibernate的配置文件中,应该把hibernate.jdbc.batch_size属性也设为20。
关闭第二级缓存。因为假如使用了第二级缓存,那么所有在第一级缓存(即Session的缓存)中创建的Customer对象还要先复制到第二级缓存中,然后再保存到数据库中,这会导致大量不必要的开销。
Customer对象的标识符生成器不能为"identity"。
2.批量更新数据
进行批量更新时,如果一下子把所有对象到加载到Session的缓存中,然后再在缓存中一一更新它们,显然是不可取的。为了解决这一问题,可以使用可滚动的结果集org.hibernate.ScrollableResults,Query的scroll()方法返回一个ScrollableResults对象。以下代码演示批量更新Customer对象,该代码一开始利用ScrollableResults对象来加载所有的Customer对象:
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
ScrollableResults customers= session.createQuery("from Customer")
.scroll(ScrollMode.FORWARD_ONLY);
int count=0;
while ( customers.next() ) {
Customer customer = (Customer) customers.get(0);
customer.setAge(customer.getAge()+1); //更新Customer对象的age属性
if ( ++count % 20 == 0 ) { //单次批量操作的数目为20
session.flush();//清理缓存,执行批量更新20条记录的SQL update语句
session.clear();//清空缓存中的Customer对象
}
}
tx.commit();
session.close();
在以上代码中,Query的scroll()方法返回的ScrollableResults对象中实际上并不包含任何Customer对象,它仅仅包含了用于在线定位数据库中CUSTOMERS记录的游标。只有当程序遍历访问S