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

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

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 = ?)

テーブルの全項目を取得していないことに注目されたい。 ただし、IDVERSIONなど、@Id@Version を指定した項目は必ず取得されるようだ。

上記で即時ロードされなかったプロパティをgetterなどで取得しようとすると、遅延ロードによって再度SQLの発行が行われる。

以上、簡単にエンティティグラフの紹介を行った。 プロパティ単位でロード方法を制御したい場合には、採用を検討するとよいだろう。

まとめ

JPA について数回にわたって紹介した。

JPA は仕組みを正しく理解して、適切に使えば非常に有用なツールである。
使い方を間違えると、N+1問題のような性能問題を引き起こしてしまうが、そうした問題への解決方法もきちんと用意されている。
少しでも興味を持っていただければ幸いである。

次回からCDI(Context And Dependency Injection)について紹介していく。

[前多 賢太郎]

*1:Eclipse Foundationが提供するJPA実装