java JPA条件元组查询失败,因为groupby子句中的join缺少列(Sping Boot 2.7.8和hib5.6.14.Final)

kgsdhlau  于 2023-01-29  发布在  Java
关注(0)|答案(1)|浏览(76)

我正在尝试使用JPA Criteria API来过滤结果,并使用简单的countminavgmax来聚合它们。我正在使用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捆绑在一起)。

fcg9iug3

fcg9iug31#

我认为“解决方案”是创建两个“独立”的查询根并将它们连接在一起:

CriteriaBuilder builder = session.getCriteriaBuilder();
CriteriaQuery<Tuple> query = builder.createTupleQuery();

Root<VehicleStopped> root = query.from(VehicleStopped.class);

// instead of Path<CameraZone> zone = root.get("cameraZone")
Root<CameraZone> zone = query.from(CameraZone.class);
query.where(builder.equal(zone, root.get("cameraZone")));

Path<Number> duration = root.get("durationSeconds");

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

session.createQuery(query).getResultList();

在这种情况下,Hibernate 5会生成以下SQL(从我的Angular 来看,由于group by子句中缺少列,这看起来很奇怪):

select
        naturalidc1_.id as col_0_0_,
        count(*) as col_1_0_,
        min(naturalidc0_.duration_seconds) as col_2_0_,
        avg(naturalidc0_.duration_seconds) as col_3_0_,
        max(naturalidc0_.duration_seconds) as col_4_0_,
        naturalidc1_.id as id1_0_,
        naturalidc1_.name as name2_0_,
        naturalidc1_.type as type3_0_,
        naturalidc1_.uuid as uuid4_0_ 
    from
        vehicle_stopped naturalidc0_ cross 
    join
        camera_zone naturalidc1_ 
    where
        naturalidc1_.id=naturalidc0_.zone_id 
    group by
        naturalidc1_.id

仅供参考。您的初始查询在Hibernate 6中确实可以工作,生成的SQL看起来确实更正确,但仍然很奇怪:

select
        c1_0.id,
        c1_0.name,
        c1_0.type,
        c1_0.uuid,
        count(*),
        min(v1_0.duration_seconds),
        avg(v1_0.duration_seconds),
        max(v1_0.duration_seconds) 
    from
        vehicle_stopped v1_0 
    join
        camera_zone c1_0 
            on c1_0.id=v1_0.zone_id 
    group by
        1,
        2,
        3,
        4

相关问题