多对多关系中 MAX id GROUP BY FOREIGN KEY 的 JPA 条件

JPA Criteria for MAX id GROUP BY FOREIGN KEY in a ManyToMany relationship

我正在使用 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());
}

我的 certificationTypeIddateFromdateToclient 标准有效。对于 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 = ?.

查询 select 记录的最佳组合

感谢任何帮助。谢谢。

**更新

这是我的代码:

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

以下代码片段创建了一个与您的 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();

您可以 create a subquery 通过调用 CriteriaQuery 上的 subquery 方法。这个 returns 一个 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 子句时引用了错误的 table。请尝试更换

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

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