web-dev-qa-db-ja.com

JPA基準APIを使用して、条件式を「AND」および「OR」述語と組み合わせる

次のコード例を適用する必要があります。

このようなMySQLクエリがあります(2015-05-04および2015-05-06は動的であり、時間範囲を象徴しています)

SELECT * FROM cars c WHERE c.id NOT IN ( SELECT fkCarId FROM bookings WHERE 
    (fromDate <= '2015-05-04' AND toDate >= '2015-05-04') OR
    (fromDate <= '2015-05-06' AND toDate >= '2015-05-06') OR
    (fromDate >= '2015-05-04' AND toDate <= '2015-05-06'))

bookingsテーブルとcarsテーブルがあります。時間範囲内でどの車が利用可能かを知りたいのですが。 SQLクエリはチャームのように機能します。

これをCriteriaBuilder出力に「変換」したいと思います。この出力で過去3時間にドキュメントを読みました(これは明らかに動作しません)。また、サブクエリのwhere部分もスキップしました。

CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
CriteriaQuery<Cars> query = cb.createQuery(Cars.class);
Root<Cars> poRoot = query.from(Cars.class);
query.select(poRoot);

Subquery<Bookings> subquery = query.subquery(Bookings.class);
Root<Bookings> subRoot = subquery.from(Bookings.class);
subquery.select(subRoot);
Predicate p = cb.equal(subRoot.get(Bookings_.fkCarId),poRoot);
subquery.where(p);

TypedQuery<Cars> typedQuery = getEntityManager().createQuery(query);

List<Cars> result = typedQuery.getResultList();

別の問題:fkCarIdは外部キーとして定義されておらず、単なる整数です。そのように修正する方法はありますか?

19
Steven X

MySQLデータベースに必要なフィールドのみを含む次の2つのテーブルを作成しました。

mysql> desc cars;
+--------------+---------------------+------+-----+---------+----------------+
| Field        | Type                | Null | Key | Default | Extra          |
+--------------+---------------------+------+-----+---------+----------------+
| car_id       | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| manufacturer | varchar(100)        | YES  |     | NULL    |                |
+--------------+---------------------+------+-----+---------+----------------+
2 rows in set (0.03 sec)

mysql> desc bookings;
+------------+---------------------+------+-----+---------+----------------+
| Field      | Type                | Null | Key | Default | Extra          |
+------------+---------------------+------+-----+---------+----------------+
| booking_id | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| fk_car_id  | bigint(20) unsigned | NO   | MUL | NULL    |                |
| from_date  | date                | YES  |     | NULL    |                |
| to_date    | date                | YES  |     | NULL    |                |
+------------+---------------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

bookingsテーブルのbooking_idは主キーであり、fk_car_idcarsテーブルの主キー(car_id)を参照する外部キーです。


IN()サブクエリを使用した対応するJPA基準クエリは、次のようになります。

CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Cars> criteriaQuery = criteriaBuilder.createQuery(Cars.class);
Metamodel metamodel = entityManager.getMetamodel();
Root<Cars> root = criteriaQuery.from(metamodel.entity(Cars.class));

Subquery<Long> subquery = criteriaQuery.subquery(Long.class);
Root<Bookings> subRoot = subquery.from(metamodel.entity(Bookings.class));
subquery.select(subRoot.get(Bookings_.fkCarId).get(Cars_.carId));

List<Predicate> predicates = new ArrayList<Predicate>();

ParameterExpression<Date> fromDate1 = criteriaBuilder.parameter(Date.class);
Predicate exp1 = criteriaBuilder.lessThanOrEqualTo(subRoot.get(Bookings_.fromDate), fromDate1);
ParameterExpression<Date> toDate1 = criteriaBuilder.parameter(Date.class);
Predicate exp2 = criteriaBuilder.greaterThanOrEqualTo(subRoot.get(Bookings_.toDate), toDate1);
Predicate and1 = criteriaBuilder.and(exp1, exp2);

