我正在尝试使用JPA Criteria API来过滤结果,并使用简单的count
、min
、avg
和max
来聚合它们。我正在使用Spring Boot 2.7.8,因此我正在尝试使用接口投影,以便这些聚合结果看起来与Spring存储库自动完成的简单查询相同。
我的域实体(为简洁起见,进行了简化)如下所示:
@Entity
@Table(name = "vehicle_stopped")
@IdClass(VehicleStopped.VehicleStoppedPK.class)
public class VehicleStopped implements Serializable {
@Id
@Column(name = "stopped_session_uuid", nullable = false)
private String stoppedSessionUuid;
@Id
@Column(name = "start_ts", nullable = false)
private OffsetDateTime startTs;
@Column(name = "end_ts", nullable = false)
private OffsetDateTime endTs;
@Column(name = "duration_seconds")
private Double durationSeconds;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "zone_id")
private CameraZone cameraZone;
@Override
public VehicleStoppedPK getId() {
VehicleStopped.VehicleStoppedPK pk = new VehicleStopped.VehicleStoppedPK();
pk.setStartTs(this.getStartTs());
pk.setStoppedSessionUuid(this.getStoppedSessionUuid());
return pk;
}
public OffsetDateTime getEndTs() {
return endTs;
}
public void setEndTs(OffsetDateTime endTs) {
this.endTs = endTs;
}
public Double getDurationSeconds() {
return durationSeconds;
}
public void setDurationSeconds(Double durationSeconds) {
this.durationSeconds = durationSeconds;
}
public CameraZone getCameraZone() {
return cameraZone;
}
public void setCameraZone(CameraZone cameraZone) {
this.cameraZone = cameraZone;
}
public VehicleType getVehicleType() {
return vehicleType;
}
public void setVehicleType(VehicleType vehicleType) {
this.vehicleType = vehicleType;
}
public String getStoppedSessionUuid() {
return stoppedSessionUuid;
}
public void setStoppedSessionUuid(String stoppedSessionUuid) {
this.stoppedSessionUuid = stoppedSessionUuid;
}
//some details removed for brevity
@Override
public static class VehicleStoppedPK implements Serializable {
private OffsetDateTime startTs;
private String stoppedSessionUuid;
public VehicleStoppedPK() {
}
public OffsetDateTime getStartTs() {
return startTs;
}
public void setStartTs(OffsetDateTime startTs) {
this.startTs = startTs;
}
public String getStoppedSessionUuid() {
return stoppedSessionUuid;
}
public void setStoppedSessionUuid(String stoppedSessionUuid) {
this.stoppedSessionUuid = stoppedSessionUuid;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
VehicleStoppedPK that = (VehicleStoppedPK) o;
return Objects.equals(startTs, that.startTs) && Objects.equals(stoppedSessionUuid, that.stoppedSessionUuid);
}
@Override
public int hashCode() {
return Objects.hash(startTs, stoppedSessionUuid);
}
@Override
public String toString() {
return "VehicleStoppedPK{" +
"startTs=" + startTs +
", stoppedSessionUuid='" + stoppedSessionUuid + '\'' +
'}';
}
}
}
@Entity
@Table(name = "camera_zone")
public class CameraZone implements Serializable {
@Id
@SequenceGenerator(name = "camera_zone_id_seq", sequenceName = "camera_zone_id_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "camera_zone_id_seq")
@Column(name = "id", updatable=false)
private Integer id;
@Column(name = "uuid", unique = true)
private String uuid;
@Column(name = "type")
private String type;
@Column(name = "name")
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CameraZone that = (CameraZone) o;
return Objects.equals(id, that.id) && Objects.equals(uuid, that.uuid) && Objects.equals(camera, that.camera) && Objects.equals(type, that.type) && Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(id, uuid, camera, type, name);
}
}
我的Repository实现中的代码如下所示:
public class SpecificationVehicleStoppedRepositoryImpl
implements SpecificationVehicleStoppedRepository {
@Autowired private EntityManager em;
@Autowired ProjectionFactory projectionFactory;
@Override
public List<VehicleStoppedAggregate> getStoppedVehiclesCount(Specification<VehicleStopped> spec) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Tuple> query = builder.createTupleQuery();
Root<VehicleStopped> root = query.from(VehicleStopped.class);
Predicate predicate = spec.toPredicate(root, query, builder);
if (predicate != null) {
query.where(predicate);
}
Path<Number> duration = root.get("durationSeconds");
Path<CameraZone> zone = root.get("cameraZone");
query
.multiselect(zone,
builder.count(root).alias("totalVehicles"),
builder.min(duration).alias("minDuration"),
builder.avg(duration).alias("avgDuration"),
builder.max(duration).alias("maxDuration"))
.groupBy(zone);
List<Tuple> rawResultList = em.createQuery(query).getResultList();
return project(rawResultList, VehicleStoppedAggregate.class);
}
private <P> List<P> project(List<Tuple> results, Class<P> projectionClass) {
return results.stream()
.map(tuple -> {
Map<String, Object> mappedResult = new HashMap<>(tuple.getElements().size());
for (TupleElement<?> element : tuple.getElements()) {
String name = element.getAlias();
mappedResult.put(name, tuple.get(name));
}
return projectionFactory.createProjection(projectionClass, mappedResult);
})
.collect(Collectors.toList());
}
}
我尝试填充的基于接口的投影(使用SpelAwareProxyProjectionFactory
)如下:
public interface VehicleStoppedAggregate {
CameraZone getCameraZone();
Integer getTotalVehicles();
Double getMinDuration();
Double getAvgDuration();
Double getMaxDuration();
}
对getStoppedVehiclesCount()
的调用失败,错误如下:ERROR: column "camerazone1_.id" must appear in the GROUP BY clause or be used in an aggregate function
此错误来自PostgreSQL数据库,这是正确的,因为SQL hib生成的是不正确的:
select
vehiclesto0_.zone_id as col_0_0_,
count(*) as col_1_0_,
min(vehiclesto0_.duration_seconds) as col_2_0_,
avg(vehiclesto0_.duration_seconds) as col_3_0_,
max(vehiclesto0_.duration_seconds) as col_4_0_,
camerazone1_.id as id1_2_,
camerazone1_.name as name2_2_,
camerazone1_.type as type3_2_,
camerazone1_.uuid as uuid4_2_
from
vehicle_stopped vehiclesto0_
inner join
camera_zone camerazone1_
on vehiclesto0_.zone_id=camerazone1_.id cross
where
vehiclesto0_.start_ts>=?
and vehiclesto0_.start_ts<=?
and 1=1
and 1=1
and 1=1
group by
vehiclesto0_.zone_id
它没有按从联接表中请求的其他字段进行分组。
如果我必须使用一个普通的类,而不是Tuple,它会工作,但这将意味着我必须创建一个类,为Hibernate的所有字段提供一个巨大的构造函数来填充它。
不知何故,当我对Spring的存储库而不是我的标准库使用基于接口的预测时,同样的场景也起作用了,它们成功地填充了一对多的关系。
有没有办法解决这个问题,让Hibernate请求正确的字段?
我使用的是Hibernate 5.6.14.Final(与Spring Boot 2.7.8捆绑在一起)。
1条答案
按热度按时间fcg9iug31#
我认为“解决方案”是创建两个“独立”的查询根并将它们连接在一起:
在这种情况下,Hibernate 5会生成以下SQL(从我的Angular 来看,由于
group by
子句中缺少列,这看起来很奇怪):仅供参考。您的初始查询在Hibernate 6中确实可以工作,生成的SQL看起来确实更正确,但仍然很奇怪: