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?
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 cars
. Je 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.
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é.
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.