ParameterExpression<Date> fromDate2 = criteriaBuilder.parameter(Date.class);
Predicate exp3 = criteriaBuilder.lessThanOrEqualTo(subRoot.get(Bookings_.fromDate), fromDate2);
ParameterExpression<Date> toDate2 = criteriaBuilder.parameter(Date.class);
Predicate exp4 = criteriaBuilder.greaterThanOrEqualTo(subRoot.get(Bookings_.toDate), toDate2);
Predicate and2 = criteriaBuilder.and(exp3, exp4);

ParameterExpression<Date> fromDate3 = criteriaBuilder.parameter(Date.class);
Predicate exp5 = criteriaBuilder.greaterThanOrEqualTo(subRoot.get(Bookings_.fromDate), fromDate3);
ParameterExpression<Date> toDate3 = criteriaBuilder.parameter(Date.class);
Predicate exp6 = criteriaBuilder.lessThanOrEqualTo(subRoot.get(Bookings_.toDate), toDate3);
Predicate and3 = criteriaBuilder.and(exp5, exp6);

Predicate or = criteriaBuilder.or(and1, and2, and3);
predicates.add(or);
subquery.where(predicates.toArray(new Predicate[0]));

criteriaQuery.where(criteriaBuilder.in(root.get(Cars_.carId)).value(subquery).not());

List<Cars> list = entityManager.createQuery(criteriaQuery)
        .setParameter(fromDate1, new Date("2015/05/04"))
        .setParameter(toDate1, new Date("2015/05/04"))
        .setParameter(fromDate2, new Date("2015/05/06"))
        .setParameter(toDate2, new Date("2015/05/06"))
        .setParameter(fromDate3, new Date("2015/05/04"))
        .setParameter(toDate3, new Date("2015/05/06"))
        .getResultList();

興味のある次のSQLクエリを生成します(Hibernate 4.3.6 finalでテスト済みですが、このコンテキストでは平均的なORMフレームワークに矛盾はないはずです)。

SELECT
    cars0_.car_id AS car_id1_7_,
    cars0_.manufacturer AS manufact2_7_ 
FROM
    project.cars cars0_ 
WHERE
    cars0_.car_id NOT IN  (
        SELECT
            bookings1_.fk_car_id 
        FROM
            project.bookings bookings1_ 
        WHERE
            bookings1_.from_date<=? 
            AND bookings1_.to_date>=? 
            OR bookings1_.from_date<=? 
            AND bookings1_.to_date>=? 
            OR bookings1_.from_date>=? 
            AND bookings1_.to_date<=?
    )

上記のクエリのWHERE句の条件式を囲む括弧は技術的にまったく不要であり、Hibernateが無視する読みやすさを向上させるためにのみ必要です-Hibernateはそれらを考慮する必要はありません。


個人的には、EXISTS演算子を使用することを好みます。したがって、クエリは次のように再構築できます。

CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Cars> criteriaQuery = criteriaBuilder.createQuery(Cars.class);
Metamodel metamodel = entityManager.getMetamodel();
Root<Cars> root = criteriaQuery.from(metamodel.entity(Cars.class));

Subquery<Long> subquery = criteriaQuery.subquery(Long.class);
Root<Bookings> subRoot = subquery.from(metamodel.entity(Bookings.class));
subquery.select(criteriaBuilder.literal(1L));

List<Predicate> predicates = new ArrayList<Predicate>();

ParameterExpression<Date> fromDate1 = criteriaBuilder.parameter(Date.class);
Predicate exp1 = criteriaBuilder.lessThanOrEqualTo(subRoot.get(Bookings_.fromDate), fromDate1);
ParameterExpression<Date> toDate1 = criteriaBuilder.parameter(Date.class);
Predicate exp2 = criteriaBuilder.greaterThanOrEqualTo(subRoot.get(Bookings_.toDate), toDate1);
Predicate and1 = criteriaBuilder.and(exp1, exp2);

