2015-04-18 10 views
7

Ho bisogno di adattare il seguente esempio di codice.Combinazione di espressioni condizionali con predicati "AND" e "OR" utilizzando l'API dei criteri JPA

Ho una query MySQL, che assomiglia a questo (2015/05/04 e 2015/05/06 sono dinamici e simboleggiano un intervallo di tempo)

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

Ho una tabella di bookings e una tabella cars. Mi piacerebbe scoprire quale macchina è disponibile in un intervallo di tempo. La query SQL funziona come un fascino.

Mi piacerebbe "convertire" questo in un output CriteriaBuilder. Ho letto la documentazione durante le ultime 3 ore con questo output (che, ovviamente, non funziona). E ho persino saltato le parti in cui sono presenti le sottoceramiche.

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

Un altro problema: il fkCarId non è definito come chiave esterna, è solo un numero intero. Qualche modo per farlo riparare in questo modo?

+0

Una cosa da notare è che le condizioni della data si riassumono in "fromDate> = '2015-05-04' AND toDate <= '2015-05-06''. – RealSkeptic

+0

Grazie, lo modificherò. Dovrebbe essere un intervallo di tempo dinamico. Se un'auto è prenotata dal 2015-05-05 al 2015-05-07, non si dovrebbe essere in grado di prenotarla, quando la data di inizio, o la data di fine, o entrambi sono in conflitto con una prenotazione esistente. –

risposta

16

Ho creato le seguenti due tabelle nel database MySQL con solo i campi necessari.

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 nella tabella bookings è una chiave primaria e fk_car_id è una chiave esterna che fa riferimento alla chiave primaria (car_id) della tabella cars.


La corrispondente all'APP interrogazione selezione utilizzando un IN() sub-query va come il seguente.

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

Produce la seguente query SQL di tuo interesse (testato su Hibernate 4.3.6 finale, ma non ci dovrebbe essere alcuna differenza sui quadri medi ORM in questo contesto).

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

parentesi intorno alle espressioni condizionali nella clausola WHERE della query sopra sono tecnicamente del tutto superfluo che sono necessari solo per una migliore precisione di lettura, che non tiene conto Hibernate - Hibernate non deve prenderle in considerazione.


Personalmente però, preferiscono utilizzare l'operatore EXISTS. Di conseguenza, la query può essere ricostruita come segue.

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

Produce la seguente query 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)) 

Che restituisce la stessa lista di risultati.


aggiuntive:

Qui subquery.select(criteriaBuilder.literal(1L));, durante l'utilizzo di espressioni come criteriaBuilder.literal(1L) nelle dichiarazioni sub-query complessa su EclipseLink, EclipseLink gets confused e provoca un'eccezione. Pertanto, potrebbe essere necessario prendere in considerazione durante la scrittura di sottoquery complesse su EclipseLink. Basta selezionare un id in questo caso, come

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

come nel primo caso. Nota: verrà visualizzato un odd behaviour nella generazione di query SQL, se si esegue un'espressione come sopra in EclipseLink anche se l'elenco dei risultati sarà identico.

Si può anche utilizzare unisce, che risultano essere più efficace su sistemi di database back-end in questo caso, è necessario utilizzare DISTINCT per filtrare le eventuali righe duplicate, in quanto è necessario un elenco di risultati dalla tabella padre. L'elenco dei risultati può contenere righe duplicate, se esiste più di una riga secondaria nella tabella dettagliata - bookings per una riga parent corrispondente cars. Lo lascio a voi. :) Ecco come va qui.

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

Produce la seguente query 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 

Come si può notare il provider Hibernate inverte le istruzioni condizionali nella clausola WHERE in risposta a WHERE NOT(...). Altri provider potrebbero anche generare l'esatto WHERE NOT(...) ma, in definitiva, è uguale a quello scritto nella domanda e produce lo stesso elenco di risultati dei casi precedenti.

I join diritti non sono specificati. Quindi, i fornitori di JPA non devono implementarli. La maggior parte di loro non supporta i giusti join.


rispettivi JPQL solo per ragioni di completezza :)

Il IN() query:

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

Il EXISTS() query:

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

L'ultimo che utilizza il left join (con parametri denominati):

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 

Tutti i precedenti comandi JPQL possono essere eseguiti utilizzando il metodo seguente, come già sapete.

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

Sostituire i parametri denominati con i corrispondenti parametri indicizzati/posizionali come e quando necessario/richiesto.

Tutte queste istruzioni JPQL generano anche le istruzioni SQL identiche a quelle generate dall'API dei criteri come sopra.


  • vorrei sempre evitare IN() sub-query in tali situazioni e soprattutto mentre usando MySQL. Vorrei usare IN() sub-query se e solo se sono strettamente necessarie alle situazioni come quando abbiamo bisogno di determinare il risultato impostare o eliminare un elenco di righe sulla base di un elenco di valori statici, come

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

    e simili.

  • Preferisco sempre le query utilizzando l'operatore EXISTS in tali situazioni, poiché l'elenco dei risultati coinvolge solo una tabella basata su una condizione in un'altra/e tabella/i. Join in questo caso produrrà righe duplicate come menzionato in precedenza che devono essere filtrate utilizzando DISTINCT come mostrato in una delle query sopra.

  • Preferisco utilizzare join, quando il set di risultati da recuperare è combinato da più tabelle di database - deve essere comunque utilizzato come ovvio.

Dopo tutto, tutto dipende da molte cose. Quelle non sono affatto pietre miliari.

Disclaimer: Ho una conoscenza molto scarsa su RDBMS.


Nota: ho usato parametrizzata/overload obsoleto data costruttore - Date(String s) per parametri indicizzati/posizionale associati con la query SQL in tutti i casi per scopi di test puro solo per evitare la tutta la confusione del rumore java.util.SimpleDateFormat che già conosci.Puoi anche utilizzare altre API migliori come Joda Time (Hibernate ha il supporto per esso), java.sql.* (quelle sono sottoclassi di java.util.Date), Java Time in Java 8 (per lo più non supportate fino a ora se non personalizzato) come e quando richiesto/necessario .

Spero che questo aiuti.

+0

Wow, questa è senza dubbio la risposta migliore e più dettagliata che abbia mai avuto su internet. Grazie mille! Mi ha aiutato molto –

+2

Mi interessava anche il completamento della risposta :) Grazie. @StevenX – Tiny

2

Sarà correre più veloce se si fa questo formato:

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

Questa forma ancora non sarà molto efficiente, in quanto dovrà eseguire la scansione di tutti i cars, per poi raggiungere in bookings una volta al.

È necessario un indice su fkCarId. "fk" odora come se fosse un FOREIGN KEY, che implica un indice. Si prega di fornire SHOW CREATE TABLE per conferma.

Se CriteriaBuilder non è in grado di costruirlo, lamentarsi o togliersi di mezzo.

Lanciando attorno forze correre più veloce:

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

In questa formulazione, spero di fare una scansione di tabella sulla bookings, filtrando i caratteri riservati, uno d ​​solo allora raggiungere in cars per la righe desiderate Questo potrebbe essere particolarmente veloce se ci sono poche macchine disponibili.

Problemi correlati