2015-04-18 19 views
7

Potrzebuję dostosować następujący przykład kodu.Łączenie wyrażeń warunkowych z predykatami "AND" i "OR" przy użyciu interfejsu API JPA

Mam kwerendy MySQL, który wygląda następująco (04.05.2015 i 06.05.2015 są dynamiczne i symbolizują zakres czasu)

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

Mam bookings stół i tabelę cars. Chciałbym się dowiedzieć, który samochód jest dostępny w danym przedziale czasowym. Zapytanie SQL działa jak urok.

Chciałbym "przekonwertować" ten na wyjście CriteriaBuilder. Czytałem dokumentację w ciągu ostatnich 3 godzin z tym wynikiem (co oczywiście nie działa). I nawet pominąłem fragmenty w podrzędnych pytaniach.

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

Kolejny problem: fkCarId nie jest zdefiniowany jako klucz obcy, to jest po prostu liczbą całkowitą. Jakikolwiek sposób, aby to naprawić w ten sposób?

+0

Należy zwrócić uwagę na to, że twoje warunki datowe są sumowane do "fromDate> = '2015-05-04' AND toDate <= '2015-05-06''. – RealSkeptic

+0

Dzięki, będę go edytować. Powinien to być dynamiczny zakres czasu. Jeśli samochód jest rezerwowany od 2015-05-05 do 2015-05-07, nie należy rezerwować go, gdy data rozpoczęcia, data zakończenia lub oba są sprzeczne z istniejącą rezerwacją. –

Odpowiedz

16

Utworzyłem następujące dwie tabele w bazie danych MySQL z tylko niezbędnymi polami.

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 w tabeli bookings jest kluczem podstawowym i fk_car_id jest klucz obcy, która odwołuje się do klucza podstawowego (car_id) tabeli cars.


Odpowiednie JPA kryteria zapytania przy użyciu IN() sub-zapytanie idzie jak poniżej.

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

Produkuje następujące zapytanie SQL zainteresowanie (testowane na Hibernate 4.3.6 final, ale nie powinno być żadnych rozbieżności średnio ram ORM w tym kontekście).

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

nawiasy wokół wyrażeń warunkowych w klauzuli powyższego zapytania WHERE są technicznie całkowicie zbędne, które są potrzebne tylko dla lepszej dokładności odczytu, który zignoruje Hibernate - Hibernate nie musi brać je pod uwagę.


Osobiście jednak wolę używać operatora EXISTS. W związku z tym kwerendę można zrekonstruować w następujący sposób.

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

Generuje następujące zapytanie 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)) 

Która zwraca tę samą listę wyników.


dodatkowe:

tutaj subquery.select(criteriaBuilder.literal(1L));, przy użyciu wyrażeń takich jak criteriaBuilder.literal(1L) w złożonych sprawozdania sub-zapytanie o EclipseLink, EclipseLink gets confused i powoduje wyjątek. Dlatego może być konieczne wzięcie pod uwagę podczas pisania złożonych pod-zapytań w EclipseLink. Wystarczy wybrać id w tym przypadku jak

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

jak w pierwszym przypadku. Uwaga: Pojawi się odd behaviour w generowaniu zapytań SQL, jeśli uruchomisz wyrażenie jak powyżej na EclipseLink, chociaż lista wyników będzie identyczna.

Możesz również użyć sprzężeń, które okazują się bardziej wydajne w systemach baz danych z zaplecza. W takim przypadku musisz użyć DISTINCT, aby odfiltrować możliwe duplikaty wierszy, ponieważ potrzebujesz listy wyników z tabeli nadrzędnej. Lista wyników może zawierać zduplikowane wiersze, jeśli istnieje więcej niż jeden wiersz podrzędny w szczegółowej tabeli - bookings dla odpowiedniego wiersza nadrzędnego cars. Zostawiam to Tobie. :) Tak to się dzieje tutaj.

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

Generuje następujące zapytanie 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 