ParameterExpression<Date> fromDate2 = criteriaBuilder.parameter(Date.class);
Predicate exp3 = criteriaBuilder.lessThanOrEqualTo(subRoot.get(Bookings_.fromDate), fromDate2);
ParameterExpression<Date> toDate2 = criteriaBuilder.parameter(Date.class);
Predicate exp4 = criteriaBuilder.greaterThanOrEqualTo(subRoot.get(Bookings_.toDate), toDate2);
Predicate and2 = criteriaBuilder.and(exp3, exp4);

ParameterExpression<Date> fromDate3 = criteriaBuilder.parameter(Date.class);
Predicate exp5 = criteriaBuilder.greaterThanOrEqualTo(subRoot.get(Bookings_.fromDate), fromDate3);
ParameterExpression<Date> toDate3 = criteriaBuilder.parameter(Date.class);
Predicate exp6 = criteriaBuilder.lessThanOrEqualTo(subRoot.get(Bookings_.toDate), toDate3);
Predicate and3 = criteriaBuilder.and(exp5, exp6);

Predicate equal = criteriaBuilder.equal(root, subRoot.get(Bookings_.fkCarId));
Predicate or = criteriaBuilder.or(and1, and2, and3);
predicates.add(criteriaBuilder.and(or, equal));
subquery.where(predicates.toArray(new Predicate[0]));

criteriaQuery.where(criteriaBuilder.exists(subquery).not());

List<Cars> list = entityManager.createQuery(criteriaQuery)
        .setParameter(fromDate1, new Date("2015/05/04"))
        .setParameter(toDate1, new Date("2015/05/04"))
        .setParameter(fromDate2, new Date("2015/05/06"))
        .setParameter(toDate2, new Date("2015/05/06"))
        .setParameter(fromDate3, new Date("2015/05/04"))
        .setParameter(toDate3, new Date("2015/05/06"))
        .getResultList();

次のSQLクエリを生成します。

SELECT
    cars0_.car_id AS car_id1_7_,
    cars0_.manufacturer AS manufact2_7_ 
FROM
    project.cars cars0_ 
WHERE
    NOT (EXISTS (SELECT
        1 
    FROM
        project.bookings bookings1_ 
    WHERE
        (bookings1_.from_date<=? 
        AND bookings1_.to_date>=? 
        OR bookings1_.from_date<=? 
        AND bookings1_.to_date>=? 
        OR bookings1_.from_date>=? 
        AND bookings1_.to_date<=?) 
        AND cars0_.car_id=bookings1_.fk_car_id))

同じ結果リストを返します。


追加:

ここでsubquery.select(criteriaBuilder.literal(1L));は、EclipseLinkの複雑なサブクエリステートメントでcriteriaBuilder.literal(1L)などの式を使用しているときに、EclipseLink gets confused となり、例外が発生します。そのため、EclipseLinkで複雑なサブクエリを作成する際に考慮する必要があります。その場合は、idを選択するだけです。

subquery.select(subRoot.get(Bookings_.fkCarId).get(Cars_.carId));

最初の場合のように。注:上記の式をEclipseLinkで実行すると、結果リストは同じになりますが、SQLクエリ生成で odd behaviour が表示されます。

また、バックエンドデータベースシステムでより効率的になる結合を使用することもできます。その場合、親テーブルからの結果リストが必要なので、DISTINCTを使用して重複する可能性のある行を除外する必要があります。詳細テーブルに複数の子行が存在する場合、結果リストには重複行が含まれる場合があります-bookingsは対応する親行carsに対応します。 あなたに任せています。 :) これがここでの方法です。

CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Cars> criteriaQuery = criteriaBuilder.createQuery(Cars.class);
Metamodel metamodel = entityManager.getMetamodel();
Root<Cars> root = criteriaQuery.from(metamodel.entity(Cars.class));
criteriaQuery.select(root).distinct(true);

ListJoin<Cars, Bookings> join = root.join(Cars_.bookingsList, JoinType.LEFT);

ParameterExpression<Date> fromDate1 = criteriaBuilder.parameter(Date.class);
Predicate exp1 = criteriaBuilder.lessThanOrEqualTo(join.get(Bookings_.fromDate), fromDate1);
ParameterExpression<Date> toDate1 = criteriaBuilder.parameter(Date.class);
Predicate exp2 = criteriaBuilder.greaterThanOrEqualTo(join.get(Bookings_.toDate), toDate1);
Predicate and1 = criteriaBuilder.and(exp1, exp2);

