JPAを深掘りする〜Criteria APIで型安全な検索を追求しよう!【基本編】


今回はJavaEEの中でも特に重要な「JPA(Java Persistence API)」を深掘りしてみます。基本編と応用編の2回に渡ってじっくりとJPAをご紹介します。

JPAとは

JPAは一言で言えば「高機能なDBアクセスフレームワーク」です。DBアクセスフレームワークは実に様々なものがありますが、JPAの特徴は

  • エンティティクラスへの操作を通してDB操作を実現する(DBの都合はプログラマから極力隠される)
  • 型安全を追求するための様々な機能を備える

が挙げられます。Javaの良さを引き出す工夫に満ちたフレームワークと言えるでしょう。

様々な機能を備えたJPAですが、本エントリでは、検索、中でもCriteria APIを使った型安全な検索を中心にご紹介します。コードをしっかり載せていきますので、Criteria APIのコンセプトを感じ取っていただけると思います。

なお、JPAを動作させる環境を構築する方法は「JavaEEだけでここまでできる!GlassFish+Eclipseで高速Webアプリ開発【環境構築編】」をご覧下さい。

準備1・メタモデルクラスを自動生成する設定

メタモデルクラスとは、エンティティクラスのプロパティに関する情報を表すクラスであり、型安全にJPAを使うためには欠かせないクラスです。

例えば次のようなエンティティクラスがあったとしましょう。

package sandbox.entity;

import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long      id;

    @Column(length = 150, unique = true, nullable = false)
    String    name;

    // getter/setterは省略
}

このエンティティクラスに対するメタモデルクラスは次のようになります。

package sandbox.entity;

import javax.annotation.Generated;
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.StaticMetamodel;

@Generated(value="Dali", date="2016-02-12T22:59:19.623+0900")
@StaticMetamodel(User.class)
public class User_ {
	public static volatile SingularAttribute<User, String> name;
	public static volatile SingularAttribute<User, Long> id;
}

メタモデルクラスはeclipseのような開発環境に自動で作ってもらうことができますので、有効にしておきましょう。

ecliseの場合の手順を紹介しておきます。プロジェクトのプロパティを開き、「プロジェクト・ファセット」から「Convert to faceted form…」をクリックします。

convert-to-faceted-form

 

プロジェクト・ファセットが有効になったので、JPAサポートを有効にします。

project-facets-jpa

ここで「 OK」を押していったんプロパティ画面を閉じます。

再びプロパティ画面を開くと「JPA」という設定項目が増えていますので、メタモデルクラスの自動生成を有効にします。

eclipse-jpa-metamodel

これで、エンティティクラスに変更が入る度にメタモデルクラスが自動生成されます。メタモデルクラスの名前はエンティティクラスの名前の後ろに _ を付けたものになります。

entity-metamodel

 

準備2・SQLをコンソールに出力する設定

JPAを使いこなすコツはJavaコードを中心に考えることですが、さすがにSQLが見えないと辛すぎます。せめてコンソールにSQLを出力するようにしておきましょう。JPAの設定ファイルであるpersistence.xmlにプロパティを追記します。

EclipseLinkの場合

<property name="eclipselink.logging.level.sql" value="FINE" />
<property name="eclipselink.logging.parameters" value="true" />
<property name="eclipselink.logging.logger" value="org.eclipse.persistence.logging.DefaultSessionLog"/>

OpenJPAの場合

<property name="openjpa.Log" value="DefaultLevel=WARN, Runtime=INFO, Tool=INFO, SQL=TRACE"/>
<property name="openjpa.ConnectionFactoryProperties" value="PrintParameters=true" />

APサーバにGlassFishを使っているのであれば、EclipseLinkの方を設定してください。

スキーマ定義をエンティティクラスで行う

JPAの良さを引き出すコツは、Javaコードを中心に考えることです。まずはスキーマ定義をエンティティクラスで行うようにしてみましょう。

ただ、実はJPAの仕様にはエンティティクラスからDDLを作る機能は含まれていません。JPAの実装であるEclipseLinkやOpenJPAの機能を使います。通常、persistence.xmlにプロパティを追記します。

EclipseLinkの場合

<property name="eclipselink.ddl-generation" value="create-tables" />

OpenJPAの場合

<property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema(ForeignKeys=true)" />

APサーバにGlassFishを使っているのであれば、EclipseLinkの方を設定してください。

Criteria APIによる検索

JPAの検索には、JPQLとCriteria APIの2つのやり方があります。JPQLは文字列でクエリを記述するため、コンパイル時に型チェックが全く働きません。

例えば以下のコードは実行時にClassCastExceptionがスローされてしまいます。

private static void selectByJPQL(final EntityManager em) {
    final String q = "select e from Tag e";
    for (final User user : em.createQuery(q, User.class).getResultList()) {
        System.out.println(user);
    }
}

よく見ると、クエリ文字列がfrom Tagとなっています。結果のエンティティはUser型で受けていますから、型が異なっていおり、実行時にClassCastExceptionがスローされるのです。

しかしCriteria APIを使うと、このようなミスはコンパイル時に検出できるようになります。上のJPQLをCriteria APIで描き直してみましょう。

private static void selectByCriteria(final EntityManager em) {
    final CriteriaBuilder builder = em.getCriteriaBuilder();
    final CriteriaQuery<Tag> criteria = builder.createQuery(Tag.class);
    criteria.from(User.class);

    final TypedQuery<User> query = em.createQuery(criteria);
    for (final User user : query.getResultList()) {
        System.out.println(user);
    }
}

eclipseでこのコードを書くと、以下のようにコンパイルエラーとしてミスを教えてくれます。

jpa-criteria-error

 

このように、Criteria APIを使うとクエリのミスをコンパイル時にチェックできるようになります。これがCriteria APIの最大の利点です。

また、副次的な効果ですが変なクエリが書かれないようになります。実際の開発現場では、変態的なSQLが横行してチューンングの足枷になったり、特定のDB製品でしか実行できない書き方がされていたりと、SQL周りの問題が起きやすいです。しかしCriteria APIを使えばクエリの記述方法がある程度統一されるため、問題が起きにくく、またレビューしやすくなります。

Criteria APIによるクエリの基本形

Criteria APIはJPQLの構造をJavaクラスで表現したものと考えることができます。JPQLの構成要素の基本は次の3つです。

  1. from句:どのエンティティ(≒テーブル)からデータを取得するか
  2. where句:抽出条件
  3. select句:どのエンティティ、あるいはプロパティなどを取得するか

この3つを踏まえると、Criteria APIを使ったプログラムの基本形は次のようになります。

private static void selectUsersByName(final EntityManager em, final String pUserName) {
    // クエリを組み立てるためのオブジェクトを得る
    final CriteriaBuilder builder = em.getCriteriaBuilder();

    // クエリ全体を表現するオブジェクトを作成.
    // 引数として、クエリ結果の型を指定する.
    final CriteriaQuery<User> query = builder.createQuery(User.class);

    // from句指定
    final Root<User> root = query.from(User.class);

    // where句指定
    query.where( //
            builder.equal(root.get(User_.name), pUserName) //
            , builder.isNotNull(root.get(User_.name)) //
    );

    // select句指定.
    // ここではプロパティではなくエンティティそのものを指定.
    // ただしfromと同じエンティティを指定する場合はこの行は省略可能.
    query.select(root);

    // クエリ実行
    final List<User> result = em.createQuery(query).getResultList();
    for (final User user : result) {
        System.out.println(user);
    }
}

結果の型を指定できるのがとても強力です。例えば全件数を得るクエリは次のように書けます。

private static void countAllUsers(final EntityManager em) {
    final CriteriaBuilder builder = em.getCriteriaBuilder();

    // 件数なので結果は数値で得るようにする
    final CriteriaQuery<Long> query = builder.createQuery(Long.class);
    final Root<User> root = query.from(User.class);

    // count関数を使う
    query.select(builder.count(root));

    final long count = em.createQuery(query).getSingleResult().longValue();
    System.out.println(count);
}

基本編はここまでです。次回の応用編では、Criteria APIの実戦的な機能をご紹介します。


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>