エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

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点だろう。

  • MEMBERID が2のデータを取得するのと同時に、外部キーを辿ってTEAMのデータも取得している
  • MEMBERIDが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を採用するなら、有識者の確保や習熟のための期間は必要だろう。

次回はクエリの発行について解説する。

[前多 賢太郎]