Hi, we have a Spring Boot application running on 3.0.x which we are currently updating to the Spring Boot 3.3.3.
We are facing problems with Criteria API and join clauses after migration - one join is left out from final query (which should exist).
We tried upgrading Hibernate to 6.5.3.Final and 6.6.1.Final, without any luck. We also upgraded spring-data to 3.3.4, which also did not help. But we managed to pinpoint the Hibernate version which broke everything - 6.5.0.Final. Every other newer hibernate version fails (including 6.5.0), all without any code change.
Lets get back to the original version that comes with spring-data-3.3.3, which is 6.5.2.Final. With 6.5.2.Final version it seems that there is some sort of optimization performed when casting Fetch to Join. If we manually perform fetch and join on root, generated query is the same.
Here are the DTOs which we are using and also a piece of code which produces such behavior.
DTOs:
ProductEntity.java
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@Table(name = "product")
public class ProductEntity implements Serializable {
/** Used to regulate the number of entities to be included in one hibernate fetch operation.*/
private static final int BATCH_SIZE = 30;
@Id
@Column(name = "prod_id")
private Long prodId;
@Column(name = "prod_code")
private String prodCode;
@Column(name = "prod_type")
private String prodType;
@Column(name = "prod_lifecycle")
private String prodLifecycle;
@Column(name = "prod_description")
private String prodDescription;
@Column(name = "login")
private String login;
@Column(name = "comment")
private String comment;
@Column(name = "party")
private String party;
@Column(name = "system_record_timestamps")
private LocalDateTime systemRecordTimestamps;
@Column(name = "system_update_timestamps")
private LocalDateTime systemUpdateTimestamps;
@Column(name = "record_modification_timestamp")
private LocalDateTime recordModificationTimestamp;
@Column(name = "creation_date")
private LocalDate creationDate;
@Column(name = "last_change_date")
private LocalDate lastChangeDate;
@Column(name = "managed_by")
private String managedBy;
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@BatchSize(size = BATCH_SIZE)
private List<ProductEntityEntity> ents = new ArrayList<>();
}
ProductEntityEntity.java
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@Table(name = "product_entity")
public class ProductEntityEntity implements Serializable {
@Id
@Column(name = "prod_entity_id")
private Long prodEntityId;
@Column(name = "prod_id", insertable = false, updatable = false)
private Long prodId;
@EqualsAndHashCode.Include
@Column(name = "entity_type")
private String entityType;
@EqualsAndHashCode.Include
@Column(name = "entity_id")
private String entityId;
@EqualsAndHashCode.Include
@Column(name = "module")
private String module;
@EqualsAndHashCode.Include
@Column(name = "initiator")
private String initiator;
@EqualsAndHashCode.Include
@Column(name = "party")
private String party;
@Column(name = "system_record_timestamps")
private LocalDateTime systemRecordTimestamps;
@Column(name = "system_update_timestamps")
private LocalDateTime systemUpdateTimestamps;
@Column(name = "record_modification_timestamp")
private LocalDateTime recordModificationTimestamp;
@ManyToOne
@JoinColumn(name = "prod_id")
@ToString.Exclude
private ProductEntity product;
}
Criteria API usage
public static Specification<ProductEntity> getProductOverviewSpecification(
FilterDto filter) {
return (alert, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
predicates.addAll(getProductPredicates(filter,
criteriaBuilder, alert));
Fetch<ProductEntity, ProductEntityEntity> entityFetch;
Join<ProductEntity, ProductEntityEntity> initiatorEntityJoin;
if (Long.class != query.getResultType()) {
entityFetch = alert.fetch("ents");
initiatorEntityJoin = (Join<ProductEntity, ProductEntityEntity>) entityFetch;
} else {
initiatorEntityJoin = alert.join("ents");
}
predicates.add(criteriaBuilder.equal(initiatorEntityJoin.get("initiator"),
AlertEntityInitiator.YES.getFlag()));
if (StringUtils.isNotBlank(filter.getMemberSign())) {
predicates.add(criteriaBuilder.equal(initiatorEntityJoin.get("party"),
filter.getMemberSign()));
}
if (StringUtils.isNotBlank(filter.getPersonId())
|| StringUtils.isNotBlank(filter.getPersonApplicationModule())
|| StringUtils.isNotBlank(filter.getSuperPersonId())) {
Join<ProductEntity, ProductEntityEntity> searchEntityJoin = alert.join("ents");
predicates.addAll(getProductEntityPredicates(filter,
criteriaBuilder, searchEntityJoin));
}
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
}
As far as I traced out, it seems that this type cast in ifology behaves differently between versions.
For this purpose, we used 6.4.10.Final as last version which works ok.
Here are the generated queries for two different Hibernate versions:
Hibernate 6.4.10.Final
select
pe1_0.prod_id,
pe1_0.comment,
pe1_0.creation_date,
e1_0.prod_id,
e1_0.prod_entity_id,
e1_0.entity_id,
e1_0.entity_type,
e1_0.initiator,
e1_0.module,
e1_0.party,
e1_0.record_modification_timestamp,
e1_0.system_record_timestamps,
e1_0.system_update_timestamps,
pe1_0.last_change_date,
pe1_0.login,
pe1_0.managed_by,
pe1_0.party,
pe1_0.prod_code,
pe1_0.prod_description,
pe1_0.prod_lifecycle,
pe1_0.prod_type,
pe1_0.record_modification_timestamp,
pe1_0.system_record_timestamps,
pe1_0.system_update_timestamps
from
product pe1_0
join
product_entity e1_0
on pe1_0.prod_id=e1_0.prod_id
join
product_entity e2_0
on pe1_0.prod_id=e2_0.prod_id
where
pe1_0.creation_date between ? and ?
and pe1_0.prod_type=?
and pe1_0.prod_lifecycle=?
and e1_0.initiator=?
and e2_0.module=?
and e2_0.entity_id=?
and e2_0.entity_type=?
order by
pe1_0.prod_code
Hibernate 6.5.2.Final
select
pe1_0.prod_id,
pe1_0.comment,
pe1_0.creation_date,
e1_0.prod_id,
e1_0.prod_entity_id,
e1_0.entity_id,
e1_0.entity_type,
e1_0.initiator,
e1_0.module,
e1_0.party,
e1_0.record_modification_timestamp,
e1_0.system_record_timestamps,
e1_0.system_update_timestamps,
pe1_0.last_change_date,
pe1_0.login,
pe1_0.managed_by,
pe1_0.party,
pe1_0.prod_code,
pe1_0.prod_description,
pe1_0.prod_lifecycle,
pe1_0.prod_type,
pe1_0.record_modification_timestamp,
pe1_0.system_record_timestamps,
pe1_0.system_update_timestamps
from
product pe1_0
join
product_entity e1_0
on pe1_0.prod_id=e1_0.prod_id
where
pe1_0.creation_date between ? and ?
and pe1_0.prod_type=?
and pe1_0.prod_lifecycle=?
and e1_0.initiator=?
and e1_0.module=?
and e1_0.entity_id=?
and e1_0.entity_type=?
order by
pe1_0.prod_code
So, with 6.4.10.Final we get two join clauses, but with 6.5.2.Final, we only get one (this is the missing one: join product_entity e2_0 on pe1_0.prod_id=e2_0.prod_id)
Can someone please check what might cause such behavior?
Thank you in advance.