【问题标题】:JPA Criteria for MAX id GROUP BY FOREIGN KEY in a ManyToMany relationship多对多关系中 MAX id GROUP BY FOREIGN KEY 的 JPA 标准
【发布时间】:2021-01-06 19:24:30
【问题描述】:

我正在使用Spring BootSpring Data JPA 开发一个应用程序。

以下是相关实体:

@Entity
public class Certification {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "certification_type_id")
    private CertificationType certificationType;
    @OneToMany(fetch = FetchType.EAGER, mappedBy = "certification", cascade = CascadeType.ALL)
    private Set<CertificationStatus> certificationStatuses;
}
@Entity
public class CertificationType {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @Enumerated(EnumType.STRING)
    private Code code;
    private String name;
    private String description;
    ...
    public enum Code {
        ...
    }
}

@Entity
public class CertificationStatus {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @ManyToOne
    @JoinColumn(name = "certification_id")
    private Certification certification;
    @ManyToOne
    @JoinColumn(name = "certification_status_type_id")
    private CertificationStatusType certificationStatusType;
    private String remarks;
}

@Entity
public class CertificationStatusType {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @Enumerated(EnumType.STRING)
    private Code code;
    private String name;
    private String description;
    @OneToMany(fetch = FetchType.EAGER, mappedBy = "certificationStatusType", cascade = CascadeType.ALL)
    private Set<CertificationStatus> certificationStatuses;
    
    public enum Code {
        ...
    }
}

在我的服务中,我有一个方法,CertificationService.findAll()。这个想法是检索certification 记录,其最新的certification_status 基于MAX(id)GROUP BY certification_idWHERE certification_status_type_id = ?。此方法有五 (5) 个参数:

Integer certificationTypeId,
Integer certificationStatusTypeId,
LocalDate dateFrom,
LocalDate dateTo,
String client

组合可以是零 (0) 到五 (5)。因此,我选择应用 JPA Criteria。到目前为止,我已经编写了以下代码:

@Override
public List<CertificationDto> findAll(
        Integer certificationTypeId,
        Integer certificationStatusTypeId,
        LocalDate dateFrom,
        LocalDate dateTo,
        String client) {
            
    var certifications = this.certificationRepository.findAll((root, criteriaQuery, criteriaBuilder) -> {
        var predicates = new ArrayList<Predicate>();
        // This is working
        if (certificationTypeId != null && certificationTypeId > 0) {
            predicates.add(criteriaBuilder.and(criteriaBuilder.equal(root.join(Certification_.certificationType, JoinType.INNER), certificationTypeId)));
        }

        // To do
        if (certificationStatusTypeId != null && certificationStatusTypeId > 0) {
            /*
             * I have no idea how to apply my SQL statement:
             * SELECT * FROM certification_status WHERE id IN (SELECT MAX(id) FROM certification_status GROUP BY certification_id) and certification_status_type_id = ?;
             */
             
             // Not sure what is the direction of the ff codes
            var certificationStatuses = root.join(Certification_.certificationStatuses);
            predicates.add(criteriaBuilder.and(criteriaBuilder.max(certificationStatuses.get(CertificationStatus_.id)))); 
            // It says Cannot resolve method 'and(javax.persistence.criteria.Expression<N>)'
            // Not also sure how to pass in parameter certificationStatusTypeId
        }

        // This is working
        if (dateFrom != null) {
            var offsetDateTimeFrom = OffsetDateTime.of(dateFrom, LocalTime.MIDNIGHT, ZoneOffset.ofHours(8));
            var dateTimeFrom = Date.from(offsetDateTimeFrom.toInstant());
            var offsetDateTimeTo = OffsetDateTime.of(Objects.requireNonNullElse(dateTo, dateFrom), LocalTime.MAX, ZoneOffset.ofHours(8));
            var dateTimeTo = Date.from(offsetDateTimeTo.toInstant());
            predicates.add(criteriaBuilder.and(criteriaBuilder.between(
                    root.get(Certification_.createdAt),
                    criteriaBuilder.literal(dateTimeFrom),
                    criteriaBuilder.literal(dateTimeTo)))
            );
        }

        // This is working
        if (client != null && !client.isBlank()) {
            predicates.add(criteriaBuilder.and(criteriaBuilder.like(criteriaBuilder.lower(root.join(Certification_.client).get(Client_.name)), "%" + client.toLowerCase() + "%")));
        }

        return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
    });
    return certifications.stream()
            .map(this.certificationMapper::fromEntityToDto)
            .collect(Collectors.toUnmodifiableList());
}

