Java EE 7 検証環境構築(8) JPA 問い合わせ(2) 動的クエリとCriteria API を試す

  1. Java EE 7 検証環境構築(1) WildFly + JBoss Tools で EARプロジェクトを作成し Arquillian で ユニットテストをグリーンにするところまで
  2. Java EE 7 検証環境構築(2) WildFly に DataSourceを作成
  3. Java EE 7 検証環境構築(3) JPAからMySQLに接続するユニットテストをArquillianで実行
  4. Java EE 7 検証環境構築(4) Java EE での DI(Dependency Injection) および CDI(Contexts and Dependency Injection)をながめる
  5. Java EE 7 検証環境構築(5) JBoss Toolsが生成したサンプルソースのCDIを確認する
  6. Java EE 7 検証環境構築(6) JPA エンティティの作成と挿入
  7. Java EE 7 検証環境構築(7) JPA 問い合わせ(1) 名前付きクエリを使ってみる。テストでトランザクションも意識する
  8. Java EE 7 検証環境構築(8) JPA 問い合わせ(2) 動的クエリとCriteria API を試す
  9. Java EE 7 検証環境構築(9) jBatch 概要をおさえる
  10. Java EE 7 検証環境構築(10) JBoss Tools で作成した EARプロジェクトをJava EE 6 から 7 に変更する
  11. Java EE 7 検証環境構築(11) jBatch用 プロジェクトの作成を行う
  12. Java EE 7 検証環境構築(12) jBatch 簡易サンプル作成と Arquillian でユニットテスト
  13. Java EE 7 検証環境構築(13) jBatch REST サービス経由で実行する
  14. Java EE 7 検証環境構築(14) WildFly の管理をGUIで行う
  15. Java EE 7 検証環境構築(15) WildFly を サービスとして設定する(Windows/Linux)
  16. Java EE 7 検証環境構築(16) WildFly と Apache を mod_jk で連携させる(Widows)

検索条件が静的なクエリは、名前付きクエリで対応出来るが、検索画面など複数の検索条件を動的に組み立てる場合には対応出来ないため、動的にクエリを組み立てる必要が出てくる。

SQL組み立て条件を、XMLで指定したりSQLの中に式を組み込んだりと、ORMマッパーの種類によりいろいろ対応方法があるが、JPAでは動的にクエリ文字列を組み立てる方法と、Criteria APIを利用して動的にクエリを組み立てる方法とがある。

1.動的クエリ

  • 動的クエリを作成するのに EntityManager.createQuery() を使用する
  • このメソッドはJPQLクエリを表す文字列を受け取る
    • Query query = em.createQuery(jpql);
  • 型付けを行いたい場合、TypedQueryを利用する
    • TypedQuery<Book> query = em.createQuery(jpql, Book.class);
  • 名前付きでパラメータを渡す場合、:parameterName で宣言し、query.setParameter(“parameterName”,parameterValue) で値を与える
  • 位置指定パラメータを渡す場合、?1 で宣言し、query.setParameter(1, parameterValue) で値を与える
  • JPQLクエリ文法

ということで実際に試してみる。まぁなんというか、昔ながらの愚直な方法ではある。

この例も、動的にくめるよねという視点のみで、動かすことのみを前提に作成しているので、実際にはもう少し機能の共通化(例えば、汎用的に条件を組み立てるとか、ページングとか)とリファクタリングが必要。

書き方も、JPQLとしての見通しがもう少しよくなるような工夫も必要だろう。

とはいえ、とりあえず意図したようには動く。また、ネイティブなSQLを組み立てて、それを実行することも当然出来る。

public List selectBooksByCondition(
        String title, 
        Float price, 
        String description,
        String isbn) {

    int condCnt = 0;
    Map parameterMap = new HashMap();
    StringBuilder buf = new StringBuilder();
    
    buf.append("select b from Book b");
    if (title != null ||
        price != null ||
        description != null || 
        isbn != null){
    
        buf.append(" where");
        
        if (title != null) {
            buf.append(((condCnt++ == 0)?"":" and"));
            buf.append(" title like :title");
            parameterMap.put("title", title + "%");
        }
        
        if (price != null) {
            buf.append(((condCnt++ == 0)?"":" and"));
            buf.append(" price = :price");
            parameterMap.put("price", price);
        }

        if (description != null) {
            buf.append(((condCnt++ == 0)?"":" and"));
            buf.append(" description like :description");
            parameterMap.put("description", "%" + description + "%");
        }

        if (isbn != null) {
            buf.append(((condCnt++ == 0)?"":" and"));
            buf.append(" isbn = :isbn");
            parameterMap.put("isbn", isbn);
        }
    
    }

    //Query query = em.createQuery(buf.toString());
    TypedQuery query = em.createQuery(buf.toString(), Book.class);
    if (parameterMap.size() > 0) {
        for(Map.Entry entry : parameterMap.entrySet()) {
            query.setParameter(entry.getKey(), entry.getValue());
        }
    }

    return query.getResultList();
}