ParameterExpression<Date> fromDate2 = criteriaBuilder.parameter(Date.class);
Predicate exp3 = criteriaBuilder.lessThanOrEqualTo(join.get(Bookings_.fromDate), fromDate2);
ParameterExpression<Date> toDate2 = criteriaBuilder.parameter(Date.class);
Predicate exp4 = criteriaBuilder.greaterThanOrEqualTo(join.get(Bookings_.toDate), toDate2);
Predicate and2 = criteriaBuilder.and(exp3, exp4);

ParameterExpression<Date> fromDate3 = criteriaBuilder.parameter(Date.class);
Predicate exp5 = criteriaBuilder.greaterThanOrEqualTo(join.get(Bookings_.fromDate), fromDate3);
ParameterExpression<Date> toDate3 = criteriaBuilder.parameter(Date.class);
Predicate exp6 = criteriaBuilder.lessThanOrEqualTo(join.get(Bookings_.toDate), toDate3);
Predicate and3 = criteriaBuilder.and(exp5, exp6);

Predicate or = criteriaBuilder.not(criteriaBuilder.or(and1, and2, and3));
Predicate isNull = criteriaBuilder.or(criteriaBuilder.isNull(join.get(Bookings_.fkCarId)));
criteriaQuery.where(criteriaBuilder.or(or, isNull));

List<Cars> list = entityManager.createQuery(criteriaQuery)
        .setParameter(fromDate1, new Date("2015/05/04"))
        .setParameter(toDate1, new Date("2015/05/04"))
        .setParameter(fromDate2, new Date("2015/05/06"))
        .setParameter(toDate2, new Date("2015/05/06"))
        .setParameter(fromDate3, new Date("2015/05/04"))
        .setParameter(toDate3, new Date("2015/05/06"))
        .getResultList();

次のSQLクエリを生成します。

SELECT
    DISTINCT cars0_.car_id AS car_id1_7_,
    cars0_.manufacturer AS manufact2_7_ 
FROM
    project.cars cars0_ 
LEFT OUTER JOIN
    project.bookings bookingsli1_ 
        ON cars0_.car_id=bookingsli1_.fk_car_id 
WHERE
    (
        bookingsli1_.from_date>? 
        OR bookingsli1_.to_date<?
    ) 
    AND (
        bookingsli1_.from_date>? 
        OR bookingsli1_.to_date<?
    ) 
    AND (
        bookingsli1_.from_date<? 
        OR bookingsli1_.to_date>?
    ) 
    OR bookingsli1_.fk_car_id IS NULL

お気づきのように、HibernateプロバイダーはWHERE NOT(...)に応じてWHERE句の条件ステートメントを反転します。他のプロバイダーも正確なWHERE NOT(...)を生成する場合がありますが、結局、これは質問に書かれたものと同じであり、前の場合と同じ結果リストを生成します。

右結合は指定されていません。したがって、JPAプロバイダーはそれらを実装する必要はありません。それらのほとんどは、正しい結合をサポートしていません。


完全を期すためのそれぞれのJPQL :)

IN()クエリ:

SELECT c 
FROM   cars AS c 
WHERE  c.carid NOT IN (SELECT b.fkcarid.carid 
                       FROM   bookings AS b 
                       WHERE  b.fromdate <=? 
                              AND b.todate >=? 
                              OR b.fromdate <=? 
                                 AND b.todate >=? 
                              OR b.fromdate >=? 
                                 AND b.todate <=? ) 

EXISTS()クエリ:

SELECT c 
FROM   cars AS c 
WHERE  NOT ( EXISTS (SELECT 1 
                     FROM   bookings AS b 
                     WHERE  ( b.fromdate <=? 
                              AND b.todate >=? 
                              OR b.fromdate <=? 
                                 AND b.todate >=? 
                              OR b.fromdate >=? 
                                 AND b.todate <=? ) 
                            AND c.carid = b.fkcarid) ) 

