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

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

JavaEE7をはじめよう(4) - JPAクエリ(その1) JPQL

今回から3回に分けて、JPAでエンティティを検索する手段であるクエリを解説する。

JPA では javax.persistence.Query というオブジェクトで、問い合わせの内容を表す。 このQueryを作成する方法は以下の3種類がある。

  • JPQL (Java Persistence Query Language)
  • Native Query
  • Criteria API

いずれの場合もEntityManagerにあるそれぞれのクエリを作成するメソッドを呼び出してQueryを生成する。
以降、それぞれの定義方法や特徴を記述するが、今回の記事ではJPQLについて解説し、Native QueryとCriteria APIに関しては次回以降で解説する。

JPQLとは

JPQL(Java Persistence Query Language)は、JPAが標準化したもので、SQLに似た問い合わせ言語である。

データベース製品ごとのSQL文法の違いを吸収するため、移植性が高い。また、エンティティに定義されている関連のナビゲーションなど、豊富な機能を提供している。 このため、JPAを利用する場合に採用するクエリ手段の第一候補となるだろう

JPQLの文法

詳細はこちらを参照のこと。

前回までのサンプルに含まれるMemberエンティティに対して、背番号が2以上の選手を取得するクエリを書くと次のようになる(サンプルに関してはこの連載の第2回記事を参照のこと)。

select m from Member m where m.playerNumber > 2

from句にはMember mのように、エンティティ名とそのエンティティのエイリアスを記述し、where句などではこのエンティティのプロパティを使って条件を指定する(SQLと異なりプロパティ名のみの記述はできず、m.playerNumber のようにエイリアスを付与する必要がある)。

またselect句にエンティティを指定すると、エンティティの全プロパティが取得され(select *に近い)、クエリの結果はエンティティのインスタンスとして返される。

select句には、エンティティ以外にも個別のプロパティや集計関数も記述できる(結果のマッピングは後述する)。

select max(m.playerNumber), m.name from Member m

結合もサポートする。
その場合、joinまたは left join に続けてfrom と同じように、エンティティ名とエイリアスを書いておく。

select m from Member m join Team t on m.belongs.id = t.id 
        where m.playerNumber > 1 order by t.name

結合条件は on句に記載可能だ(on句はJava EE7からサポートされている。on句ではなく、where句に書いても問題ない)。
ちなみに、JPQLでは内部結合と左外部結合のみをサポートする。

次の例のように、結合条件にキーではなくエンティティを指定することも可能だ。

select m from Member m join Team t on m.belongs = t 
        where m.playerNumber > 1 order by t.name

更に、エンティティに対して、アノテーションで関連を指定してあれば、結合条件がなくとも関連を辿ることができる。

select m from Member m  
        where m.playerNumber > 1 order by m.belongs.name

上の3つのJPQLは、ほぼ同じSQLに翻訳される。

JPQLの定義と使用方法

次に、JPQLをどのように定義し、使用するかを見ていこう。

まず定義方法には、以下の2つがある。

  • NamedQuery(名前つきクエリ)による定義
  • 文字列による定義

JPQLの使用方法(1) - NamedQuery

NamedQueryは静的なクエリを定義する手段で、主に@NamedQueryアノテーションで指定する(説明は割愛するが、xmlファイルとして定義することも可能だ)。
このNamedQueryは、アプリケーション起動時にJPQLの解析を行い、翻訳されたSQLをキャッシュするため、効率面で有利である。

定義方法としては、エンティティごとのクエリをエンティティクラスに指定するのが一般的だ。

下記の例では、Member.byName, Member.byNumberRange という名前の2つのクエリを定義している。 クエリ名はアプリケーション全体で一意にする必要があるので、エンティティ名などを付与すると良いだろう。

@Entity
@Table(uniqueConstraints = @UniqueConstraint(
              columnNames = {"PLAYER_NUMBER", "belongs_id"}))
@NamedQueries({
  @NamedQuery(name = "Member.byName", 
     query = "Select m from Member m where m.name = ?1"),
  @NamedQuery(name = "Member.byNumberRange", 
     query = "Select m from Member m where m.playerNumber between "
        + ":from and :to AND not m.playerNumber in :excludes")
})
public class Member implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
//省略

上記のJPQLの中の、 ?1:from などは、クエリの可変なパラメータを表現するバインド変数である。
バインド変数の詳細は次節で解説するが、? はインデックス指定、 : はパラメータ名の文字列指定を意味する(1つのクエリの中に両者は混在できない)。
特別な理由がなければ、後者の文字列指定のほうがわかりやすいだろう。

NamedQueryの実行

NamedQueryの実行方法は以下のようになる。

// 単一結果取得クエリの結果をOptionalにマップする関数
Function<String, Optional<Member>> getByOption = name -> {
    try {
        // NamedQueryの実行、インデックスによるパラメータ指定、単一取得
        Member m = em.createNamedQuery("Member.byName", Member.class)
          .setParameter(1, name)
          .getSingleResult();
        return Optional.of(m);
    } catch(NoResultException e){
        // getSignglResultは結果が無い場合、NoResultExceptionを投げる。
        return Optional.empty();
    }
};

// name1という選手を検索し、存在しない場合は空のMemberインスタンスを得る。
Member player = getByOption.apply("name1").orElse(new Member());


// 複数件取得クエリの実行、文字列によるパラメータ指定。
List<Member> r2 = em.createNamedQuery(
       "Member.byNumberRange", Member.class)
        .setParameter("from", 1)
        .setParameter("to", 12)
        // IN句にはリストを渡せる。
        .setParameter("excludes", Arrays.asList(3,5,10)) 
        // 取得開始位置や件数の指定。
        .setFirstResult(1).setMaxResults(10) 
        .getResultList();
クエリ条件の指定

上記のコードの5行目で、EntityManagercreateNamedQueryメソッドを呼び出す際に、Member.byNameという名前のNamedQueryを指定している。 第2引数に指定しているのはクエリ実行結果を格納するクラスである。第2引数を指定することで結果オブジェクトのキャストが不要になる。

createNamedQueryに限らず、EntityManagercreatexxxQueryメソッドjavax.persistence.Queryを実装したインスタンスを返す。
Queryに対する設定は、上記にあるようにメソッドチェーンで記述できる。

setParameterはバインド変数を設定するメソッドであり、インデックス指定なら数値を、文字列指定ならJPQLに記述した変数名(:は除く)を指定し、第2引数には設定値を記述する。

特筆すべきは、IN句にリストなどの可変値を直接渡せることだろう。こうした値の渡し方は、通常のプリペアドステートメントでは不可能だが、この記述をすればリストの要素数に応じたSQLが生成される。

setFirstResultsetMaxResults複数結果の取得開始件数や取得件数を指定することができる。 これらのメソッドを実行すると、SQLlimitoffsetなどのデータベースにあわせた件数指定が付与される。 (Oracleではlimit句やoffset句が存在しないため、row_numを指定する必要があるが、そうしたデータベース製品ごとの違いはJPAが吸収してくれる。)

クエリ結果の取得

クエリの実行および結果の取得はgetSingleResultまたはgetResultListで行う。

getSingleResultは名前の通り1件のみ結果を取得するが、結果が取得できない場合はNoResultExceptionが発生し、2件以上取得できた場合は、NonUniqueResultException が発生する。

getResultList複数件取得の結果をListに詰めて返す。結果が無い場合は0件のリストとなる。

本題から少し外れるが、上記のサンプルコードではJava8から提供された Optionalラムダ式を使用して、NoResultExceptionが発生した場合に空のOptionalを生成するようにした。こうすることで、呼び出し側で単純なnull判定の代わりに、柔軟な記述ができるようにしている。

JPQLの使用方法(2) - 文字列による定義

文字列によるJPQLは、例えば、画面から入力値によってWHERE条件を可変とするような動的なクエリの生成に使用する。 使用する方法は単純で、EntityManagercreateQueryメソッドの引数にJPQLを直接記述する。

em.createQuery("select m from Member m "
                 + "where m.playerNumber > :num order by m.id", 
               Member.class)

先ほどのcreateNamedQueryと同様に、このcreateQueryメソッドQueryインスタンスを生成するので、後の操作はNamedQueryの場合と同じである。

この方式では、クエリ実行のたびにJPQLの解析が行われる。このため、静的なJPQLは、NamedQueryで定義するほうがよいだろう。

結果のマッピング

ここまで紹介してきたJPQLでは、

select m from Member m

のように単一のエンティティをselect句に指定してきた。

