【问题标题】:Combining conditional expressions with "AND" and "OR" predicates using the JPA criteria API使用 JPA 条件 API 将条件表达式与“AND”和“OR”谓词组合
【发布时间】:2015-04-18 15:31:33
【问题描述】:

我需要修改以下代码示例。

我有一个 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'))

我有一个bookings 表和一个cars 表。我想知道在某个时间范围内有哪辆车可用。 SQL 查询就像一个魅力。

我想将此“转换”为CriteriaBuilder 输出。在过去的 3 个小时里,我用这个输出阅读了文档(显然,这不起作用)。我什至跳过了子查询中的 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();

另一个问题:fkCarId 没有定义为外键,它只是一个整数。有什么办法可以解决这个问题?

【问题讨论】:

  • 需要注意的是,您的日期条件总和为fromDate &gt;= '2015-05-04' AND toDate &lt;= '2015-05-06'
  • 谢谢,我会编辑的。它应该是一个动态的时间范围。如果汽车是在 2015 年 5 月 5 日至 2015 年 5 月 7 日期间预订的,则当开始日期或结束日期或两者与现有预订发生冲突时,应该无法预订。

标签: java mysql jpa criteria


【解决方案1】:

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

bookings 表中的booking_id 是主键,fk_car_id 是引用cars 表的主键 (car_id) 的外键。


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

它会生成您感兴趣的以下 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<=?
    )

上述查询的WHERE 子句中的条件表达式周围的括号在技术上是完全多余的,它们只是为了更好的可读性而需要Hibernate 忽略 - Hibernate 不必考虑它们。


然而,我个人更喜欢使用EXISTS 运算符。因此,查询可以重构如下。

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

它产生以下 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))

返回相同的结果列表。


补充:

这里是subquery.select(criteriaBuilder.literal(1L));,而在EclipseLink 上的复杂子查询语句中使用criteriaBuilder.literal(1L) 之类的表达式时,EclipseLink gets confused 会导致异常。因此,在 EclipseLink 上编写复杂的子查询时可能需要考虑到这一点。在这种情况下只需选择一个id,例如

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

与第一种情况一样。注意:如果您在 EclipseLink 上运行上述表达式,尽管结果列表将相同,但您将在 SQL 查询生成中看到 odd behaviour

您也可以使用在后端数据库系统上更高效的连接,在这种情况下,您需要使用DISTINCT 过滤掉可能的重复行,因为您需要父表中的结果列表。如果详细表中存在多个子行,则结果列表可能包含重复行 - bookings 对应父行 cars我把它留给你。 :) 这里是这样的。

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

它产生以下 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

可以注意到,Hibernate 提供程序反转了WHERE 子句中的条件语句以响应WHERE NOT(...)。其他提供者也可能会生成确切的WHERE NOT(...),但毕竟这与问题中所写的相同,并且生成的结果列表与之前的案例相同。

未指定右连接。因此,JPA 提供者不必实现它们。他们中的大多数不支持右连接。


为了完整起见,各自的 JPQL :)

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

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

最后一个使用左连接(带有命名参数):

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

如您所知,上述所有 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();

在需要/需要时将命名参数替换为相应的索引/位置参数。

所有这些 JPQL 语句还生成与上述条件 API 生成的 SQL 语句相同的 SQL 语句。


  • 在这种情况下我总是会避免 IN() 子查询,尤其是在 使用 MySQL。我会使用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);
    

    等等。

  • 在这种情况下,我总是更喜欢使用 EXISTS 运算符的查询,因为结果列表涉及 仅基于另一个表中的条件的单个表。加入 在这种情况下会产生前面提到的需要过滤掉的重复行 使用DISTINCT,如上述查询之一所示。

  • 当要检索的结果集是 由多个数据库表组合而成 - 无论如何都必须使用。

毕竟,一切都取决于许多事情。这些根本不是里程碑。

免责声明:我对 RDBMS 知之甚少。


注意:我在所有情况下都使用了参数化/重载的不推荐使用日期构造函数 - Date(String s) 用于与 SQL 查询关联的索引/位置参数出于纯粹的测试目的,只是为了避免您已经知道的java.util.SimpleDateFormat 噪音的全部混乱。您还可以使用其他更好的 API,例如 Joda Time(Hibernate 支持它)、java.sql.*(这些是 java.util.Date 的子类)、Java 8 中的 Java Time(除非定制,否则目前大部分不支持)和需要/需要时。

希望对您有所帮助。

【讨论】:

  • 哇,这无疑是我在互联网上得到的最好和最详细的答案。非常感谢!真的帮了我很多。
  • 我也有兴趣完成答案:) 谢谢。 @StevenX
  • 这是一个很好的答案。解决了很多问题
【解决方案2】:

如果你这样做,它会运行得更快:

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

这个表单仍然不是很有效,因为它必须扫描所有cars,然后每次到达bookings

您确实需要fkCarId 的索引。 “fk” 闻起来像是一个FOREIGN KEY,这意味着一个索引。请提供SHOW CREATE TABLE进行确认。

如果 CriteriaBuilder 无法构建它,请向他们投诉或解决它。

翻转它可能跑得更快:

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

在这个公式中,我希望对 bookings 进行表扫描,过滤掉保留的字符,然后 d 才到达 cars 以获得所需的行。如果可用的汽车很少,这可能会特别快。

【讨论】:

    猜你喜欢
    • 2018-11-22
    • 2017-03-28
    • 2015-02-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-24
    • 1970-01-01
    相关资源
    最近更新 更多