我对@9​​87654332@、dateFromdateToclient 的标准有效。对于certificationStatusTypeId,我想根据以下 SQL 语句创建条件:

SELECT * FROM certification_status WHERE id IN (SELECT MAX(id) FROM certification_status GROUP BY certification_id) and certification_status_type_id = ?;

此语句在数据库中有效。不确定这是否是基于MAX(id)GROUP BY certification_idWHERE certification_status_type_id = ? 选择记录的最佳查询组合。

感谢任何帮助。谢谢。

**更新

现在这是我的代码:

    if (certificationStatusTypeId != null && certificationStatusTypeId > 0) {
        Subquery<Long> subQuery = criteriaQuery.subquery(Long.class);
        Root<CertificationStatus> subRoot = subQuery.from(CertificationStatus.class);
        subQuery.select(criteriaBuilder.max(subRoot.get(CertificationStatus_.id)))
                .groupBy(subRoot.get(CertificationStatus_.certification).get(Certification_.id));
        predicates.add(criteriaBuilder.and(criteriaBuilder.in(subRoot.get(CertificationStatus_.id)).value(subQuery)));
        predicates.add(criteriaBuilder.and(criteriaBuilder.equal(subRoot.get(CertificationStatus_.certificationStatusType).get(CertificationStatusType_.id), certificationStatusTypeId)));
    }

但它会引发以下错误:

org.hibernate.hql.internal.ast.QuerySyntaxException: Invalid path: 'generatedAlias1.id' [select generatedAlias0 from entity.Certification as generatedAlias0 where ( generatedAlias1.id in (select max(generatedAlias1.id) from entity.CertificationStatus as generatedAlias1 group by generatedAlias1.certification.id) ) and ( generatedAlias1.certificationStatusType.id=2 )]; nested exception is java.lang.IllegalArgumentException: org.hibernate.hql.internal.ast.QuerySyntaxException: Invalid path: 'generatedAlias1.id' [select generatedAlias0 from entity.Certification as generatedAlias0 where ( generatedAlias1.id in (select max(generatedAlias1.id) from entity.CertificationStatus as generatedAlias1 group by generatedAlias1.certification.id) ) and ( generatedAlias1.certificationStatusType.id=2 )]

查看生成的 JPQL,它似乎与我的 SQL 语句相似。我只是不明白为什么是Invalid path

**更新 我很抱歉!我的SQL语句其实和下面类似:

select
    // props of Certification
from certification c
inner join certification_type ct on ct.id = c.certification_type_id
inner join client cl on cl.certification_id = c.id
inner join certification_status cs on cs.certification_id = c.id
inner join certification_status_type cst on cst.id = cs.certification_status_type_id
where cs.id in (select max(cs2.id) form certification_status cs2 group by cs2.certification_id) and cs.certification_status_type_id = ?;

更新

解决了。感谢Thorben Janssen 耐心调查我的问题。

这是我的代码的最后一部分:

    if (certificationStatusTypeId != null && certificationStatusTypeId > 0) {
        var certificationStatuses = root.join(Certification_.certificationStatuses);
        Subquery<Long> subQuery = criteriaQuery.subquery(Long.class);
        Root<CertificationStatus> subRoot = subQuery.from(CertificationStatus.class);
        subQuery.select(criteriaBuilder.max(subRoot.get(CertificationStatus_.id)))
                .groupBy(subRoot.get(CertificationStatus_.certification).get(Certification_.id));
        predicates.add(criteriaBuilder.and(certificationStatuses.get(CertificationStatus_.id).in(subQuery)));
        predicates.add(criteriaBuilder.and(criteriaBuilder.equal(certificationStatuses.get(CertificationStatus_.certificationStatusType).get(CertificationStatusType_.id), certificationStatusTypeId)));
    }