左結合を使用する最後のもの(名前付きパラメーター付き):

SELECT DISTINCT c FROM Cars AS c 
LEFT JOIN c.bookingsList AS b 
WHERE NOT (b.fromDate <=:d1 AND b.toDate >=:d2
           OR b.fromDate <=:d3 AND b.toDate >=:d4
           OR b.fromDate >=:d5 AND b.toDate <=:d6)
           OR b.fkCarId IS NULL

上記のすべてのJPQLステートメントは、すでにご存じのとおり、次の方法を使用して実行できます。

List<Cars> list=entityManager.createQuery("Put any of the above statements", Cars.class)
                .setParameter("d1", new Date("2015/05/04"))
                .setParameter("d2", new Date("2015/05/04"))
                .setParameter("d3", new Date("2015/05/06"))
                .setParameter("d4", new Date("2015/05/06"))
                .setParameter("d5", new Date("2015/05/04"))
                .setParameter("d6", new Date("2015/05/06"))
                .getResultList();

必要/必要に応じて、名前付きパラメーターを対応するインデックス付き/位置パラメーターに置き換えます。

これらのJPQLステートメントはすべて、上記の基準APIによって生成されたものと同じSQLステートメントも生成します。


  • このような状況、特にMySQLを使用している場合は、IN()サブクエリを常に回避します。 IN()サブクエリは、次のような静的な値のリストに基づいて結果セットを決定したり、行のリストを削除する必要がある場合など、絶対に必要な場合にのみ使用します。

    SELECT * FROM table_name WHERE id IN (1, 2, 3, 4, 5);`
    DELETE FROM table_name WHERE id IN(1, 2, 3, 4, 5);
    

    と同様。

  • 結果リストには別のテーブルの条件に基づいた単一のテーブルのみが含まれるため、このような状況ではEXISTS演算子を使用するクエリを常に優先します。この場合の結合は、上記のクエリの1つで示されているように、DISTINCTを使用して除外する必要がある前述の重複行を生成します。

  • 取得する結果セットが複数のデータベーステーブルから結合される場合は、結合を使用するのが好ましいでしょう-とにかく使用する必要があります。

結局のところ、すべては多くのものに依存しています。これらはマイルストーンではありません。

免責事項:RDBMSに関する知識はほとんどありません。


注:パラメーター化/オーバーロードを使用しました非推奨日付コンストラクター-Date(String s)すべての場合にSQLクエリに関連付けられたインデックス付き/位置パラメータ用。純粋なテスト目的で、Java.util.SimpleDateFormatノイズの混乱を回避するためだけです。 Joda Time(Hibernateでサポートされている)、Java.sql.*Java.util.Dateのサブクラス)、Java Time in = Java 8(カスタマイズされていない限り、現時点ではほとんどサポートされていません)必要に応じて/必要に応じて。

お役に立てば幸いです。

33
Tiny

この形式を使用すると、より高速に実行されます。

SELECT c.*
    FROM cars c
    LEFT JOIN bookings b 
             ON b.fkCarId = c.id
            AND (b.fromDate ... )
    WHERE b.fkCarId IS NULL;

このフォームは、すべてのcarsをスキャンしてからbookingsに1回アクセスする必要があるため、まだあまり効率的ではありません。

fkCarIdにインデックスが必要です。 「fk」はFOREIGN KEY、これはインデックスを意味します。どうか提供してください SHOW CREATE TABLE 確認のため。

CriteriaBuilderがそれを構築できない場合、それらに文句を言うか、邪魔にならないようにしてください。

might高速で実行する:

SELECT c.*
    FROM bookings b 
    JOIN cars c
           ON b.fkCarId = c.id
    WHERE NOT (b.fromDate ... );

この定式化では、bookingsでテーブルスキャンを行い、予約済みの文字をフィルタリングして、dが目的の行のcarsに到達することを望んでいます。使用可能な車が非常に少ない場合、これは特に高速です。

2
Rick James