多对多关系中 MAX id GROUP BY FOREIGN KEY 的 JPA 条件
JPA Criteria for MAX id GROUP BY FOREIGN KEY in a ManyToMany relationship
我正在使用 Spring Boot
和 Spring 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_id
和 WHERE 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());
}
我的 certificationTypeId
、dateFrom
、dateTo
和 client
标准有效。对于 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_id
和 WHERE 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)));
我正在使用 Spring Boot
和 Spring 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_id
和 WHERE 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());
}
我的 certificationTypeId
、dateFrom
、dateTo
和 client
标准有效。对于 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_id
和 WHERE 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)));
}
以下代码片段创建了一个与您的 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)));