简体   繁体   English

使用JPA条件API将条件表达式与“AND”和“OR”谓词组合在一起

[英]Combining conditional expressions with “AND” and “OR” predicates using the JPA criteria API

I need to adapt the following code example. 我需要调整以下代码示例。

I've got a MySQL query, which looks like this (2015-05-04 and 2015-05-06 are dynamic and symbolize a time range) 我有一个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'))

I've got a bookings table, and a cars table. 我有一张bookings桌和一张cars桌。 I'd like to find out which car is available in a time range. 我想知道哪个车在一个时间范围内可用。 The SQL query works like a charm. SQL查询就像一个魅力。

I'd like to "convert" this one into a CriteriaBuilder output. 我想将这个“转换”为CriteriaBuilder输出。 I've read documentation during the last 3 hours with this output (which, obviously, does not work). 我在过去3个小时内阅读了这个输出的文档(显然,这不起作用)。 And I even skipped the where parts in the sub queries. 我甚至跳过了子查询中的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();

Another issue: the fkCarId is not defined as a foreign key, it's just an integer. 另一个问题: fkCarId没有被定义为外键,它只是一个整数。 Any way to get it fixed that way? 有什么方法可以解决这个问题吗?

I have created the following two tables in MySQL database with only the necessary fields. 我在MySQL数据库中创建了以下两个表,只有必要的字段。

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 in the bookings table is a primary key and fk_car_id is a foreign key that references the primary key ( car_id ) of the cars table. booking_idbookings表是一个主键, fk_car_id是一个外键引用主键( car_id的)的cars表。


The corresponding JPA criteria query using an IN() sub-query goes like the following. 使用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();

It produces the following SQL query of your interest (tested on Hibernate 4.3.6 final but there should not be any discrepancy on average ORM frameworks in this context). 它会产生您感兴趣的以下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<=?
    )

Brackets around the conditional expressions in the WHERE clause of the above query are technically utterly superfluous which are only needed for better a readability which Hibernate disregards - Hibernate does not have to take them into consideration. 上述查询的WHERE子句中的条件表达式周围的括号在技术上完全是多余的,只有Hibernate忽略的更好的可读性才需要--Hibernate不必考虑它们。


I personally however, prefer to use the EXISTS operator. 但我个人更喜欢使用EXISTS运算符。 Accordingly, the query can be reconstructed as follows. 因此,可以如下重建查询。

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

It produces the following SQL 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))

Which returns the same result list. 返回相同的结果列表。


Additional: 额外:

Here subquery.select(criteriaBuilder.literal(1L)); 这里是subquery.select(criteriaBuilder.literal(1L)); , while using expressions like criteriaBuilder.literal(1L) in complex sub-query statements on EclipseLink, EclipseLink gets confused and causes an exception. ,在EclipseLink上的复杂子查询语句中使用criteriaBuilder.literal(1L)等表达式时,EclipseLink 会混淆并导致异常。 Therefore, it may need to be taken into account while writing complex sub-queries on EclipseLink. 因此,在EclipseLink上编写复杂的子查询时可能需要考虑它。 Just select an id in that case such as 只需在这种情况下选择一个id ,例如

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

as in the first case. 和第一种情况一样。 Note : You will see an odd behaviour in SQL query generation, if you run an expression as above on EclipseLink though the result list will be identical. 注意:如果在EclipseLink上运行如上所述的表达式,您将在SQL查询生成中看到奇怪的行为 ,尽管结果列表将是相同的。

You may also use joins which turn out to be more efficient on back-end database systems in which case, you need to use DISTINCT to filter out possible duplicate rows, since you need a result list from the parent table. 您还可以使用在后端数据库系统上更高效的连接,在这种情况下,您需要使用DISTINCT来过滤掉可能的重复行,因为您需要父表中的结果列表。 The result list may contain duplicate rows, if there exists more than one child row in the detailed table - bookings for a corresponding parent row cars . 结果列表可以包含重复行,如果存在在详细表中的多个子行- bookings的相应父行cars I am leaving it to you. 我把它留给你。 :) This is how it goes here. :)这就是它的方式。

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

It produces the following SQL 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

As can be noticed the Hibernate provider inverses the conditional statements in the WHERE clause in response to WHERE NOT(...) . 可以注意到,Hibernate提供程序反转WHERE子句中的条件语句以响应WHERE NOT(...) Other providers may also generate the exact WHERE NOT(...) but after all, this is the same as the one written in the question and yields the same result list as in the previous cases. 其他提供者也可以生成确切的WHERE NOT(...)但是毕竟,这与问题中写的相同,并产生与前面的情况相同的结果列表。

