我们在用数据库时,如果可以批量插入或者更新数据,可以很大地提高使用数据库的性能。但是我在用Hibernate实现批处理的过程中,走了一些弯路。下面我就把我的整个配置过程和经验总结分享,希望可以帮助到后来人。
撰写代码
首先你必须要撰写或者修改你的批处理代码。比如你的目标是通过Hibernate一次保存50个Customer对象,那你必须每save满50个对象,就flush一次,如下:
Transaction tx = session.beginTransaction();
for(Customer customer : customers) {
session.save(customer );
if(++i % 50 == 0) {
session.flush();
session.clear();
}
}
tx.commit();
这是因为Hibernate会把新插入的数据都保存在缓存(persistence context cache)中。因为缓存大小的限制,所以隔一定数量就要flush,否则可能会碰上OutMemorException.
修改配置文件
Hibernate官方指导文档建议,为了最好的性能,应该把hibernate.jdbc.batch_size设置成和代码中procedure batch一样的size,在hibernate.cfg.xml中加上:
<property name="hibernate.jdbc.batch_size">50</property>
这里还有一点要注意的是,记得关掉batch操作的二级缓存。否则的话,每个对象被保存时,都会同时保存到二级缓存,这是一个不必要的浪费。
修改标识符生成器(id generator)
上面两个步骤看起来应该大功告成了,其实离成功还得远。首先,Hibernate明确说明了这样一个事实:
Hibernate silently disables JDBC batch inserts if your entity is mapped with an identity identifier generator; many JDBC drivers don’t support batching in that case
也就是说,如果你用的是identity的标识符生成器,那Hibernate的批处理就不灵了,所以我们必须得换一个,下面告诉你如何用MultipleHiLoPerTableGenerator。这个标识符生成器由Hibernate按照HiLo算法来生成id,它从数据库的特定表的字段长获取high值。至于什么是Hilo算法,可以参考这篇文章(https://vladmihalcea.com/2014/06/23/the-hilo-algorithm/)。在你的Customer.hbm.xml里,加上下面的代码:
<id name="id" type="java.lang.Integer">
<column name="id"/>
<generator class="org.hibernate.id.MultipleHiLoPerTableGenerator">
<param name="table">identityGenerator</param>
<param name="primary_key_column">sequence_name</param>
<param name="value_column">next_hi_value</param>
<param name="primary_key_value">TableCustomer</param>
<param name="max_lo">10000</param>
</generator>
</id>
hi值存放在identityGenerator表的next_hi_value字段中:
CREATE TABLE IdentityGenerator (
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
sequence_name VARCHAR(36) NOT NULL,
next_hi_value INTEGER NOT NULL
)ENGINE= InnoDB DEFAULT CHARSET=latin1;
批处理到块处理
到这一步位置,batch操作已经完成了,但是你可能发现性能没有太多的提高,如果你看一眼mysql的log,你会发现数据实际上还是一条条插入数据库的。事实上,我们之前做的事情,只是把50条insert语句打包成了一个batch,发给了数据库,而数据库并没有把这50条insert变成下面这个我们期望的样子:
INSERT INTO Customer () VALUES (), (), (), (), ()
这被称作bulk insert的操作通常依赖于数据库connector provider。Hibernate依赖的mysql-connector-java提供了rewriteBatchedStatements这个配置参数来帮助我们把batch inserts转成bulk inserts。再次修改你的hibernate.cfg.xml如下:
<property name="hibernate.connection.url">jdbc:mysql://local:3306/database?rewriteBatchedStatements=true</property>
查看数据库log
最后,你还可以查看下mysql的log,来确定这会是不是真的bulk insert了
sudo tail -f /var/log/mysql/mysql.log
mysqllog的打开方法如下:
mysql> show global variables like '%general%';
mysql> SET GLOBAL general_log = 'ON'; // 打开
mysql> SET GLOBAL general_log = 'OFF'; // 关闭
一个常见的flush时抛出的异常
如果你跟我一样不幸运,session.flush()时可能会看到以下异常:
org.hibernate.jdbc.BatchedTooManyRowsAffectedException: Batch update returned unexpected row count from update [0]; actual row count: 10; expected: 1
我在stackoverflow上找到了两种解决方法:
- 检查你的mysql-connector-java版本,把mysql-connector-java降级到5.1.24,或者升级到更新的版本(参考:http://stackoverflow.com/questions/23663557/hibernate-mysql-rewritebatchedstatements-true)
在org.hibernate.jdbc这个源码包中,拷贝Exceptations.java这个文件到你的工程里(不要改变包名和相对路径),然后在你拷贝的这个Exceptions.java中,注释掉checkBatched方法中抛出BatchedTooManyRowsAffectedException的相关代码
我是用第二种方法解决的。
以上就是我根据自己的实际经验总结的如果用Hibernate实现一次插入多条数据到数据库。欢迎留言指正讨论。