JavaOne2014 3日目メモ (9/30)
今日も晴れていて、過ごしやすい気候。3回目のJavaOneでも雨に振られたことがないので、サンフランシスコは雨が少ない地域なのだろうか。
Building a Distributed Application for the Cloud with Akka Clustering and Java 8 [TUT6483]
PlayフレームワークとAkkaを組み合わせて、最近話題のリアクティブなシステムを実現しましょうという話。
リアクティブ・マニフェストについて
詳細については原文を参照のこと。
- Responsive (応答性)
- システムは可能な限り最適なタイミングで応答すること。
- Resilient (障害からの回復)
- システムは処理が失敗した場合にも、失敗したことを応答すること。これは高可用性やミッションクリティカルシステムだけに求めるのではない - 処理の失敗時に何も応答できないシステムはResilientではない。
- Elastic (柔軟性)
- システムは大量のリクエスト下においても、応答すること。
Vert.x + WebSocket + Cloud = Awesome Map Tracking [CON1695]
Vert.xでwebsocketを実装して、以下の画面のように常に動くバス情報をブラウザ上の地図画像にアイコンとして表示するデモのセッション。RedHatが提供しているPaaSであるOpenShiftがVert.xに対応しているので、合わせてOpenShiftの紹介も少し。スライドはこちらで公開中。
セッションの冒頭で早速『今までのTomcatやJBossと何が違うの?』という質問が飛ぶ。分散環境でフレキシブルに重点を置いた新しいアーキテクチャと答える。
Going Native: Bringing FFI to the JVM [CON3979]
従来のJNIに代えて、もっと簡易にネイティブコードが呼び出せる仕組み JNR (Java Native Runtime)に関するセッション。スピーカーはJRubyの開発者であるCharles Nutterさん。JNRについてはgithubにて公開中。
JRubyを作っていて困った事
Javaで作られているJRubyを作るにあたって、OSレベルへのアクセス、例えばファイルシステムアクセスや、グラフィック/暗号化などのネイティブライブラリへのアクセスで困っている。しかし既存のJNIは使いにくい面も多い。もっと簡単にネイティブコードが呼び出せる仕組みがあると良い。
JNRのコード例
現在のプロセスIDを取得するlibcのgetpid関数を呼び出す例。とても直感的なコード例になっている。
import jnr.ffi.LibraryLoader; import jnr.ffi.annotations.IgnoreError; public class GetPidJNRExcample { public interface GetPid { @IgnoreError long getpid(); } public static void main(String[] args) { GetPid getpid = LibraryLoader.create(GetPid.class) .load("c"); getpid.getPid(); } }
JNRの構成要素
ユーザレベルの関数だけでなく、色々なネイティブ関数にアクセスできる。例えば以下のようなもの。
- jnr-posix : POSIX準拠の関数をJavaから実行
- jnr-unixsocket : unix socketをJavaから実行
- jnr-x86asm : x86アセンブラをJavaから実行
The Modular Java Platform and Project Jigsaw [CON5435]
前々から検討されているJDKへのモジュールシステム導入 Project Jigsaw に関するセッション。
互換性の維持と新機能の要望により増え続けるJDKのコアAPI実装にモジュール化システムを導入して、使いたいモジュールのみをロードする仕組みを作る。デモではrt.jarがなくなったよとのアピールがあり。
jlinkと呼ばれるツールで依存性を管理するらしいが、詳細は理解できなかった。JDK9のEarly Accessには今のところJavaOneでデモがあったjlinkやrt.jarの削除が盛り込まれていないので、JDK9のEAにマージされるのが待ち遠しい。
Troubleshooting with Serviceability and the New Runtime Monitoring Tool HeapStats [BOF3108]
NTT OSSセンタが開発したJavaトラブル解析支援ツール HeapStats のBOF。NTT OSSセンタはNTTグループ会社向けのOSSのテクニカルサポートを提供しており、頻出トラブルとトラブル解決の迅速化を目的に開発されたHeapStatsの紹介が行われた。
よくあるトラブルについて
OSSセンタに寄せられたサポート依頼では、ヒープ関連のトラブルシュート依頼が特に多かった。
トラブルシュートの課題
情報がない
ヒープのトラブルが多いが、トラブル発生時のヒープダンプが残っていない事も多く、-XX:+HeapDumpOnOutOfmemoryErrorをトラブル発生後に追加してもリークスピードが遅く、再現に数ヶ月かかるケースもあった。
既存ツールは重い
ヒープダンプは取得に時間がかかるためサービス影響が大きく、ヒストグラムではオブジェクト間の参照が見れないため解析情報が不足する。
JavaOne2014 2日目メモ (9/29)
2日目。朝は霧が出ていて少し寒いが、日中になると日差しが出て半袖でも良いくらい暑くなる。今日から本格的にセッションの開始。
Java EE 8 [CON3015]
Java EE8のスペックリードであるLinda DeMichielさんによる、Java EE 8の現状の方針を話すセッション。
Java EE 8の大きなテーマは3つ。
HTML5/Web層の改善
- JSON-Bの導入 : JSON - オブジェクトマッピングを標準化する
- JSON-P 1.1 : JSON Patch、JSON Pointerへの対応
- ServerSentEvent(SSE)への対応 : 既にGlassFishのJAX-RS実装であるJerseyで実装
- アクションベースのMVC
- Servlet4.0によるHTTP2.0対応
Easy of Development / CDI Alignment
Tutorial: JVM Platform as a Service [TUT3525]
スエーデンのCITERUSという企業の技術者が、2012年にJavaが使えるPaaSを使ったプロジェクトを経験談を交えながら、各PaaSプラットフォームの特徴やデモを示すセッション。
なぜPaaS?
- 何よりもtime-to-marketの達成がPaaSを使う一番の目的。
- 過去の事例では、顧客のコアドメインがITと関連が薄く、ITインフラ(サーバとか)を持ってなかったのがPaaS適用を推進した。
dotcloudを使ってみて
- Java、MongoDB、PostgreSQL、RabbitMQが使いたかったため、テクノロジスタックが合った
- 実際に運用してみたら色々問題も出てきた
- ヨーロッパ - アメリカのレイテンシによる性能問題、価格の値上げ改定
- 結果的にjerasticを使ってサービスを提供しているスエーデン国内のPaaSサービスに移行し、各々の問題に対処した。
HTTP 2.0 Comes to Java: What Servlet 4.0 Means to You [CON5989]
HTTP2.0(SPDY)の登場に伴い、ServletやJava SE9のAPIへの機能追加について紹介するセッション。まだ JSR369 Servlet4.0 として検討がはじまったところで、いくつかのコード案が紹介されたが写真取れず。
Java SE 9側でのHTTP2.0クライアントのアイディアについては、以下のようなコード例が紹介された。
HttpRequestGroup group = HttpRequestGroup.create(); HttpRequest req = group.createRequest() .setRequestMethod("POST") .setRequestURI(new URI("http://www.foo.com/a/b")) .setRequestBody("param1=1,param2=2") .onResponseHeader("X-Foo", (request, name, value) -> { System.out.println("receive an X-Foo header") }) .sendRequest() .waitForCompletion();
HTTP2.0では、1つのコネクションが1対1でのリクエスト/レスポンスモデルではなく、複数のリクエストが1コネクションで並行して送受信されるため、リクエストのグループ化(HttpRequestGroup)が導入し、その後のリクエスト送信にもメソッドチェーンや、リクエスト応答のヘッダ処理にラムダ式を書いたり、最近のJavaっぽいAPI案となっている。
Modular Architectures Using Microservices [CON6132]
モジュラアーキテクチャの考え方を、クラウド環境でOSGiを使うためにリモート呼び出しなどをサポートするAmdatuを使って実現する方法を紹介したセッション。
まず、amdatuによる実装を示す前に、ソフトウェアをなるべくモジュール化して連携させるモジュラアーキテクチャとマイクロサービスの考え方を紹介した。
モジュラアーキテクチャとは
原則として以下の2つを要件として紹介した。
マイクロサービスとは
- マーチンファウラ氏のブログの内容を時折引用。
- 分散システム固有の設計には注意する必要
- どれか1つが落ちていても、サービスが縮小継続できるように
- モニタリングに手間が掛かるのは、分散の代償
OSGiについて
Banking on OpenJDK: How Goldman Sachs Is Using and Contributing to OpenJDK [CON5177]
Goldman Sachs社のOpenJDKへの取り組みに関するセッション。
Goldman SachsとJava
- 1998年頃からJavaを技術評価として利用開始
- 2004年から自社コレクションフレームワークを作り始め(githubで公開中)
- 2013年からOCAにサインして、OpenJDKへのコントリビュートを開始した
- 190GB以上のヒープを持つシステム、大量の分岐があって、JITコードキャッシュに影響を与える特殊な環境が特徴的。
なぜOpenJDKに力を入れ始めたのか
- ソースが見れるのが何よりも重要
- 商用でクラッシュしても解析できる
- HotSpotの内部で改善できそうな部分を自分たちで見つけることができる
- パッチを投げることが、自分たちのためにも、コミュニティのためにもなる
どんな風にOpenJDKを使っている
- トラブルシューティング、新規能の研究・検証に使用
- JVMTIでアサーションエラーが発生したバグ解析とパッチ作成の事例を紹介
- 今のところ、プロダクション環境では使っていない
OpenJDKしてのソースが公開により、筆者自身もAPIやHotspotでのトラブル時に大変助かっている。Redhat Enterprise Linuxを使っているシステムではOpenJDKがバンドルされていて気軽に使えるので、もっと使われるところが増えると嬉しい。
JavaOne2014 1日目メモ (9/28)
JavaOne2014の初日はコミュニティが主体のユーザグループフォーラム(UGF)と、キーノートがサンフランシスコのモスコーンセンタにて行われた。カメラとPCを繋ぐケーブルを家に忘れたので、写真は後日追加予定。
以下、荒い部分も数多くあるがセッション参加メモ。
Lambdas and Laughs [UGF9672]
プロジェクトラムダの振り返りに関するセッション。
ラムダ式とは?ストリームAPIとは?Optionalは?など、Java8に入った新機能についてコードを見ながら振り返り、JavaOneでの関連セッションを紹介する。ユーザグループフォーラムのセッションらしく、途中でウケ狙いのスライドがたくさん入っているが、何回もスベる。
内容については様々なJava8特集で既出のため、特記事項なし。
GlassFish Roadmap and Executive Panel [UGF9120]
GlassFishのロードマップと、今後の方針に関するパネルディスカッション。アジェンダについては、GlassFishのホームページにもまとめられている。
ユーザグループフォーラムにも関わらず、オラクルのJava EE関連のエヴァンジェリストReza Rahmanさんや、この後のストラテジキーノートでも講演するCameron Purdyさん、日本でもWebLogicの製品発表関連で見かけるMichael Lehmannさんなど、オラクルとしての方針を話そうな立場の人たちがパネラーに並ぶ。
GlassFish 製品の基本的な戦略 (アナウンス済み内容と同じ)
GlassFish4.1の新機能
Java EE8の今のところの方針
The OpenJDK Project: Your Java. Our Java. [UGF9755]
OpenJDKの今後の方針とコントリビュート方法に関するセッション。JEP(JDK Enhancement Proposals)に挙がっているJDKの改善案の紹介が行われた。いずれもドラフト段階だが興味深い。
紹介されたキーワードを以下のまとめる。
- Shenandoah : 新しいGCストラテジ
- 100GBヒープのような巨大ヒープでも、10ms以下の停止を目指す
List<Integer> list = #[ 1, 2, 3 ];
- Measurement API
- 色々な物事の値を単なるintやdoubleのような数値ではなく、クラスとして定義して抽象化したもの
- githubのコード例により何となくイメージできる。例えば以下は心拍数を示す。
public static void main(String[] args) { HeartRate rate = HeartRateAmount.of(BigDecimal.valueOf(90), BPM); System.out.println(rate); }
- HTTP2.0 Client
- 軽量なJSONパーサ などなど
Strategy keynote / Technical keynote
Strategy Keynote
- キーノートの前日に子供たちにプログラミングを教えるイベントがあったらしく、キーノートは子供たちの成果発表から始まる。
- 子供向けだけではなく、コミュニティやJCPを通じてコミュニケーションを取る姿勢をアピール
Java SE
- Java 8 buzz
- Java8は色々な記事にも取り上げられ、書籍もたくさん出版されて確実に広まっている。
- OpenJDKへのコントリビュート
- オラクル以外のコミュニティによる貢献が引き続き行われている。ダグ・リーさんによるコンカレントAPIへの継続的な貢献など。
- Goldman sachsによるJava8の適用事例
Java ME
- 普段使ってないので聞いてもよくわからず。
Technical Keynote
- 言語仕様やJVMの改善を盛り込むProject Valhalla、ネイティブコード呼び出しを改善するProject Panamaについては時間切れで説明なし
今年のキーノートは過去の振り返りが中心で新しい発表はなく、OpenJDKコミュニティやJCPで流れているJava9やJavaEE8の内容はまだまだ議論の途中でオラクルによる公式な周知はまだない印象を受けました。
HeapStatsをChefでインストールする
HotSpotJVMの監視・解析OSSツールであるHeapStatsをChefでインストールできるようにクックブックを作ってみた。
n-agetsu/chef-heapstats · GitHub
HeapStatsとはなんぞやについては、JJUGナイトセミナーなどのスライドやキャスレー技術ブログ JavaVM監視・解析ツール HeapStatsを使ってみた参照。特に後述のブログには公式のwikiよりも詳しくまとまっている。
簡単にいうと、ヒープダンプのように一気に負荷をかけなくても、FullGC契機に少しずつヒープ情報をロギングしてくれる優れものである。
HeapStatsはアタッチ先のJVMへのオーバーヘッドを抑えるため、対応しているマシンが対応しているCPU命令セット(sse4 or avx or いずれも未対応)に応じてインストールするrpmファイルが異なる。このクックブックを使うと、マシンに合ったrpmをダウンロードしてインストールする。
前提条件
HeapStatsの動作条件は以下の通り。
HeapStatsクックブックの使い方
JDK7、WildFly、HeapStats agentの3つをChefでインストールする例を以下に示す。
vagrantやchef、knife-solo、berkshelfのインストールについては、Web上の記事やChef 実践入門本に詳しく紹介されているので省略。vagrant-berkshelfはハマってうまくいかなかったので、素直にknife-soloでセットアップする。
1. Cookbookの作成
適当な作業ディレクトリにクックブックを生成する。
cd work knife solo init .
2. Berksfileの編集
knife solo init .
の実行により、カレントディレクトリにBerksfileファイルが作成されている。ここにjava/WildFly8/HeapStatsのクックブックへの依存を定義する。
$ vim Berksfile source "https://supermarket.getchef.com" cookbook 'java' cookbook 'heapstats', git: 'git@github.com:n-agetsu/chef-heapstats.git' cookbook 'wildfly', git: 'git@github.com:bdwyertech/chef-wildfly.git' $ berks install
cookbook 'heapstats'で指定している部分が今回作ってみたHeapStatsクックブック。berks install
により、依存先クックブックは./cookbooksにダウンロードされる。
3. Nodeオブジェクトの作成
対象マシンに何をインストールするか定義するnodeファイルを作成する。ファイル名のcentos.jsonのcentosの部分は、マシンのホスト名 or IPアドレスを指定。
run_listと各クックブックのattributeを定義する。作ってみたHeapStatsのクックブックの依存先はjavaのみで、内部的にinclude_recipe 'java'して呼び出している。*1
wildflyクックブックにおいて、HeapStatsの有効化に必要なJava起動オプション-agentlib:heapstats
を追加している。他のAPサーバの場合でも、Java起動オプションに-agentlib:heapstatsを追加できればHeapStatsは有効化される。
heapstatsのfile/heaplogfile/archivefileはHeapStatsが出力する各種ファイルの出力先を指定している。ログが一カ所にまとまるように、wildflyのログディレクトリを指定した。
$ vim nodes/centos.json { "run_list": [ "recipe[heapstats]", "recipe[wildfly]" ], "java": { "install_flavor":"openjdk", "jdk_version":"7", "openjdk_packages":["java-1.7.0-openjdk", "java-1.7.0-openjdk-devel", "java-1.7.0-openjdk-debuginfo"] }, "wildfly": { "jpda": { "enabled":false }, "java_opts": { "other": ["-agentlib:heapstats"] } }, "heapstats": { "file":"/opt/wildfly/standalone/log/heapstats_snapshot.dat", "heaplogfile":"/opt/wildfly/standalone/log/heapstats_log.csv", "archivefile":"/opt/wildfly/standalone/log/heapstats_analyze.zip" } }
HeapStatsクックブックで指定可能な属性についてはREADME.mdに記載している。
4. プロビジョニング
knife solo bootstrap
で、対象サーバへのchefのセットアップknife solo prepareと、クックブックの実行knife solo cookの両方が動く。
$ knife solo bootstrap centos --bootstrap-version 11.12.0
centosの部分は、プロビジョニング対象マシンのホスト名を示す。IPアドレスでも可。
bootstrap-versionの指定がないとエラーになって動かない場合はがある。詳しくはstackoverflowに記載がある。
これでインストールは完了。JVMにHeapStatsがアタッチされた状態のWildFly8が起動している状態となる。
今後の課題
試しに作ってみたものの、以下のような課題があるので今後追加予定。
- HeapStatsの各種出力ファイルへのログローテーション (logrorate.d) 対応
- OpenJDKを使用する場合のdebuginfoインストール漏れのチェック
JDK8からあるちょっと嬉しいGCログオプション
JDK8およびJDK8u20では、GCログに関連する2つの便利な機能が追加されている。いずれの機能も2014/8現在最新のJDK7 update 67 には含まれていないが、JDK7u80にてバックポートされる予定。
GCログにpidと日付を含める (JDK8より)
JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/wildfly/gc_%p_%t.log" => 実際のファイル名例 : gc_pid31455_2014-08-31_14-20-16.log.0
GCログのフォーマットに%pを入れるとpid-Xloggc:gc.log.`date +%Y%m%d%H%M%S`
のようにOSコマンドによってファイル名に日付を付与していたが、この機能の追加によりOSコマンドへの依存がなくなるので便利。
jcmd GC.rotate_logによるローテーション (JDK8u20より)
JDK8u20よりjcmdにGCログをローテーションさせるコマンドが追加されている。
jcmd <pid> GC.rotate_log
以前よりGCログにはサイズローテーション機能が付与されていたが、Javaの場合ログのリロード機能がないため、logrotate.dなどによる日付ローテーションが困難であった。GC.rotate_logの導入により、以下の起動オプションと組み合わせると日付契機のログローテーションが実現できる。
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=0
UseGCLogFileRotationおよびNumberOfGCLogFilesが設定されていない状態でjcmd
を実行すると、GCログローテーションが有効となっていないことを示すエラー(Target VM does not support GC log file rotation.)となる。また、GCLogFilzeSize=0を設定し、ファイルサイズ契機のローテーションを止めている。
NumberOfGCLogFilesの指定がGC.rotate_logの実行に必須であるため、 ログの世代数管理をlogrotate.dに任せるのは難しい。またファイルのコピーや世代数を超えたファイルの削除はJVMが行ってくれるため、logrotate.dを使わずにまずはお試しとして直接cron.dailyに入れてみた。
# touch /etc/cron.d/cron.daily/gclog_rotate # vim /etc/cron.d/cron.daily/gclog_rotate #!/bin/sh sudo -u wildfly /usr/java/jdk1.8.0_20/bin/jcmd `cat /var/run/wildfly/wildfly.pid` GC.rotate_log
/var/run/wildfly/wildfly.pidより起動中のWildFlyのプロセスIDを確認している。WildFlyでなくても、/etc/rc.d/init.dに登録している場合は/var/run配下に現状のプロセスIDを示すファイルが出力されると思う。
Hibernateのバッチ更新効果を実測してみる
Hibernateには内部的にjava.sql.Statement.addBatch()およびexecuteBatch()を使ってバッチ更新でSQLを発行するオプションhibernate.jdbc.batch_size
がある。
デフォルトは無効となっているが、1トランザクションで大量のエンティティを登録する処理において、どれくらい効果的か実測してみる。
テスト環境
WildFlyはMac上で起動、DBはVirtualBox上のCentOSで起動しているので、取得できるデータはあくまで参考値レベル。綺麗な検証環境ではない。
テスト内容
1トランザクションでのエンティティ登録数を10、100、500、1000、3000、5000、10000エンティティと数を増やしていき、batch_size=30
を設定した場合としない場合の処理時間を比較する。
テストコード
一括エンティティ登録するEJB。大量エンティティ登録時にHibernate一次キャッシュによるOutOfMemoryErrorとならないように、バッチサイズに合わせてflush&clearでDBにSQLを発行させてキャッシュをクリアした。
@Stateless public class CustomerService { @PersistenceContext(unitName = "PostgresPU") private EntityManager em; public void bulkimport(int cnt) { for (int i = 0; i < cnt; i++) { Customer c = new Customer("test user" + i, "tokyo"); em.persist(c); if (i % 30 == 0) { em.flush(); em.clear(); } } } }
@WebServlet("/BulkImport") public class BulkImportServlet extends HttpServlet { @Inject private CustomerService customerService; protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { long start = System.currentTimeMillis(); customerService.bulkimport(10); long finish = System.currentTimeMillis(); long time = finish - start; System.out.println("time : " + time); response.setContentType("text/plain;charset=UTF-8"); try (PrintWriter out = response.getWriter()) { out.println("import done."); } } // 省略 ...
persistence.xmlは以下のようにシンプルな設定。batch_sizeなしのテストをするときは、hibernate.jdbc.batch_sizeをコメントアウトした。
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <persistence-unit name="PostgresPU"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <jta-data-source>java:jboss/jdbc/PostgresDS</jta-data-source> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQL9Dialect" /> <property name="hibernate.hbm2ddl.auto" value="create-drop" /> <property name="hibernate.jdbc.batch_size" value="30" /> </properties> </persistence-unit> </persistence>
測定結果
あくまで1つの参考値に過ぎないが、batch_size=30の設定でエンティティの一括登録が早くなることがわかる。
(処理時間の単位はミリ秒)
一括登録エンティティ数 | batch_size=30 | batch_sizeなし |
---|---|---|
10 | 321 | 297 |
100 | 417 | 504 |
500 | 1432 | 1878 |
1000 | 2023 | 2888 |
3000 | 4262 | 5331 |
5000 | 8999 | 12048 |
10000 | 13196 | 30902 |
まとめ
一度に大量のエンティティを追加・更新することが事前にわかっている場合は、hibernate.jdbc.batch_size
の設定は効果がありそう。エンティティの操作数が数十〜多くても1000エンティティくらいのWebアプリケーションではそれほど意識しなくても良いと思う。
メモ
addBatch()とexecuteBatch()の仕組みを知ろうとpostgresql-jdbc-9.3-1101のソースを眺めていて気がついたが、addBatch()はスレッドセーフではないので、素でJDBCを使って1つのStatementオブジェクトにマルチスレッドでaddBatch()すると、意図しない動作になりそう。
詳細はorg.postgresql.jdbc2.AbstractJdbc2Statement.javaのaddBatch(String p_sql)を参照。まだaddBatch()が一度もされていないかのnullチェックが同期化されていないため、タイミングによっては二重初期化されて登録済みのSQLが消えそうに見える。
JPAで少しずつデータを処理する方法を考える
OutOfMemoryErrorの主な要因例として、DBMSからデータを取得しすぎがあります。
LASYフェッチによるN + 1 問題を回避するために、結合先テーブルの要素を一気に持ってくるJOIN FETCH
を使ったところ、引き換えにJavaヒープ使用量が多くなるのはよくあるケースです。
以下のような、1つのIssueに対して複数のIssueAttributeを持つ1対多関連のエンティティの操作を例に、OutOfMemoryErrorを少しでも避ける方法を考えてみます。
@Entity public class Issue implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue private int issueId; private String title; @OneToMany @JoinColumn(name = "issueId") private List<IssueAttribute> attributes = new ArrayList<>();
ページングして少しずつ取ってくる
JPAではHibernate固有のScrollableResultsのようなAPIがないため、SQLのoffsetを示すsetFirstResult()とlimitを示すsetMaxResult()を組み合わせてページングします。後述していますが、JOIN FETCHではsetFirstResult()とsetMaxResult()の組み合わせが効かないため、普通のJOINを使っています。
private static final int FETCH_SIZE = 100; // 途中省略 ... String sql = "SELECT i FROM Issue i JOIN i.attributes ia"; int offset = 0; List<Issue> chunk = pagingQuery(em.createQuery(sql), offset, FETCH_SIZE); while (!chunk.isEmpty()) { for (Issue i : chunk) { for (IssueAttribute ia : i.getAttributes()) { // なんらかの処理 } } // ページングによって取得したエンティティをデタッチ em.flush(); em.clear(); offset += FETCH_SIZE; chunk = pagingQuery(em.createQuery(sql), offset, FETCH_SIZE); } private List<Issue> pagingQuery(Query query, int offset, int limit) { query.setFirstResult(offset).setMaxResults(limit); return query.getResultList(); }
@BatchSizeの設定
JOINでは結合先テーブルのエンティティがLASYフェッチになってしまうため、N + 1問題のように関連先の要素を取得するSQLが多く発行されてしまいます。Hibernate固有の機能ですが@BatchSize
を使うと、関連先エンティティ取得SQLの実行回数を減らすことができます。
@Entity public class Issue implements Serializable { @OneToMany @JoinColumn(name = "issueId") @org.hibernate.annotations.BatchSize(size = 100) private List<IssueAttribute> attributes = new ArrayList<>();
LASYフェッチでは1つのIssueごとに関連先のIssueAttributeを取得するSQLが実行されますが、上記のように@BatchSizeを設定した場合、100個のIssueエンティティごとに関連先のIssueAttribute取得するようになります。
JOIN FETCH ではページングがうまくいかない
手元のHibernate4.3.5&PostgreSQL9.3ではJOIN FETCHとsetFirstResult()およびsetMaxResult()を組み合わせると、以下のような警告が出力され、offsetとlimitが投げられるSQLに反映されずに一気にデータを取ってしまい、意図したようにページングができません。
19:28:32,647 WARN [org.hibernate.hql.internal.ast.QueryTranslatorImpl] (default task-1) HHH000104: firstResult/maxResults specified with collection fetch; applying in memory! 19:28:32,648 INFO [stdout] (default task-1) Hibernate: /* SELECT i FROM Issue i JOIN FETCH i.attributes ia */ select issue0_.issueId as issueId1_1_0_, attributes1_.attributeName as attribut1_2_1_, attributes1_.issueId as issueId2_2_1_, issue0_.priority as priority2_1_0_, issue0_.reporter_id as reporter4_1_0_, issue0_.title as title3_1_0_, attributes1_.attributeValue as attribut3_2_1_, attributes1_.issueId as issueId2_1_0__, attributes1_.attributeName as attribut1_2_0__, attributes1_.issueId as issueId2_2_0__ from Issue issue0_ inner join IssueAttribute attributes1_ on issue0_.issueId=attributes1_.issueId
JPA2.1仕様では、fetch joinとsetMaxResultsまたはsetFirstResultの併用時の挙動は定義しないとされています。
The effect of applying setMaxResults or setFirstResult to a query involving fetch joins over collections is undefined.
3.10.7 QueryExecution
StackOverFlowにこのWARNに関するQ&Aがあり、何行とってくるとエンティティが組み立てられるかわからないので、とりあえず毎回全部取ってくる実装になっているようです。
エンティティをこまめにデタッチする
JOIN FETCHをしながら少しでもJavaヒープの消費を抑えるには、EntityManager.clear()を使って少しでも処理済みの不要なエンティティをデタッチする方法もよく見かけますが、効果は限定的です。
String sql = "SELECT i FROM Issue i JOIN FETCH i.attributes ia"; // ↓ ここでヒープ消費が最大になる場合では効果薄 List<Issue> issues = em.createQuery(sql).getResultList(); int i = 0; for (Issue issue : issues) { for (IssueAttribute ia : issue.getAttributes()) { // ここの処理が時間がかかったり、 // この中でたくさんオブジェクトを展開する場合は効果あり } i++; if (i % 1000 == 0) { em.flush(); em.clear(); } }
getResultList()を実行した行で、既に取得した全てのエンティティがJavaヒープに展開されているため、この後でclear()しても最大消費量を抑えることはできません。取得したデータを処理する段階で、時間がかかったり、たくさんオブジェクトを展開する必要がある場合であれば効果が得られると思います。
補足 LasyInitializationException
@OneToManyのデフォルトやJPQLのJOIN
によって行われるLASYフェッチを使用してた場合、処理の途中でEntityManager.clear()を実行すると、以下のような例外を招きます。
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: net.agetsuma.jpatest.entity.Issue.attributes, could not initialize proxy - no Session