JDBCのaddBatchとexecuteBatchの頻度変更時の動きをstatspackで見てみる - kagamihogeの日記は、JDBCを直接使用していた。が、JPAから同じことをするにはどういう設定などをすればよいのか。それをやってみて、ついでに実行速度の違いも計測する。
実際には、正確にはJPAではなくJPAのプロバイダーであるEclipsLink,Hibernateそれぞれに固有の設定をする。とはいえ、結局はそれらの設定もJDBCのバッチ更新をラッピングしているだけ、と思われる。
準備
下記のようなテーブルを作り、そこへ100万行INSERTしていく。検証用プログラムを一回開始するごとに、DROP&CREATEと、テーブル再作成して中身を空にしておく。ついでにバッファキャッシュもクリアしておく。
DROP TABLE INSERT_TO_MILLION_ROWS_TABLE2 PURGE; CREATE TABLE INSERT_TO_MILLION_ROWS_TABLE2 ( ENTITY_ID INTEGER, ENTITY_VALUE VARCHAR(20) ); ALTER SYSTEM FLUSH BUFFER_CACHE;
検証用プログラム
バッチ更新無し
特に何もしないバージョン。速度計測時は、JPAプロバイダはEclipseLinkを使用する。ここのコードはHibernate Reference Documentation 4.3.0.CR2 - Chapter 15. Batch processing - 15.1. Batch insertsを参考にした。
public class Main { public void go() { EntityManagerFactory emf = null; EntityManager em = null; try { emf = Persistence.createEntityManagerFactory("defaultPU"); em = emf.createEntityManager(); insertMillionRows(em); } finally { em.close(); emf.close(); } } private void insertMillionRows(EntityManager em) { EntityTransaction tx = em.getTransaction(); tx.begin(); try { for (int i=1; i<=1_000_000; i++) { InsertToMillionRowsTable2 newRecord = new InsertToMillionRowsTable2( i, RandomStringUtils.randomAlphanumeric(20)); em.persist(newRecord); if (i % 100 == 0) { em.flush(); em.clear(); } } } finally { tx.commit(); } } public static void main(String[] args) { long start = System.currentTimeMillis(); new Main().go(); long end = System.currentTimeMillis(); System.out.println(end - start); } }
エンティティクラス。
@Entity @Table(name = "INSERT_TO_MILLION_ROWS_TABLE2") public class InsertToMillionRowsTable2 implements Serializable { private static final long serialVersionUID = 1L; @Id @Column(name = "ENTITY_ID") private Integer entityId; @Column(name = "ENTITY_VALUE") private String entityValue; public InsertToMillionRowsTable2() { } public InsertToMillionRowsTable2(Integer entityId, String entityValue) { super(); this.entityId = entityId; this.entityValue = entityValue; } public Integer getEntityId() { return this.entityId; } public void setEntityId(Integer entityId) { this.entityId = entityId; } public String getEntityValue() { return this.entityValue; } public void setEntityValue(String entityValue) { this.entityValue = entityValue; } }
EclipseLink
pom.xml
<dependency> <groupId>org.eclipse.persistence</groupId> <artifactId>org.eclipse.persistence.jpa</artifactId> <version>2.5.1</version> </dependency> <dependency> <groupId>org.eclipse.persistence</groupId> <artifactId>javax.persistence</artifactId> <version>2.1.0</version> </dependency>
persistence.xml 追加で設定してるプロパティについては後述。
<?xml version="1.0" encoding="UTF-8"?> <persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"> <persistence-unit name="defaultPU" transaction-type="RESOURCE_LOCAL"> <class>jpawitheclipselink.entity.InsertToMillionRowsTable2</class> <properties> <property name="javax.persistence.jdbc.url" value="jdbc:oracle:thin:@192.168.0.20:1521:XE" /> <property name="javax.persistence.jdbc.user" value="kagamihoge" /> <property name="javax.persistence.jdbc.driver" value="oracle.jdbc.OracleDriver" /> <property name="javax.persistence.jdbc.password" value="xxxx" /> <!-- http://www.eclipse.org/eclipselink/documentation/2.5/jpa/extensions/p_jdbc_batchwriting.htm#CIHIAGAF --> <property name="eclipselink.jdbc.batch-writing" value="jdbc"/> <!-- http://www.eclipse.org/eclipselink/documentation/2.5/jpa/extensions/p_jdbc_batchwritingsize.htm#CIHJADHF --> <property name="eclipselink.jdbc.batch-writing.size" value="100"/> </properties> </persistence-unit> </persistence>
EclipseLinkでバッチ更新するときのプログラム例。必要な部分だけ抜粋。EntityManagerFactory作成時にプロパティを設定するコード以外、バッチ更新無しのコードと同じなので、必要な箇所以外は省略している。
public class Main { public void go() { EntityManagerFactory emf = null; EntityManager em = null; try { emf = Persistence.createEntityManagerFactory("defaultPU", createProps()); em = emf.createEntityManager(); insertMillionRows(em); } finally { em.close(); emf.close(); } } @SuppressWarnings("all") private Map createProps() { Map props = new HashMap(); props.put(PersistenceUnitProperties.BATCH_WRITING, BatchWriting.OracleJDBC); props.put(PersistenceUnitProperties.BATCH_WRITING_SIZE, "100"); return props; }
マニュアル的にはjdbc.batch-writing | EclipseLink 2.5.x Java Persistence API (JPA) Extensions Referenceとかjdbc.batch-writing.size | EclipseLink 2.5.x Java Persistence API (JPA) Extensions Referenceとかのあたりを参照。EclipseLinkでバッチ更新をするには、jdbc_batchwritingを設定し、jdbc_batchwritingsizeを変更したければ任意の値を設定する。
BATCH_WRITINGの設定方法は二種類あり、どちらか好きな方を選ぶ。一つはpersistence.xmlのpersistence-unitごとにプロパティを設定する方法で、もう一つはJavaプログラム中でcreateEntityManagerFactoryするとき引数に設定を渡す方法。上記のプログラム例では、サンプルということで両方ともに設定しているけど、実際には片方だけで良い。現実的には、複数のpersistence-unit作ってバッチとそうでない時で使い分けたりとかすると思われる。なお速度検証は、Javaプログラム中に設定するやり方で行った。
jdbc.batch-writingは、いくつかの種類が選べる。今回は、DBにOracle使ってるのでoracle-jdbcでやってみた。この環境だと、たぶんjdbcでもoracle-jdbcでも変わらないと思われる。他の設定値は試していない。
jdbc.batch-writing.sizeは、100にしておいた。これは、JDBC経由で100万件取得・追加してみた - kagamihogeの日記とかOracle Database JDBC開発者ガイド11gリリース2(11.2)- コーディングのヒント - 標準バッチ更新とOracleバッチ更新とかの経験から、そんなもんでええやろ、という判断をしている。サイズを変えれば実行時間は変化するだろうけど、そこは今回試していない。
Hibernate
pom.xml
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>4.3.0.CR2</version> </dependency>
persistence.xml
<?xml version="1.0" encoding="UTF-8"?> <persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"> <persistence-unit name="defaultPU" transaction-type="RESOURCE_LOCAL"> <class>jpawitheclipselink.entity.InsertToMillionRowsTable2</class> <properties> <property name="javax.persistence.jdbc.url" value="jdbc:oracle:thin:@192.168.0.20:1521:XE" /> <property name="javax.persistence.jdbc.user" value="kagamihoge" /> <property name="javax.persistence.jdbc.driver" value="oracle.jdbc.OracleDriver" /> <property name="javax.persistence.jdbc.password" value="xxxx" /> <!-- http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html/ch15.html --> <property name="hibernate.jdbc.batch_size" value="100" /> </properties> </persistence-unit> </persistence>
Hibernate使った時の例。プロパティ設定部分以外は同じコードなので、省略している。
public void go() { //(省略) emf = Persistence.createEntityManagerFactory("defaultPU", createProps()); //(省略) } @SuppressWarnings("all") private Map createProps() { Map props = new HashMap(); props.put(AvailableSettings.STATEMENT_BATCH_SIZE, "100"); return props; }
マニュアル的にはHibernate Reference Documentation 4.3.0.CR2 Chapter 15. Batch processingあたりを参照。EclipseLinkとは異なり、こちらはバッチサイズだけで良いらしい。マニュアル全部読んだわけではないので確証は無いが。
実行結果
種類 | 1 | 2 | 3 |
---|---|---|---|
EclipseLink(none) | 490891 | 490782 | 487345 |
EclipseLink(OracleJDBC,BATCH_SIZE=100) | 28421 | 27000 | 26828 |
Hibernate(BATCH_SIZE=100) | 28062 | 25437 | 26234 |
というわけで15倍くらいは速度差がついてるんで、バッチ更新の動作を確認できました。
ソースコード
https://github.com/kagamihoge/jpawitheclipselink
https://github.com/kagamihoge/jpawithhibernate
参考文献&URL
EclipseLink
- EclipseLink Documentation Center
- jdbc.batch-writing | EclipseLink 2.5.x Java Persistence API (JPA) Extensions Reference
- jdbc.batch-writing.size | EclipseLink 2.5.x Java Persistence API (JPA) Extensions Reference
- PersistenceUnitProperties (EclipseLink 2.5.1, build 'v20130918-f2b9fc5' API Reference)
- Performance Features | EclipseLink 2.5.x Understanding EclipseLink