Jak można zauważyć dostawca Hibernacja odwraca warunkowe oświadczeń w klauzuli w odpowiedzi na WHERE NOT(...)WHERE. Inni dostawcy mogą również wygenerować dokładną wartość: WHERE NOT(...), ale ostatecznie jest to ta sama, co w pytaniu i daje taką samą listę wyników, jak w poprzednich przypadkach.

Prawe sprzężenia nie są określone. Dlatego dostawcy JPA nie muszą ich wdrażać. Większość z nich nie obsługuje właściwych połączeń.


odpowiednich JPQL tylko przez wzgląd na kompletność :)

IN() zapytania:

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() zapytania:

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

Ostatnim, który używa left join (z nazwanymi parametrami):

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 

Wszystkie powyższe instrukcje JPQL można uruchomić przy użyciu poniższej metody, jak już wiesz.

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

Wymień nazwane parametry na odpowiednie parametry indeksowane/pozycyjne w razie potrzeby/wymagane.

Wszystkie te instrukcje JPQL generują również identyczne instrukcje SQL, jak wygenerowane przez API kryteriów, jak wyżej.


  • ja zawsze unikać IN() sub-zapytań w takich sytuacjach i szczególnie podczas MySQL. użyłbym IN() sub-zapytań wtedy i tylko wtedy, gdy są one absolutnie potrzebne w sytuacjach takich jak, kiedy musimy określić wynik ustawić lub usunąć listę wierszy w oparciu o listy wartości statycznych takich jak

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

    i podobne.

  • W takich sytuacjach wolałbym zawsze używać zapytań z użyciem operatora EXISTS, ponieważ lista wyników obejmuje tylko jedną tabelę na podstawie warunku w innej tabeli (tabelach). Łączy się z w tym przypadku będzie produkować zduplikowane wiersze, jak wspomniano wcześniej, które należy odfiltrować przy użyciu DISTINCT, jak pokazano w jednym z powyższych zapytań.

  • Preferowałbym użycie złącz, gdy zestaw wyników do pobrania jest połączony z wielu tabel bazy danych - muszą być używane tak czy inaczej oczywiste.

Wszystko zależy od wielu rzeczy. To wcale nie są kamienie milowe.

Oświadczenie: Mam bardzo małą wiedzę na temat RDBMS.


Uwaga: Użyłem parametryczne/przeciążony przestarzałe datę konstruktora - Date(String s) dla indeksowanych/pozycyjny parametrów związanych z kwerendy SQL we wszystkich przypadkach do czystego celu testowania tylko uniknąć cały bałagan java.util.SimpleDateFormat hałasu, który już znasz.Możesz również użyć innych lepszych interfejsów API, takich jak Czas Jody (Hibernate obsługuje go), java.sql.* (są to podklasy java.util.Date), Czas Java w Javie 8 (w większości przypadków nie jest obsługiwany, chyba że dostosowany) w razie potrzeby/potrzeby .

Nadzieję, że pomaga.

+0

Wow, to bez wątpienia najlepsza i najbardziej szczegółowa odpowiedź, jaką kiedykolwiek miałem w Internecie. Dzięki wielkie! Pomógł mi bardzo dużo. –

+2

Byłem także zainteresowany uzupełnieniem odpowiedzi :) Dziękuję. @StevenX – Tiny

2

będzie działać szybciej, jeśli nie ten format:

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

Ta forma nadal nie będzie bardzo wydajny, ponieważ będzie musiał skanować wszystkie cars, a następnie dotrzeć do bookings raz za.

Potrzebujesz indeksu na fkCarId. "fk" pachnie jak to jest FOREIGN KEY, co implikuje indeks. Podaj nr SHOW CREATE TABLE w celu potwierdzenia.

Jeśli CriteriaBuilder nie może tego zbudować, złożyć skargę lub usunąć ją z drogi.

Rzut wokół potęga biegać szybciej:

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

W tym preparacie, mam nadzieję zrobić skan tabeli na bookings, odfiltrowanie zastrzeżone znaki, z d tylko wtedy dotrzeć do cars dla osób żądane wiersze. Może to być szczególnie szybsze, jeśli dostępnych jest niewiele samochodów.