読者です 読者をやめる 読者になる 読者になる

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

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

JavaEE7をはじめよう(5) - JPAクエリ(その2) Native Query

JavaEE JPA

前回の記事ではJPAのクエリの定義方法のうち、JPQLについて解説した。
今回はNative Queryについて解説する。

Native Queryとは

Native Queryは直接SQLを実行する機能であり、JPQLではサポートされてないSQLやデータベース製品固有の方言などを実行することができる。

JPQLでサポートされてない構文には以下のようなものがある。

  • 右外部結合、完全外部結合、直積
  • FROM句でのサブクエリ使用。(select句やwhere句では使用できる)
  • DDLの実行(テストでの使用を想定)
  • CTE(共通テーブル式)の使用
  • UNION

ただしNative Queryは、JPQLで実現できない機能を利用する場合や、JPQLが生成するSQLが非効率な場合のみに使用するべきであり、JPQLがSQLに翻訳するコストがかかるという理由で使用するべきではない。 これは、JPQLからSQLへの翻訳コストは無視可能なほど低いことに加えて、後述するようにNative QueryはJPQLに比べて不便な所が多いからである。

定義・使用方法

JPQLと似ている部分があるので、異なる部分を中心に解説する。

JPQL同様に、静的・動的の2種類の定義方法がある。

  • NamedNativeQuery(名前つきネイティブクエリ)による定義
  • 文字列による定義

NamedNativeQueryの使用方法

NamedNativeQuery はアノテーション@NamedNativeQueryを用いて指定する。 @NamedQuery と異なる部分は、結果のマッピングに使用するプロパティ resultClass, resultSetMapping がある点だ。
詳細は後述するが、resultClassはクエリの結果を格納するエンティティクラスを指定するプロパティであり、resultSetMapping はクエリの結果をオブジェクトにマッピングする定義の名称を記載するプロパティである。 両者のうち、いずれかを設定する必要がある。

以下にNamedNativeQueryを使ったコード例を示す。
ここではMember.getNumberという名前をつけたNaitive Queryに対して、クエリ結果をMemberオブジェクトとして受け取るため、resultClassプロパティにMember.classオブジェクトを指定している。

@Entity
@Table(uniqueConstraints = @UniqueConstraint(
               columnNames = {"PLAYER_NUMBER", "belongs_id"}))
@NamedNativeQueries({
    @NamedNativeQuery(name = "Member.getNumber", 
      query = "Select id, PLAYER_NUMBER from member",
      resultClass = Member.class)
})
public class Member implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
    /** 背番号 */
    @Column(name="PLAYER_NUMBER",scale = 4)
    private int playerNumber;

実行する場合は、JPQL同様にEntityManager#createNamedQueryを使用する。

List<Member> list
  = em.createNamedQuery("Member.getNumber").getResultList();

文字列による定義の使用方法

文字列ベースで指定する場合は、EntityManager#createNativeQuery を使用する。その場合、第二引数には、resultClassまたはresultSetMapping名を指定する。これは、@NamedNativeQuery にあるプロパティと同じものだ。

List<Member> list
  = em.createNativeQuery("Select id, PLAYER_NUMBER from member", 
                         Member.class)
      .getResultList();

クエリ作成後の操作はJPQLの場合と同じである。

結果のマッピング

Native Queryの結果を任意のオブジェクトにマッピングするには、resultClassまたはresultSetMappingを使用する。 これらを指定しない場合、select句が単一の場合はObjectが、複数の場合はObject配列が結果の型となる。

ちなみに、先ほど説明したように、resultClassresultSetMappingは、NamedNativeQuery(名前つきネイティブクエリ)を利用する場合にはアノテーションのプロパティとして指定し、文字列による定義を利用する場合にはcreateNamedQueryメソッドの第2引数として指定する。

resultClass

resultClassに指定可能なのは、上記のサンプルにあるとおり、エンティティのclassオブジェクトのみである。
JPAselect句に指定した列から、エンティティのインスタンスを作成し、永続性コンテキストに管理状態として登録する。 そのため、select句にはエンティティクラスのIDを設定した列(ここではid)を必ず設定する必要がある。また、select句で指定されなかった列に対応するエンティティのプロパティにはnullが設定される。

resultSetMapping

resultSetMappingは任意のクラス構造に結果を設定する方法であり、JPA2.1より追加された。

これを利用することで、複数のエンティティのマッピングや、列名とプロパティ名が異なる場合のマッピング、エンティティ以外のクラスへのマッピングなど、様々なマッピングが可能となる。

マッピング内容はアノテーションとして指定し、クエリ生成時にそのマッピング名を指定する。

ここでは、前回の記事の後半で、コンストラクタ式を解説する際に紹介した、チームごとの人数を取得する例をこの機能を使用して実現する。

まず、SQLselect句からオブジェクトへのマッピング内容を定義するための@SqlResultSetMappingアノテーションを設定する。

@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = "name"))
@SqlResultSetMappings({
  @SqlResultSetMapping(name="summary", 
    classes = @ConstructorResult(targetClass = TeamSummary.class, 
              columns = {@ColumnResult(name = "name"), 
                         @ColumnResult(name = "count")}))
})
public class Team implements Serializable {
//....

上記の例では、summaryという名前のマッピングを定義し、その中で、クエリ実行時にTeamSummary クラスのコンストラクタを呼び出しての引数にselect句のname, countの値を順に渡すことを定義している。

このマッピングを使用するには、createNativeQueryメソッドの呼び出し時にマッピング名(@SqlResultSetMappingname属性)を指定する。

List<TeamSummary> summary2 = em.createNativeQuery(
        "Select t.name as name, count(*) as count"
             + " from Member m join Team t on m.belongs_id = t.id"
             + " group by t.id, t.name",
         "summary"
).getResultList();

Native Queryの注意点

JPQL と比較したNative Queryを扱う際の注意点を挙げる。

  • テーブル名、列名は実際の名前を使用する必要がある
    JPAでは、エンティティクラスに指定するアノテーションにより、実際のテーブル名や列名と異なる名前をプロパティ名に指定できる。
    前回説明したJPQLでは、クエリにエンティティのプロパティ名を指定したが、NativeQueryを利用する場合は実際の列名を指定する必要がある。

  • バインド変数にはインデックス指定 (?n)のみが使用でき、 文字列指定は使えない。

  • JPQLの便利な機能を利用できない
    JPQLの場合にはQuery#setFirstResultQuery#setMaxResultsを実行すると、 データベース製品にあわせた件数指定のSQLが生成される(例えば、OracleなどLIMIT句,OFFSET句が無い製品であっても、代替となる構文や関数を使用したSQLを自動で生成する)。
    しかし、Native Queryで同様の指定をした場合は、SQLはそのまま実行し、結果セットから必要な件数分のデータを取得するように動作する。
    このため、大量データから必要なデータを取得してページングするようなクエリでは、全データを取得するSQLが発行されるため、JPQLの場合と比較して実行効率が悪くなる可能性がある。
    また、NativeQuery で件数指定を行う場合は、データベース製品にあわせた専用のクエリを別途作成する必要があるため、JPQLを使用する方が移植性が高くなる。
    加えて、JPQLの説明で紹介した、ListによるIN句の設定や関連の自動ナビゲーションなどの便利な機能も利用できない。

まとめ

前回に続いて、今回はJPAのクエリの実行方法として、Native Queryを紹介した。次回は、Criteria APIを紹介する。

[前多 賢太郎]