Right joins are not specified. 未指定右连接。 Hence, JPA providers do not have to implement them. 因此,JPA提供者不必实现它们。 Most of them do not support right joins. 他们中的大多数不支持正确的连接。


Respective JPQL just for the sake of completeness :) 相应的JPQL只是为了完整性:)

The IN() query : 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 <=? ) 

The EXISTS() query : 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) ) 

The last one that uses the left join (with named parameters): 最后一个使用左连接(带有命名参数):

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

All of the above JPQL statements can be run using the following method as you already know. 如您所知,所有上述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();

Replace named parameters with corresponding indexed/positional parameters as and when needed/required. 在需要/需要时,使用相应的索引/位置参数替换命名参数。

All of these JPQL statements also generate the identical SQL statements as those generated by the criteria API as above. 所有这些JPQL语句也生成与上述条件API生成的SQL语句相同的SQL语句。


  • I would always avoid IN() sub-queries in such situations and especially while using MySQL. 在这种情况下,特别是在使用MySQL时,我总是会避免使用IN()子查询。 I would use IN() sub-queries if and only if they are absolutely needed for situations such as when we need to determine a result set or delete a list of rows based on a list of static values such as 我会使用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); 

    and alike. 和我一样。

  • I would always prefer queries using the EXISTS operator in such situations, since the result list involves only a single table based on a condition in another table(s). 在这种情况下,我总是更喜欢使用EXISTS运算符的查询,因为结果列表仅涉及基于另一个表中的条件的单个表。 Joins in this case will produce duplicate rows as mentioned earlier that need to be filtered out using DISTINCT as shown in one of the queries above. 在这种情况下,联接将产生前面提到的重复行,需要使用DISTINCT过滤掉,如上面的一个查询中所示。

  • I would preferably use joins, when the result set to be retrieved is combined from multiple database tables - have to be used anyway as obvious. 我最好使用连接,当要检索的结果集从多个数据库表中组合时 - 必须使用,无论如何都必须使用。

Everything is dependent upon many things after all. 毕竟,一切都取决于许多事情。 Those are not milestones at all. 这些都不是里程碑。

Disclaimer : I have a very little knowledge on RDBMS. 免责声明:我对RDBMS知之甚少。


Note : I have used the parameterized/overloaded deprecated date constructor - Date(String s) for indexed/positional parameters associated with the SQL query in all the cases for a pure testing purpose only to avoid the whole mess of java.util.SimpleDateFormat noise which you already know. 注意:我已经在所有情况下使用参数化/重载的弃用日期构造函数 - Date(String s)用于与SQL查询相关联的索引/位置参数,仅用于纯测试目​​的,以避免整个混乱的java.util.SimpleDateFormat噪声你已经知道了。 You may also use other better APIs like Joda Time (Hibernate has support for it), java.sql.* (those are sub-classes of java.util.Date ), Java Time in Java 8 (mostly not supported as of now unless customized) as and when required/needed. 你也可以使用其他更好的API,比如Joda Time(Hibernate支持它), java.sql.* (那些是java.util.Date子类),Java 8中的Java Time(现在大多不支持)根据需要/需要定制)。

Hope that helps. 希望有所帮助。

It will run faster if you do this format: 如果您执行以下格式,它将运行得更快:

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

This form still won't be very efficient since it will have to scan all cars , then reach into bookings once per. 这种形式仍然不会非常有效,因为它必须扫描所有cars ,然后每次进入bookings

You do need an index on fkCarId . 你确实需要fkCarId的索引。 "fk" smells like it is a FOREIGN KEY , which implies an index. “fk”闻起来像是一个外FOREIGN KEY ,这意味着一个索引。 Please provide SHOW CREATE TABLE for confirmation. 请提供SHOW CREATE TABLE进行确认。

If CriteriaBuilder can't build that, complain to them or get it out of your way. 如果CriteriaBuilder无法构建它,请向他们抱怨或将其排除在外。

Flipping it around might run faster: 翻转它可能会更快:

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

In this formulation, I am hoping to do a table scan on bookings , filtering out the reserved chars, an d only then reach into cars for the desired rows. 在这个表述中,我希望对bookings进行表格扫描,过滤掉保留的字符,然后才能进入cars以获得所需的行。 This might be especially faster if there are very few available cars. 如果可用的汽车很少,这可能会更快。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM