kuniku’s diary

はてなダイアリーから移行(旧 d.hatena.ne.jp/kuniku/)、表示がおかしな箇所はコメントをお願いします。記載されている内容は日付およびバージョンに注意してください。直近1年以上前は古い情報の可能性が高くなります。

Java Persistence API(JPA)で大量データを取得する

ベストっぽい方法を探す

実行環境・ライブラリ

・JavaEE6(JDK7)
・GlashFish v3

JPAの機能で大量レコードを処理するためにページング相当を行うには、

いずれにおいてもメモリを使いすぎてしまうためNGでした。

模索

  • jdbcのResultSet やiBatisのRowHandlerなどあるけど、JPA使っている限りJPAを経由する方法を模索する。
メモリを使いすぎる要因
  • JPAの、#setFirstResult()、#setMaxResults() は、指定した位置(オフセット)からの指定した件数は返却してくれるのですが、内部的な動きがよくなく、単にフェッチしたものを読み飛ばしているだけでした。
  • フェッチだけならまだしも、読み飛ばしているだけで、メモリを使ってしまうのです。
  • レコード数が十万件程度で、-Xmx 512MでOutOfMemmory Error が発生
  • レコード数が20万件程度で30分かかって、-Xmx 768MでOutOfMemmory Error が発生
JPAでの動き
  • 単にフェッチして読み飛ばしている気はしていたが、それがメモリを使いすぎていると気づくまでに時間がかかった。
  • JPAでのSQLのログが、件数指定する箇所がなくWHERE句とORDER BY のSQLになっていて、そのまま実行されていることは気づいていたが・・・。
  • 今回やろうとしたことは、テーブルのデータをCSVに出力するのですが、1000件フェッチしてCSV出力して、またフェッチして・・・を繰り返していたのですが
Query #setFirstResult(0)、 #setMaxResults(1000)
を実行し
次に
Query #setFirstResult(1000)、#setMaxResults(2000)

と動く場合に、

1-1000件、1001件-2000件という取得でなく
1-1000件、1-2000件がJPAでは取得して、JPAが1001件から2000件を切り取って返す

といった動きをするのです。
メモリを使いすぎるだけでなく都度1件目からの処理なので、激遅です。

対処・対応

どう対応したかは、今回はOarcleであったためOracleの関数 row_number を使いました。

	<named-native-query name="Xxx.findRowNumber" result-set-mapping="XxxEntity">
	    <query><![CDATA[
	        Select *
	        from
		        (select row_number() over(order by col1) rn,
		            col2,col3
		        From TableName
		        Where 
		          xxx =?1 
		        Order by col1 ASC)
	        Where rn >= ?2 and rn <= ?3
	]]></query>
	</named-native-query>      

として、?1が検索条件で、?2,?3がoffset,Limitです。

@PersistenceContext
  private EntityManager em ;

・・・

em.createNamedQuery("Xxx.findRowNumber", XxxEntity.class)
                   .setParameter(1, foo).setParameter(2,offset).setParameter(3,offset + max -1).getResultList();

みたいに、?1が検索条件で、?2,?3がoffset,Limitです。
これで、20万件のCSV出力が1分程度になった。

その他の対処方法
  • 大量に取得する際の、テーブルアクセスするkeyとなる情報のみのエンティティクラスを用意して、そのkeyをメモリ上に保持し取得する。
  • keyは、SQLのIn句または、Existsに使う、さすがに1件指定だと、数十万件でも都度DB接続になる場合は遅くなりそう。
  • また、EntityManager#clear しないと、JPAがどんどんメモリ上にキャッシュしてしまう。