kagamihogeの日記

kagamihogeの日記です。

JPAからJDBCのバッチ更新を使う

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倍くらいは速度差がついてるんで、バッチ更新の動作を確認できました。