web-dev-qa-db-fra.com

Combinaison d'expressions conditionnelles avec des prédicats "AND" et "OR" à l'aide de l'API de critères JPA

J'ai besoin d'adapter l'exemple de code suivant.

J'ai une requête MySQL, qui ressemble à ceci (2015-05-04 et 2015-05-06 sont dynamiques et symbolisent une plage de temps)

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'))

J'ai une table bookings et une table cars. J'aimerais savoir quelle voiture est disponible dans un intervalle de temps. La requête SQL fonctionne comme un charme.

Je voudrais "convertir" celui-ci en une sortie CriteriaBuilder. J'ai lu la documentation au cours des 3 dernières heures avec cette sortie (qui, évidemment, ne fonctionne pas). Et j'ai même ignoré les parties Where dans les sous-requêtes.

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();

Autre problème: le fkCarId n'est pas défini comme une clé étrangère, c'est juste un entier. Est-il possible de le réparer de cette façon?

19
Steven X

J'ai créé les deux tables suivantes dans la base de données MySQL avec uniquement les champs nécessaires.

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)

booking_id Dans la table bookings est une clé primaire et fk_car_id Est une clé étrangère qui fait référence à la clé primaire (car_id) De cars table.


La requête de critères JPA correspondante utilisant une sous-requête IN() se présente comme suit.

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();

Il produit la requête SQL suivante de votre intérêt (testée sur Hibernate 4.3.6 final mais il ne devrait pas y avoir de divergence sur les cadres ORM moyens dans ce contexte).

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<=?
    )

Les crochets autour des expressions conditionnelles dans la clause WHERE de la requête ci-dessus sont techniquement tout à fait superflus qui ne sont nécessaires que pour une meilleure lisibilité qu'Hibernate ne tient pas compte - Hibernate n'a pas à les prendre en considération.


Personnellement, cependant, je préfère utiliser l'opérateur EXISTS. Par conséquent, la requête peut être reconstruite comme suit.

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();

Il produit la requête SQL suivante.

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))

Ce qui renvoie la même liste de résultats.


Supplémentaire:

Ici subquery.select(criteriaBuilder.literal(1L));, tout en utilisant des expressions comme criteriaBuilder.literal(1L) dans des instructions de sous-requête complexes sur EclipseLink, EclipseLink devient confus et provoque une exception. Par conséquent, il peut être nécessaire de le prendre en compte lors de l'écriture de sous-requêtes complexes sur EclipseLink. Sélectionnez simplement un id dans ce cas, tel que

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

comme dans le premier cas. Remarque: Vous verrez un comportement étrange dans la génération de requête SQL, si vous exécutez une expression comme ci-dessus sur EclipseLink bien que la liste de résultats soit identique.

Vous pouvez également utiliser des jointures qui s'avèrent plus efficaces sur les systèmes de base de données principaux.Dans ce cas, vous devez utiliser DISTINCT pour filtrer les lignes en double possibles, car vous avez besoin d'une liste de résultats de la table parent. La liste de résultats peut contenir des lignes en double, s'il existe plusieurs lignes enfants dans le tableau détaillé - bookings pour une ligne parent correspondante carsJe vous le laisse. :) C'est comme ça que ça se passe ici.

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();

Il produit la requête SQL suivante.

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

Comme on peut le remarquer, le fournisseur Hibernate inverse les instructions conditionnelles dans la clause WHERE en réponse à WHERE NOT(...). D'autres fournisseurs peuvent également générer la WHERE NOT(...) exacte mais après tout, c'est la même que celle écrite dans la question et donne la même liste de résultats que dans les cas précédents.

Les jointures droites ne sont pas spécifiées. Par conséquent, les fournisseurs JPA n'ont pas à les mettre en œuvre. La plupart d'entre eux ne prennent pas en charge les jointures droites.


JPQL respectif juste pour être complet :)

La requête 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 <=? ) 

La requête 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) ) 

Le dernier qui utilise la jointure gauche (avec des paramètres nommés):

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

Toutes les instructions JPQL ci-dessus peuvent être exécutées en utilisant la méthode suivante comme vous le savez déjà.

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();

Remplacez les paramètres nommés par des paramètres indexés/positionnels correspondants au fur et à mesure des besoins/des besoins.

Toutes ces instructions JPQL génèrent également les instructions SQL identiques à celles générées par l'API de critères comme ci-dessus.


  • J'éviterais toujours les sous-requêtes IN() dans de telles situations et surtout lors de l'utilisation de MySQL. J'utiliserais les sous-requêtes IN() si et seulement si elles sont absolument nécessaires pour des situations telles que lorsque nous devons déterminer un jeu de résultats ou supprimer une liste de lignes basée sur une liste de valeurs statiques telles que

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

    et pareil.

  • Je préfère toujours les requêtes utilisant l'opérateur EXISTS dans de telles situations, car la liste de résultats n'implique qu'une seule table basée sur une condition dans une autre table (s). Les jointures dans ce cas produiront des lignes en double comme mentionné précédemment qui doivent être filtrées à l'aide de DISTINCT comme indiqué dans l'une des requêtes ci-dessus.

  • J'utiliserais de préférence des jointures, lorsque l'ensemble de résultats à récupérer est combiné à partir de plusieurs tables de base de données - doivent être utilisés de toute façon comme évident.

Après tout, tout dépend de beaucoup de choses. Ce ne sont pas du tout des jalons.

Clause de non-responsabilité: J'ai très peu de connaissances sur le SGBDR.


Remarque: J'ai utilisé le constructeur de date paramétré/surchargé déconseillé - Date(String s) pour les paramètres indexés/positionnels associés à la requête SQL dans tous les cas à des fins de test pur uniquement pour éviter tout le désordre de bruit Java.util.SimpleDateFormat que vous connaissez déjà. Vous pouvez également utiliser d'autres meilleures API comme Joda Time (Hibernate le prend en charge), Java.sql.* (Ce sont des sous-classes de Java.util.Date), Java Time in Java 8 (la plupart du temps non pris en charge à moins qu'il ne soit personnalisé) au fur et à mesure des besoins/besoins.

J'espère que ça t'as aidé.

33
Tiny

Il fonctionnera plus rapidement si vous faites ce format:

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

Ce formulaire ne sera toujours pas très efficace car il devra scanner tout cars, puis atteindre bookings une fois par.

Vous avez besoin d'un index sur fkCarId. "fk" sent comme si c'était un FOREIGN KEY, ce qui implique un index. Veuillez fournir SHOW CREATE TABLE pour confirmation.

Si CriteriaBuilder ne peut pas construire cela, faites-leur une plainte ou faites-le sortir de votre chemin.

Le retourner pourrait courir plus vite:

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

Dans cette formulation, j'espère faire un scan de table sur bookings, en filtrant les caractères réservés, et puis seulement atteindre cars pour les lignes désirées. Cela pourrait être particulièrement rapide s'il y a très peu de voitures disponibles.

2
Rick James