JavaEE7をはじめよう(3) - JPAの永続性コンテキスト概要
前回の記事では、JUnit上で動作するJPAのサンプルコードを紹介した。
今回は、前回のサンプルをベースにして、JPAを使う上で重要な機能である永続性コンテキストについて解説する。
永続性コンテキスト
JPAは永続性コンテキストというある種のバッファのような仕組みによって、データベースと同期を取っている。 この仕組みを理解しておかないと、不可解な挙動に思えてしまうことだろう。
登録・更新時の動作
例えば、以下のコードを実行してみる。
@Test public void 永続化コンテキスト(){ Team t1 = new Team(); t1.setName("チームA"); EntityManager em = getEm(); em.getTransaction().begin(); // 永続化コンテキストへの登録 em.persist(t1); t1.setName("チームB"); em.getTransaction().commit(); }
この場合、em.getTransaction().commit()
を実行したタイミングで以下のSQLが実行される。
-- 実際にはバインド変数が使用される。 INSERT INTO TEAM (ID, NAME, UPDATED, VERSION) VALUES (1, 'チームB', 'システム時刻', 1)
結果として、NAMEフィールドにはpersist
の呼び出し後に設定した "チームB"という値が設定される。
永続性コンテキストの仕組みを知らなければ、em.persist(t1)
でINSERT文が発行されるように思えるのではないだろうか。
永続性コンテキストはプログラムとデータベースの間で、1つのトランザクション内のエンティティの集合を管理し、データベースとエンティティとのデータの同期を行う。
persist
メソッドは引数のエンティティを永続性コンテキストの管理対象とするメソッドだが、その時点でデータベースを更新するわけではない。管理状態となったエンティティは、永続性コンテキストによって適切なタイミングによってデータベースと同期される。
実際に同期を開始するのはトランザクションのコミットのタイミング(em.getTransaction().commit()
)である。前述のコードでは、データベース側にエンティティに対応するデータが無いので、その時点のエンティティが持っているデータ(t1.setName("チームB")
が呼び出された後)に対応するINSERT文が生成される。
このように同期を行うまではSQL発行を行わないので、登録・更新に関するSQLの発行回数を減らせるメリットがある。
取得時の動作
登録・更新のSQLだけでなく、データベースからの取得のSQL(SELECT)発行を減らすこともできる。
永続性コンテキストは、エンティティごとのID属性(主に @Id
を付与したプロパティ)をキーとしてデータベースから取得したデータを自身に保持する。
よって、以下の例のような同一のIDによるエンティティの取得を複数回行った場合、実際にSQLが発行されるのは初回分のみとなる。2回目以降の呼び出しでは、初回の呼び出しで永続性コンテキスト内に保持されたエンティティが返される。
Team t1 = em.find(Team.class, 1); // データベースから取得 Team t2 = em.find(Team.class, 1); // 永続性コンテキストから取得
また、エンティティに関連がある場合に、便利な機能を提供する。
たとえば、前回の記事で用意したTEAM
, MEMBER
のテーブルに以下のようなデータが入っているとする。
(選手2名が所属するチームが1つある状態)
TEAM
ID NAME 1 チームA MEMBER
ID PLAYER_NUMBER NAME BELONGS_ID 2 9 PLAYER 1 1 3 11 PLAYER 2 1
上記の状態で、以下のコードを実行してみよう。
Member m1 = em.find(Member.class, 2L); Member m2 = em.find(Member.class, 3L); System.out.println(m1.getBelongs().getName()); // チームA System.out.println(m2.getBelongs().getName()); // チームA
Member
エンティティには、多対1の関連(@ManyToOne
)が Team
エンティティに対して定義してあるので、関連を辿ってTeam
エンティティを取得できる。
このとき発行されるSQLは以下のようになる。
-- MEMBER.ID=2の取得。 TEAM.ID=1のデータ取得も同時に行われる。 SELECT ID, NAME, PLAYER_NUMBER, UPDATED, VERSION, BELONGS_ID FROM MEMBER WHERE (ID = 2) SELECT ID, NAME, UPDATED, VERSION FROM TEAM WHERE (ID = 1) -- MEMBER.ID=3の取得 SELECT ID, NAME, PLAYER_NUMBER, UPDATED, VERSION, BELONGS_ID FROM MEMBER WHERE (ID = 3)
注目すべきは、以下の2点だろう。
MEMBER
のID
が2のデータを取得するのと同時に、外部キーを辿ってTEAM
のデータも取得しているMEMBER
のID
が3の外部キーのTEAM
のデータはすでに取得済みなので、TEAM
を取得するSQLは発行されない。
このように、永続性コンテキストは、関連も考慮してデータ取得と無駄なSQL発行を抑制している。 (なお、この挙動はJPAの実装として、EclipseLinkを使用した場合のものであり、実装によっては結合によって取得するなど、 最適化される可能性がある)
また、1対多の関連の取得も可能だ。
Team t1 = em.find(Team.class, 1L); System.out.println(t1.getName()); // チームA for(Member m : t1.getMembers()) { System.out.println(m.getName()); //PLAYER 1, PLAYER 2 }
Team
エンティティには、1対多の関連(@OenToMany
)が Member
エンティティに定義してあるため、以下のようなSQLが発行されてMember
エンティティの取得も特に問題なく行われる。
SELECT ID, NAME, UPDATED, VERSION FROM TEAM WHERE (ID = 1) -- 1対多の関連の取得は遅延フェッチ SELECT ID, NAME, PLAYER_NUMBER, UPDATED, VERSION, BELONGS_ID FROM MEMBER WHERE (BELONGS_ID = 1)
ただし、多対1の場合と異なり、1対多の関連を取得するSQLの発行は、t1.getMembers()
メソッドを呼び出すまでは行われない(遅延フェッチ)。
これは、1対多の関連では大量データを取得する可能性があるため、デフォルトの設定では、必要になるまでデータ取得を行わないためだ(次回以降、設定の変更方法について記述する)。
このように、永続性コンテキストは関連を辿って必要なデータを取得する。 関連の定義をしておけば、自前でSQLを用意せずとも永続性コンテキストが必要なデータをそろえてくれる。 また、データのキャッシュも行ってくれるのでSQL発行回数を減らし、効率の向上も見込める。
EntityManager
EntityManager
は永続性コンテキストにてエンティティを操作やクエリの発行を行うインターフェースであり、JPAの主要なクラスだ。
JPAを理解するにはEntityManager
によるエンティティの操作とデータベースとの同期を理解しておく必要がある。
エンティティの操作
エンティティの操作を行う主要なメソッドを挙げる。これらのメソッドによってエンティティの状態が変更される。
メソッド | 内容 | エンティティの状態 |
---|---|---|
find |
IDによってエンティティを検索する | 管理対象 |
getReference |
IDによってエンティティを検索する。find と異なる点は、IDのみ取得し他のデータは遅延フェッチされる点である |
管理対象 |
persist |
新しくエンティティを管理対象にする | 管理対象 |
remove |
エンティティを削除対象にする | 削除対象 |
detach |
引数で指定されたエンティティを管理対象外(分離)にする | 分離 |
merge |
detach したエンティティなど対象外のエンティティを再度管理対象とする |
管理対象 |
clear |
全てのエンティティを管理対象外にする | 分離 |
エンティティの状態は以下の通り。
状態 | 内容 |
---|---|
新規(new) | エンティティクラスをコンストラクタで作成しただけの状態。persist の引数になるのはこの状態となる |
管理状態(managed) | 永続性コンテキスト内で管理されている状態。遅延フェッチや同期の際にINSERTやUPDATE(データベースと差異がある場合のみ)が行われる。 |
削除(removed) | データベースからの削除が予約された状態。同期時にDELETEが行われる。 |
分離(detached) | 永続性コンテキストの管理対象から外れた状態。プロパティの変更を行っても同期は一切行われない。 |
検索処理など、大量データを取得するだけの場合などは、意図的にclear
を発行することで永続性コンテキストで管理されるエンティティを減らし実行効率を向上させることが可能だ。
データベースとの同期
データベースとの同期を行う主なタイミングは以下の通り。
- トランザクションの終了時
- クエリの発行時
EntityManager#flush
の呼び出し(永続性コンテキストからデータベースへの反映)EntityManager#reload
の呼び出し(データベースの最新データを永続性コンテキストのエンティティに反映)
EntityManager#flush
はプログラマが明示的にデータベースへの反映を指示する手段として有用である。
また、同期と関係のあるエンティティの設定(アノテーション)を紹介する。 (前回のサンプルコードでも使用している)
@GeneratedValue
IDの連番など、シーケンスのようなデータベース側の連番機能を使用するカラムに付与する。
このアノテーションを付与したプロパティは新規登録時(persist
を呼び出す際)には、null
のままでかまわない。
永続性コンテキスト側でシーケンスからの連番取得などを自動で行って補完する。@Version
数値や時刻のプロパティに設定しておくと、永続性コンテキストが自動でこのプロパティを使用して楽観排他を行う。
(数値の場合、INSERT時は0で設定し、以降UPDATEのたびに WHERE条件への追加とインクリメントを行う)。
更新できなかった場合は、例外(OptimisticLockException
)が発生しロールバックされる。
これらの設定を使用することで、連番の設定や楽観排他のような定型的なコードを記述する必要が無くなる。
まとめ
永続性コンテキストやEntityManager
についての概要を説明した。
JPA(JPAに限らずO-Rマッパー全般)は、エンティティに対応するSQLを発行する以外に様々な機能を提供するため、 正しく理解して使わないと、使いづらいと感じたり、思わぬ挙動を示すことがあるだろう。
JPAを使う上で、永続性コンテキストの挙動を理解しておくことは重要だ。
上手に使えばSQLの発行回数や、SQLの記述、コードなどを削減できるし、
EntityManager
によって細かくデータベースの同期を取ったり、効率の向上を図ることができるためだ。
一方で、(1対多の関連取得で触れた遅延フェッチのような)様々な設定があるため、学習コストは高いといえるだろう。 JPAを採用するなら、有識者の確保や習熟のための期間は必要だろう。
次回はクエリの発行について解説する。
[前多 賢太郎]