読者です 読者をやめる 読者になる 読者になる

kagamihogeの日記

kagamihogeの日記です。

JPAからフェッチサイズを変更したかった

Java Oracle DB

JPAだけで完結するのはさすがにムリがあったのでこういうタイトルにした。が、JPAプロバイダ固有のAPIのレベルではフェッチサイズを変更する効果を確認できた。

JDBCのsetFetchSize変更時の動きをstatspackで見てみる - kagamihogeの日記ではJDBCを直接使用していたが、このエントリではJPAプロバイダ(EclipseLink, Hibernate)固有のAPIを使用して100万行取得するコードの速度を、フェッチサイズ変更無しと100の時とでどのくらい速度差が生じるかを確認する。

準備

下記のようなテーブルを作成し、そこへ予め100万行追加しておく。また、検証用プログラムを実行するまえに全行取得するSELECTを発行しておく。常にキャッシュへすべての行が乗った状態にしておいた上で、検証用プログラムを実行する。

DROP TABLE million_rows_table PURGE;
CREATE TABLE million_rows_table 
(
  column_pk_int INT NOT NULL 
, column_vchar VARCHAR2(20) 
, CONSTRAINT million_rows_table_pk PRIMARY KEY 
  (
    column_pk_int 
  )
  ENABLE 
);
INSERT INTO million_rows_table(column_pk_int, column_vchar)
  SELECT ROWNUM, dbms_random.string('X', 20)
    FROM 
      (SELECT ROWNUM FROM all_catalog WHERE ROWNUM <= 1000),
      (SELECT ROWNUM FROM all_catalog WHERE ROWNUM <= 1000);
COMMIT;

検証用プログラム

EclipseLink

pom.xmlとpersistence.xmlは、基本的にJPAからJDBCのバッチ更新を使う - kagamihogeの日記で使ったのと同じものなので割愛。

フェッチサイズを変更してSELECTを発行するJavaのプログラム。フェッチサイズ指定無しの測定は、下記コードのコメント部分にあるように、ReadQuery#setFetchSize をコメントアウトして実行する。

まず、EntityManager#unwrapJPAプロバイダであるEclipseLinkのEntityManagerの実体が取得できる。

次に、CursoredStreamがResultSet的にカーソル読み込みがやれるクラスなので、これを使えるようにする。DataReadQuery#useCursoredStreamを呼んでおくと、CursoredStreamが使えるようになる。ついでにReadQuery#setFetchSizeで、JDBCに対するフェッチサイズを指定できる。

あとはCursoredStreamを順繰りに読んでいくだけ……なのだが、適当なタイミングでCursoredStream#clearを呼ばないとOutOfMemoryになる。CursoredStream#clear()javadocを読むと「This should be performed when reading in a large collection of objects in order to preserve memory.」と書いてあり、読み込んだデータはキャッシュしてるから大量のデータ読むときはテキトーにクリアーしてね、とかなんとか書いてある。

public class Main {

    public void go() {
        EntityManagerFactory emf = null;
        EntityManager em = null;
        try {
            emf = Persistence.createEntityManagerFactory("defaultPU");
            em = emf.createEntityManager();

            fetchMillionRows(em);
        } finally {
            em.close();
            emf.close();
        }
    }
    
    private void fetchMillionRows(EntityManager em) {
        JpaEntityManager eclipseLinkem = em.unwrap(JpaEntityManager.class);

        final String sql = "SELECT column_pk_int, column_vchar FROM million_rows_table";
        DataReadQuery q = new DataReadQuery(sql);
        q.useCursoredStream();
        q.setFetchSize(100);//フェッチサイズ指定無しの場合、ここをコメントアウトする。
        
        Session eclipseLinkSession = eclipseLinkem.getActiveSession();
        CursoredStream eclipseLinkCursor = (CursoredStream) eclipseLinkSession.executeQuery(q);
        
        int i=0;
        while (eclipseLinkCursor.hasNext()) {
            Object row = eclipseLinkCursor.next();
            i++;
            if (i % 10000 == 0) {
                eclipseLinkCursor.clear();
            }
        }
        System.out.println(i);

    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        new Main().go();

        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

}
Hibernate

pom.xmlとpersistence.xmlは例によって割愛。

Chapter 15. Batch processingとかを読むと、こういうケースの場合はStatelessSession使うべし、と書いてあるのでソレを使う。なお、下記のコードだとopenStatelessSessionで取得したStatelessSessionのインスタンスをクローズしていない。その理由は、Oracle側からセッションをモニタすると一本しか来ないため。新しいセッションが張られても良さそうなモンであるが、来ないのでStatelessSessionのインスタンスに対してのクローズは行っていない。

Query#setFetchSizeでフェッチサイズを設定したあとはScrollableResultsを順繰りに読んでいく。このとき、ScrollMode.FORWARD_ONLYでインスタンスを作成する。その理由はそうしないとこの環境ではOutOfMemoryになるため。ScrollableResults#previousとかあるので、キャッシュに行データを残し続けるからだと思われるが、今回は不要なのでScrollMode.FORWARD_ONLYにした。

FORWARD_ONLYにすると今まで読んだ行データは読んだ端から破棄されていくので定期的なクリアは不要だし、StatelessSessionなので一次キャッシュの溢れも気にしなくてよい。

public class Main {

    public void go() {
        EntityManagerFactory emf = null;
        EntityManager em = null;
        try {
            emf = Persistence.createEntityManagerFactory("defaultPU");
            em = emf.createEntityManager();

            fetchMillionRows(em);
        } finally {
            em.close();
            emf.close();
        }
    }

    private void fetchMillionRows(EntityManager em) {
        Session s = em.unwrap(Session.class);
        StatelessSession stateLessSession = s.getSessionFactory().openStatelessSession();
        
        final String sql ="SELECT column_pk_int, column_vchar FROM million_rows_table";
        SQLQuery q = stateLessSession.createSQLQuery(sql);
        q.setFetchSize(100);

        ScrollableResults scroll = q.scroll(ScrollMode.FORWARD_ONLY);

        int i = 0;
        while (scroll.next()) {
            Object[] row = scroll.get();
            i++;
        }
        System.out.println(i);
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        new Main().go();

        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

結果

種類 1 2 3
EclipseLink(none) 33329 32437 33688
EclipseLink(100) 7250 7375 7313
Hibernate(100) 7219 7296 7297

とまぁ、そんなわけでフェッチサイズ無しか100かによって5,6倍は速度差が出る結果となりました。