単一の値をselect句に書いた場合は、その値の型がクエリの戻り値となる。
例えば、

select m.name from Member m

とした場合は、Member#nameの型であるStringとなる。

複数の値をselect句に書いた場合、結果はObject配列となる。 そのため、以下のような煩わしいコードを書く必要がある。

Object[] result = em.createQuery(
        "select m,t from Member m join Team t "
             + "on m.belongs = t where m.id = 1", Object[].class)
        .getSingleResult();

// 取得結果のキャストが必要
System.out.println(((Member)result[0]).getName());
System.out.println(((Team)result[1]).getName());

JPQLでは、これに対応するためコンストラクタ式という文法が用意されている。 コンストラクタ式とは、任意のクラスにselect句の取得結果を設定する機能である。

チーム名と、そのチームに所属するメンバー数のようなサマリを取得する例を使って説明しよう。

まず、サマリに対応するクラスを作成する。ポイントは引数ありのコンストラクタを持たせることだ。

package sample.dto;

public class TeamSummary {
    
    private String teamName;
    private long memberCount;
    
    public TeamSummary(String teamName, long count){
        this.teamName = teamName;
        this.memberCount = count;
    }
    
    public TeamSummary(){}
    
    // 以下、getter,setterが続く

そして、JPQLでは、newのキーワードで上記クラスのコンストラクタを呼び出し、select句で取得したい値を中に書く。 こうすることで、クエリ結果の型はTeamSummaryクラスとなる。

List<TeamSummary> summary = em.createQuery(
        "select new sample.dto.TeamSummary(t.name, count(t)) "
          + "from Member m join Team t on m.belongs = t group by t",
        TeamSummary.class
).getResultList();

for(TeamSummary s : summary) {
    System.out.println(s.getTeamName() + ":" + s.getMemberCount());
}

このようにすれば、オブジェクト配列を扱うことなく様々な結果を取得できる。

コンストラクタ式を使えば永続性コンテキストにキャッシュされない

select句にエンティティを指定した場合、クエリから取得したエンティティは永続性コンテキストの管理対象として扱われる。

これは、クエリから取得したエンティティのプロパティなどを変更すれば、自動的にデータベースと同期されることを意味する。
また、エンティティに関連がある場合は、関連を辿ったデータ取得も自動で行われることも意味する(これにより、 N+1問題といわれるパフォーマンスの問題を引き起こす可能性がある。この問題に関しては、今後の記事で解説する)。

大量データを取得する場合に永続性コンテキストにエンティティがキャッシュされ、メモリを圧迫する問題の対処方法については、過去の記事でも紹介したが、上で解説したコンストラクタ式を用いる方法でも回避できる。

サンプルで示したとおり、コンストラクタ式に使用するクラスはエンティティである必要は無く、エンティティでないクラスは永続化コンテキストにキャッシュされないためだ。(ただし、プロパティとしてエンティティを持つ場合、そのエンティティについては永続化コンテキストにキャッシュされる)。

サマリの取得のような、取得したい情報だけを定義したクラスをコンストラクタ式に指定することで、永続性コンテキストにエンティティをキャッシュすることなく、必要なデータを取得できる。

更新・削除クエリ

Queryを使って、UPDATE文やDELETE文を発行することも可能だ。エンティティのプロパティの更新やEntityManger#removeによる削除は、1件ずつSQLが実行されるが、この方法を使えば、複数件のレコードをまとめて操作できる。

次のように、更新・削除のクエリを作成し、executeUpdateメソッドによってSQLを実行すればよい。

int deleteted = em.createQuery(
               "Delete from Member m where m.name like :name")
            .setParameter("name", "P%")
            .executeUpdate();

固有な関数の実行

JPQLで使用可能な関数は、ここに書かれている。

ただし、JPA2.1 (Java EE7)から、JPQLにfunction句が追加され、ユーザー定義関数など任意の関数を利用できるようになった。以前はこのような場合、次回解説するNative Queryを使用するしかなかったため、Native Queryを減らすことができるだろう。

2つの引数を取るmyFunc関数がある場合、以下のようにfunction句に、関数名と引数を指定して実行する。

select function('myFunc', e.name, 100) from Member m

まとめ

今回はJPQLの使い方とクエリの実行方法について解説した。 次回はNative Queryについて解説する。

[前多 賢太郎]