JavaEE7をはじめよう(9) - JPA TIPS
今回はJPAのまとめとして、JPAをより良く使う方法や知っておくとよいトピックについて述べる。
1. テーブル定義とエンティティクラス定義の整合性を保つ仕組み
以前の記事で紹介したように、JPAではスキーマジェネレーションを用いて、エンティティクラスからDDLを生成して実行したり、任意のDDLを実行したりすることができる。必須というわけではないが、この仕組みは、データベース製品の違いを吸収したり、ユニットテストでテーブルのセットアップが簡単に行えたりできて便利である。
とはいえ、テーブル定義などは別のツールで管理することもあるだろう。 その場合でも、IDEの機能によりテーブルからエンティティを作成したり、エンティティからテーブルやDDLを作成したりすることが可能なので、これらの機能を使用し、データベースとエンティティの定義を二重管理しないようにするとよい。
アノテーションによるインデックス定義
少々余談になるが、JPA2.1よりアノテーションの設定でインデックスを作成することもできるようになった。 これによってDDLの記述量を減らせるだろう。
@Entity // @Tableアノテーションのindexesでインデックスを定義。 // coolumnListはテーブル側の列名でカンマ区切り @Table(indexes = @Index(name = "member_index", columnList = "NAME,ID", unique = true )) public class Member implements Serializable {
JPAの機能を利用して関連や制約を定義する
JPAでエンティティ間の関連を設定することにより、関連を辿る際に結合のクエリを書く必要がなくなる。 他にも、カスケードの設定を行うことで、エンティティの登録時に関連エンティティが無い場合にエラーとしたり、逆にエンティティの登録・削除時に関連エンティティもまとめて登録・削除するといった設定が可能だ。
他にも、プロパティごとにユニーク制約やNULL値の可否、データサイズの指定などを行うことができる。
これらの設定を適切に行う事で、永続化周りのコードやクエリを省力化することが可能だ。
また、上記の関連や制約はデータベース側にも、外部キーや制約といった形で反映されるので、アプリケーションのチェック漏れなどによって発生する不正なデータの登録を防ぐことができる。
JPAで関連や制約を定義することは、結果としてデータベースに正しく制約を設定することにつながる。
(もちろん、正しく定義したデータベースの内容からエンティティを作ることも可能だ)。
O-Rマッパーは、データベースのことを知らなくても使えるツールと考える人もいるかもしれない。
しかし、機能が豊富なJPAは、むしろデータベースの仕組みきちんと理解した上で使いこなすべきツールといえるだろう。
2. 性能問題と排他制御への対処方法
次に、性能問題やロックなど、データベースを扱うプログラムで考慮すべき課題について、JPAで対処する方法を紹介する。
2.1 N+1問題
1対1、多対1の関連があるエンティティを取得する際、関連エンティティを取得するSQLが別途実行されることを 以前の記事で述べた。
エンティティを1件取得するだけなら問題ないが、クエリによって大量のエンティティを取得すると、各エンティティについて関連を取得するSQLが発行されるため、パフォーマンスの低下を招く。
この問題は、1回のクエリでN件のエンティティを取得した際に、N+1回のSQLが発行されるため N+1問題と呼ばれる。
(補足しておくが、関連するテーブルのデータが非常に少なければ、永続性コンテキストのキャッシュが効くので、問題にならない場合もある。)
SQLを使用する場合なら、結合を行って関連するデータをまとめ、1回のクエリでデータをまとめて取得するだろう。
JPQLでは、次のようにJOIN FETCH
を指定することで、関連エンティティを結合したSQLを生成して、一度のクエリで取得することが可能となる。
List<Member> members = em.createQuery("select m from Member m JOIN FETCH m.belongs", Member.class) .getResultList();
JOIN FETCH <関連のあるプロパティ>
と指定することで、その関連をJOINするSQLが作られる。
また、JOIN FETCH
は 1対多、多対多への関連(@OneToMany
, @ManyToMany
アノテーションを設定したプロパティ)に対しても実行可能だ。
// 1対多の場合、結合先のレコード数に合わせて結果が増幅するのでdistinctを行う。 // 結合先のレコードが無い場合を考慮し、外部結合で取得する。 List<Team> ts = em.createQuery("select distinct t from Team t LEFT JOIN FETCH t.members", Team.class) .getResultList(); for (Team team : ts) { // JOIN FETCH していない場合、ここでクエリ実行が行われる。 System.out.println(team.getName() + " has " + team.getMembers().size() + " members."); }
1対多の関連(@OneToMany
アノテーションを指定したエンティティ)の取得方法はデフォルトでは遅延取得だが、JOIN FETCH
を用いると結合による即時取得が可能となり1回のクエリで全てのデータを取得できる。
余談になるが、@OneToMany(fetch = FetchType.EAGER)
とすれば即時取得が可能となる。ただし、その場合の挙動は、getter の呼び出し時などに遅延実行されるN+1回分のクエリが、最初にまとめて実行されるだけである。このためFetchType.EAGER
の指定では、N+1問題の解決にはならない。
N+1問題は、JPAを使用する上で避けて通れないので、設計段階から対策を考慮しておくとよいだろう。
2.2 プロパティの遅延ロード
JPQLで、select e
のように記述すると、通常はテーブルの全カラムを取得してしまう。BLOBのような巨大なデータを含むカラムがある場合には、無駄にデータを取得してしまう可能性がある。
そうした状況に対処するため、JPAでは一部のカラムを遅延ロードすることができる。
その場合、エンティティ定義に @Basic
アノテーションを付与し、fetch
属性に遅延ロードすることを指定しておく。
@Entity public class ContainsBlob implements Serializable { @Id private String name; @Column @Lob @Basic(fetch = FetchType.LAZY) private byte[] largeData; // 省略 }
プログラムでは特に意識せずに、遅延ロードでデータが取得される。
// この時点では select句に largeData は指定されない。 List<ContainsBlob> list = em.createQuery( "Select e from ContainsBlob e", ContainsBlob.class) .getResultList(); // 省略 // 遅延フェッチ対象のプロパティの取得が行われるときに、 // 再度クエリを発行してBLOBデータを取得する。 return list.get(0).getLargeData();
この例に限った話ではないが、遅延ロードではデータが必要になって初めてSQLを発行する。このため、トランザクションが終了している場合にはデータを取得できない点に注意が必要だ。
2.3 行ロック
@Version
アノテーションによる楽観排他の方法は以前の記事で紹介した。
楽観排他とは別に、テーブルの行ロックをかける (いわゆる SELECT ... FOR UPDATE
) 方法が JPA2.0より用意されている。
具体的な方法としては、クエリ取得時に一括でロックする方法と、エンティティに個別でロックをかける方法がある。
クエリ取得時に一括でロックする方法(Query#setLockMode
)
List<Member> members = em.createQuery("select m from Member m", Member.class) .setLockMode(LockModeType.PESSIMISTIC_WRITE) .getResultList();
任意のタイミングで個別にロックをかける方法(EntityManager#lock
)
em.lock(entity, LockModeType.PESSIMISTIC_WRITE);
LockModeType
には何種類か存在するが、使用頻度が高いのは、読み取りロックを取得するPESSIMISTIC_READ
と、書き込みロックを取得するPESSIMISTIC_WRITE
だろう。
ロック待ちなし(NOWAIT) を設定する方法
SELECT FOR UPDATE NOWAIT
を設定することも可能だ。javax.persistence.lock.timeout
プロパティに 0
を設定すればよい。
このプロパティの設定には、定義ファイルに記述する方法と、プログラムで個別に指定する方法の2通りがある。
定義ファイルに記述する方法
persistence.xmlファイルに以下の記述を追加する。この指定を行うと、ロックを取得する全てのクエリにFOR UPDATE NOWAIT
が設定される。
<property name="javax.persistence.lock.timeout" value="0"/>
プログラムで個別に設定する方法
EntityManager#lock
メソッドにMap
でプロパティを設定すると、エンティティごとにNOWAIT
の設定ができる。
Map<String, String> props = new HashMap<>(); props.put("javax.persistence.lock.timeout", "0"); em.lock(entity, LockModeType.PESSIMISTIC_WRITE, props);
この方法を使った場合、特定のエンティティに対するすべてのクエリにNOWAIT
が設定される。
同じエンティティであってもクエリごとNOWAIT
の有無を使い分けたい場合は、次のクエリヒントを使う必要がある。
3. クエリヒント
クエリヒントとは、JPAのクエリにカスタマイズを加える機能である。
どのようなクエリヒントがあるかは、JPAの実装によって異なる。
このため、クエリヒントを使用することでJPA実装の変更が難しくなることに留意する必要がある。
EclipseLink*1で提供されているヒントはこちらにある。
いくつかの例を紹介する。
3.1 クエリの結果を永続性コンテキストにキャッシュさせない方法
本ブログでは、永続性コンテキストをキャッシュさせない方法をいくつか紹介してきた。 今までに紹介したものは次の3つだ。
EclipseLinkが提供するクエリヒント MAINTAIN_CACHE
を使っても同様の機能が実現できる。
JPA実装がサポートしているならば、この方法がキャッシュを無効化するための、もっともお手軽な方法である。
Member m = em.createQuery( "select m from Member m where m.id=1", Member.class) .setHint(QueryHints.MAINTAIN_CACHE, HintValues.FALSE) .getSingleResult(); // contains はエンティティが永続性コンテキストに管理状態にあるかを調べる em.contains(m) // false
3.2 クエリごとに NOWAIT
ロックをかける方法
2.3節で説明したロックの方法では、NO WAIT
のロックをかけるには、設定ファイルで一律で設定するか、クエリ発行後にエンティティごとに個別に設定するしかなかった。
以下のクエリヒントを使用することで、クエリごとに NO WAIT
ロックをかけることが可能になる。
なお、PESSIMISTIC_LOCK
も EclipseLink固有のヒントである。
List<Member> members = em.createQuery( "select m from Member m", Member.class) .setHint(QueryHints.PESSIMISTIC_LOCK, PessimisticLock.LockNoWait) .getResultList();
3.3 JPQLでJOIN FETCH
を指定せずにJOIN FETCH を行う
JOIN FETCH を行うには、JPQL中に、JOIN FETCH <対象プロパティ>
を記述する必要があった。
FETCH
ヒントを使うことで、同様の機能が使用可能になる。
このFETCH
ヒントも EclipseLink固有のヒントである。
// 前述のN+1問題と同じ結果を得るJPQL List<Team> ts = em.createQuery("select t from Team t", Team.class) // JPQL の JOIN FETCH に指定するプロパティを引数にする .setHint(QueryHints.FETCH, "t.members") .getResultList();
4. エンティティグラフ
エンティティグラフとは、JPA2.1 から追加された エンティティのプロパティの即時・遅延ロードを指定する機能である。
すでに、@Basic
や @OneToMany
などのアノテーションにおけるFetchType
属性でロードタイミングを指定する方法は述べたが、これらの指定方法は静的なため、ロード方法を切り替えるにはアノテーション属性を変更する必要がある。
エンティティグラフはクエリ実行時に動的にロード方法を設定する仕組みである。
エンティティグラフを設定する方法には、以下の2つがある
EntityManager#createEntityGraph
によりプログラムで作成する@NamedEntityGraph
アノテーションにより静的に設定し、EntityManager#getEntityGraph
で取得する
今回は前者の例を紹介する。
// Teamの name, membersプロパティ, // 関連先のMemberのplayerNumberプロパティのみを即時ロードする EntityGraph<Team> graph = em.createEntityGraph(Team.class); graph.addAttributeNodes("name", "members"); graph.addSubgraph("members").addAttributeNodes("playerNumber"); List<Team> teams = em.createQuery("select t from Team t", Team.class) // エンティティグラフはクエリヒントで設定する。 .setHint("javax.persistence.fetchgraph", graph) .getResultList();
上記の例では、Team
エンティティのname
と、関連エンティティのmembers
プロパティ、およびMember
エンティティのplaylerNumber
を即時ロードするようにエンティティグラフを設定した。
エンティティグラフをクエリに設定するには以下のいずれかのクエリヒントを指定すればよい。
javax.persistence.fetchgraph
- エンティティグラフ内の項目は即時ロード、それ以外は遅延ロードjavax.persistence.loadgraph
- エンティティグラフ内の項目は即時ロード、それ以外はエンティティの設定に従う
上記のエンティティグラフを設定したところ、以下のようなSQLが発行された。
SELECT ID, NAME, UPDATED, VERSION FROM TEAM SELECT ID, PLAYER_NUMBER, VERSION FROM MEMBER WHERE (BELONGS_ID = ?)
テーブルの全項目を取得していないことに注目されたい。
ただし、ID
やVERSION
など、@Id
や @Version
を指定した項目は必ず取得されるようだ。
上記で即時ロードされなかったプロパティをgetterなどで取得しようとすると、遅延ロードによって再度SQLの発行が行われる。
以上、簡単にエンティティグラフの紹介を行った。 プロパティ単位でロード方法を制御したい場合には、採用を検討するとよいだろう。
まとめ
JPA について数回にわたって紹介した。
JPA は仕組みを正しく理解して、適切に使えば非常に有用なツールである。
使い方を間違えると、N+1問題のような性能問題を引き起こしてしまうが、そうした問題への解決方法もきちんと用意されている。
少しでも興味を持っていただければ幸いである。
次回からCDI(Context And Dependency Injection)について紹介していく。
[前多 賢太郎]