【问题讨论】:

    标签: java hibernate jpa subquery jpa-criteria


    【解决方案1】:

    以下代码 sn-p 创建一个与您的 SQL 语句相同的 CriteriaQuery。因为您使用的是 Spring Data JPA,所以可以忽略第一个块和最后一行。 Spring Data JPA 为您提供它们。

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<CertificationStatus> criteriaQuery = cb.createQuery(CertificationStatus.class);
    Root<CertificationStatus> root = criteriaQuery.from(CertificationStatus.class);
    criteriaQuery.select(root);
    
    Subquery<Long> sub = criteriaQuery.subquery(Long.class);
    Root<CertificationStatus> subRoot = sub.from(CertificationStatus.class);
    sub.select(cb.max(subRoot.get("id")));
    sub.groupBy(subRoot.get("certification").get("id"));
    
    criteriaQuery.where(root.get("id").in(sub));
    
    em.createQuery(criteriaQuery).getResultList();
    

    您可以通过在您的CriteriaQuery 上调用subquery 方法来create a subquery。这将返回一个Subquery 接口,您可以像使用CriteriaQuery 接口一样使用它。

    定义 Subquery 后,您可以定义 IN 子句并将其包含在查询的 WHERE 子句中。这是总是看起来有点奇怪的部分。您无需使用CriteriaBuilder 来定义Expression,而是获取实体属性并在其上调用in 方法并引用您的Subquery

    在最后一步中,您使用CriteriaQuery 实例化TypedQuery 并执行它。激活logging of SQL statements后,可以看到Hibernate执行如下SQL语句:

    select
        certificat0_.id as id1_4_,
        certificat0_.certification_id as certific3_4_,
        certificat0_.certification_status_type_id as certific4_4_,
        certificat0_.remarks as remarks2_4_ 
    from
        CertificationStatus certificat0_ 
    where
        certificat0_.id in (
            select
                max(certificat1_.id) 
            from
                CertificationStatus certificat1_ 
            group by
                certificat1_.certification_id
        )
    

    **更新

    您在定义 IN 子句时似乎引用了错误的表。请尝试更换

    predicates.add(criteriaBuilder.and(criteriaBuilder.in(subRoot.get(CertificationStatus_.id)).value(subQuery)));
    

        predicates.add(criteriaBuilder.and(certificationStatuses.get(CertificationStatus_.id).in(subQuery)));
    

    【讨论】:

    • 我的错!我发布的 SQL 语句只是更大语句的一部分。我的select ... from 实际上是针对主要实体Certification。查看我的实体。我会更新我的帖子。
    • 我更新了我的帖子。结果应该是Certification 的列表。鉴于@ManyToMany 的关系,将包括CertificationStatus 的列表。
    • 您在创建 IN 子句时似乎引用了错误的 Root 接口。请看我的更新
    • 谢谢!我根据您的建议更新了代码。它给了我一个想法来更新下一行:predicates.add(criteriaBuilder.and(criteriaBuilder.equal(subRoot.get(CertificationStatus_.certificationStatusType).get(CertificationStatusType_.id), certificationStatusTypeId)));predicates.add(criteriaBuilder.and(criteriaBuilder.equal(certificationStatuses.get(CertificationStatus_.certificationStatusType).get(CertificationStatusType_.id), certificationStatusTypeId)));。它现在就像一个魅力。
    猜你喜欢
    • 2011-12-29
    • 1970-01-01
    • 2022-01-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-01
    • 2021-08-02
    • 1970-01-01
    相关资源
    最近更新 更多