2.Criteria API(オブジェクト指向クエリ)

  • 文字列で指定するクエリは間違えやすく、実行時に検出されるため原因の特定が困難となる
  • JPA2.0から Criteria API でオブジェクト指向のプログラミングで正しいクエリ構文を記述出来るようになっており、コンパイル時にエラーを検出出来る
  • Criteria APIはJPQLで出来ることがすべて出来るようになっている
  • チュートリアル

で、上記で動的にJPQLを作成してみたが、同様なことを、Criteriap API を使用して、タイプセーフに行うこともができる。

コードとしては、こちらの方がすっきりしてはいるが、やはり、このAPIの挙動を知る必要がある。ある程度複雑なSQLを、Criteria APIに翻訳して実装しなければいけないし、一旦翻訳すると、元のSQLはプロダクトコードではなくなってしまうし。。。

一概に、Criteria API の方が、動的クエリよりよいとは言いがたいところもある。

public List<book> selectBooksByCondition2(
        String title, 
        Float price, 
        String description,
        String isbn) {        

    CriteriaBuilder builder = em.getCriteriaBuilder();
    CriteriaQuery<book> query = builder.createQuery(Book.class);
    
    Root<book> book = query.from(Book.class);
    
    query.select(book);
   
    List<predicate> creteria = new ArrayList<predicate>();
        
    if (title != null) {
        creteria.add(builder.like(book.<string>get("title"), title + "%"));
    }
    
    if (price != null) {
        creteria.add(builder.equal(book.<float>get("price"), price));
    }

    if (description != null) {
        creteria.add(builder.like(book.<string>get("description"), "%"+description+"%"));
    }

    if (isbn != null) {
        creteria.add(builder.equal(book.<string>get("isbn"), isbn));
    }
    
    if (creteria.size() > 0) {
        query.where(builder.and(creteria.toArray(new Predicate[]{})));
    }
    
    return em.createQuery(query).getResultList();
}

タイプセーフなCriteria API

上記、Criteria API では、クエリの構築は確かにタイプセーフとなっているが、モデルのプロパティアクセスには、文字列を使っているためタイプセーフとは言いがたい。型も自らコーディングしている。

JPAでは、メタモデルを定義でき、以下の様にCriteria API を利用することが出来る。

http://relation.to/Bloggers/HibernateStaticMetamodelGeneratorAnnotationProcessor

エンティティ

@Entity public class Order {
    @Id 
    Integer id;
    @ManyToOne 
    Customer customer;
    @OneToMany 
    Set items;
    BigDecimal totalCost;
    ...
}

メタモデル

@StaticMetamodel(Order.class)
public class Order_ {
    public static volatile SingularAttribute id;
    public static volatile SingularAttribute customer;
    public static volatile SetAttribute items;
    public static volatile SingularAttribute totalCost;
}

クエリ

CriteriaBuilder cb = ordersEntityManager.getCriteriaBuilder();
CriteriaQuery cq = cb.createQuery(Order.class);
SetJoin itemNode = cq.from(Order.class)
	.join(Order_.orderItems);
cq.where( cb.equal(itemNode.get(Item_.id), 5 ) ) 
	.distinct(true);

エンティティのプロパティの型指定と、型名の文字列をクエリで記述することが不要になっている。エンティティ+”_”がメタ情報を管理している。

例えば、このメタモデルのリフレクションを使用すると、クライアントからの検索条件をタイプセーフにクエリに落とし込む共通処理がかけそうだなーとか思いながら、メタモデルをEclipse+JBoss Tools 上で簡単に自動生成する手順がよくわからないので、今後の課題。

Follow me!

コメントを残す

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