現在はjbatchな仕事をしていて、DBアクセスにはJPAを採用しています。
JPA自体は、実はあまりbatch向きではないようにも思えますが、
batchなので当然、大量にデータを処理する必要がある場面に出くわします。
大量の度合いにもよりますが、
普通にqueryを発行して結果をEntityで受けて・・・
なんてやっていると、
JPAのEntity管理機構により、発行したqueryの結果の
Entityがメモリに積み上がり、すぐにOutOfMemoryなどで
落ちてしまいます。
さて、どうしましょうか??
通常な使い方だとこんな感じ
@SuppressWarnings("unchecked") public List<TestUserTableEntity> select() { Query query = entityManager.createNativeQuery("SELECT * FROM TEST_USER_TABLE", TestUserTableEntity.class); return (List<TestUserTableEntity>) query.getResultList(); }
ここでは、NativeQuery(SQLを直接発行するもの)を使用しています。
jpaなので、jpqlを使用したものや、Entityに名前付きSQLとして発行する
NamedQueryなど色々あるのですが、それはおいおいで。
直感的にわかりやすいNativeQueryとしています。
これだと何がダメなのか?
query.getResultList()がSQLの結果をEntityの形にマッピングし、
返却してくれます。
当然、Listの1要素が1レコードです。
ここで、query.getResultList()の結果が1000万件くらいあったとしたらどうでしょう?
jpaではselectした結果はresultSet→Entityのマッピングを行うため、
単純に考えて2倍のメモリが必要になりますね。
なので、1000万件を格納する容量があっても、
メモリはあっという間にパンクする恐れがあります。
そこで、カーソルの出番です
public <T> List<T> getCacheSafeEntity(Query query, Class<T> clazz) { // この記述にはguavaが必要。 List<T> list = Lists.newArrayList(); // カーソル処理設定 query.setHint(QueryHints.CURSOR, true); CursoredStream cursor = (CursoredStream) query.getSingleResult(); long processedRow = 0L; while (cursor.hasNext()) { T entity = (T) cursor.next(); // Select結果をsql以外の条件でフィルターする領域 if (filterProcess(entity)) { list.add(entity); } processedRow++; if (processedRow % batchBlockSize == 0) { cursor.clear(); } } cursor.close(); return list; }
DBでプロシージャなどを実装したことがある方にはおなじみ、
カーソルの出番です。
eclipseLinkではカーソルを使用することができるような
実装がされています。
ということで、こんなメソッドを用意しました。
queryは呼び元で生成して、返すEntityの型をコンパイラに判断させるために、
引数としてClass型をもらいます。
queryにHistとしてカーソル使用する設定をします。
そうして、query.getSingleResultで取得し、CursorStreamという型で受けます。
カーソル使用を設定しておくと、1件毎のループが回るようになります。
ただ、ここで気をつけるのは、
カーソルを使っていてもそのままループさせていれば
通常の使用と同じ問題がおきます。
そこで、
処理件数をインクリメントさせつつ、
ある件数に到達した際に、
カーソルをクリアして上げる必要があります。
そうすることで1000万件回し切ることができます。
あ、最後にカーソルのクローズを忘れずに。
少し余談
QueryHintsの中に、QueryHints.SCROLLABLE_CURSORというのがあります。
一見すると、なんかカーソルより使えそうな感じがしますね。
ですが、検証してみたところ、通常の場合と同じようにOutOfMemoryで
エラーになりました。
そこで、QueryHints.CACHE_USAGEというものがあり、
それにCacheUsage.NoCahceをセットすれば動作することが確認できました。
何をしているかというと、
select結果をキャッシュしないでねという設定になります。
また、QueryHints.SCROLLABLE_CURSORを使用した場合は、
上記で示したサンプルと動作時間を比較したときに、
数十秒もの差が出て遅かったんです。
ハッキリと確認したわけではないですが、
eclipseLinkの実装を見た感じだと、
sqlの発行回数に差があり、その分が動作時間差にでているように
見えました。
まとめ
と、いうわけでjpaによる大量データ取得の仕方でした。
カーソル的に処理する機構がeclipseLinkにあるので、
それをうまく使いましょうということでした。
insertを大量にする場合も、これと同じように、
ある件数でEntityManagerをflushしてcloseするという流れに
すれば問題なく処理できます。
flush:更新されたEntityのsql発行
close:管理対象Entityの開放
それでは!
20代前半までは東京で音楽をやりながら両手の指以上の業種でアルバイト生活をしていましたが、某大手プロバイダのテレアポのバイトでPCの知識の無さに愕然とし、コンピュータをもっと知りたい!と思ったことをきっかけに25歳の時にITの世界に未経験で飛び込みました。
紆余曲折を経て、現在は個人事業主としてお仕事させていただいており、10年ほどになります。
web制作から企業システム構築、ツール開発など、フロントエンドもバックエンドもサーバーもDBAも依頼があれば何でもやってきた雑食系エンジニアです。
今風にいうとフルスタックエンジニアということになるのでしょうか??
→ 詳細プロフィールというか、生い立ちはこちら
→スキルシートをご覧になる場合はこちら
→お仕事のご依頼やお見積りなどお問い合わせはこちらから!