I am using Spring Data JPA with Hibernate 6. I want to fetch a list of cars into a DTO using JPQL SELECT NEW syntax, with LEFT JOIN, GROUP BY, and CASE conditions.
I keep getting the following error:
Caused by: java.lang.IllegalArgumentException: org.hibernate.query.SemanticException: Missing constructor for type 'CarsResponseDto' [ SELECT NEW com.duydev.backend.presentation.dto.response.CarsResponseDto(
c.id,
c.brand,
c.model,
c.year,
c.pricePerHour
)
FROM CarsEntity c
LEFT JOIN c.location loc
LEFT JOIN c.bookings b
WHERE (:__$synthetic$__1 IS NULL OR c.brand = :__$synthetic$__2)
AND (:__$synthetic$__3 IS NULL OR c.year = :__$synthetic$__4)
AND (:__$synthetic$__5 IS NULL OR loc.province = :__$synthetic$__6)
AND (:__$synthetic$__7 IS NULL OR loc.ward = :__$synthetic$__8)
AND (:__$synthetic$__9 IS NULL OR c.pricePerHour >= :__$synthetic$__10)
AND (:__$synthetic$__11 IS NULL OR c.pricePerHour <= :__$synthetic$__12)
AND (b.endTime >= :__$synthetic$__13 OR b.id IS NULL)
GROUP BY c.id
HAVING SUM(
CASE
WHEN b.id IS NOT NULL
AND b.status = 'CONFIRMED'
AND b.endTime > :__$synthetic$__14
AND b.startTime < :__$synthetic$__15
THEN 1
ELSE 0
END
) = 0
]
My file CarsRepository:
package com.duydev.backend.domain.repositories;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import com.duydev.backend.domain.model.CarsEntity;
import com.duydev.backend.presentation.dto.request.RequestGetCarsDto;
import com.duydev.backend.presentation.dto.response.CarsResponseDto;
@Repository
public interface CarsRepository extends JpaRepository<CarsEntity, Long> {
@Query("""
SELECT NEW com.duydev.backend.presentation.dto.response.CarsResponseDto(
c.id,
c.brand,
c.model,
c.year,
c.pricePerHour
)
FROM CarsEntity c
LEFT JOIN c.location loc
LEFT JOIN c.bookings b
WHERE (:#{#requestGetCarsDto.getBrand()} IS NULL OR c.brand = :#{#requestGetCarsDto.getBrand()})
AND (:#{#requestGetCarsDto.getYear()} IS NULL OR c.year = :#{#requestGetCarsDto.getYear()})
AND (:#{#requestGetCarsDto.getProvince()} IS NULL OR loc.province = :#{#requestGetCarsDto.getProvince()})
AND (:#{#requestGetCarsDto.getWard()} IS NULL OR loc.ward = :#{#requestGetCarsDto.getWard()})
AND (:#{#requestGetCarsDto.getMinPrice()} IS NULL OR c.pricePerHour >= :#{#requestGetCarsDto.getMinPrice()})
AND (:#{#requestGetCarsDto.getMaxPrice()} IS NULL OR c.pricePerHour <= :#{#requestGetCarsDto.getMaxPrice()})
AND (b.endTime >= :#{#requestGetCarsDto.getStartTime()} OR b.id IS NULL)
GROUP BY c.id
HAVING SUM(
CASE
WHEN b.id IS NOT NULL
AND b.status = 'CONFIRMED'
AND b.endTime > :#{#requestGetCarsDto.getStartTime()}
AND b.startTime < :#{#requestGetCarsDto.getEndTime()}
THEN 1
ELSE 0
END
) = 0
""")
public Page<CarsResponseDto> findCars(@Param("requestGetCarsDto") RequestGetCarsDto requestGetCarsDto,
Pageable pageable);
}
This is my CarsEntity:
package com.duydev.backend.domain.model;
import java.math.BigDecimal;
import java.util.List;
import org.hibernate.annotations.ColumnTransformer;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "tbl_cars")
public class CarsEntity extends AbstractEntity<Long> {
@ManyToOne
@JoinColumn(name = "owner_id", referencedColumnName = "id")
private User user;
@Column(name = "brand")
private String brand;
@Column(name = "model")
private String model;
@Column(name = "license_plate")
private String licensePlate;
@Column(name = "year")
private Integer year;
@Column(name = "price_per_hour")
private BigDecimal pricePerHour;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "location_id", referencedColumnName = "id")
private LocationEntity location;
@Column(name = "images", columnDefinition = "jsonb")
@ColumnTransformer(write = "?::jsonb")
private String images;
@OneToMany(mappedBy = "car")
private List<BookingEntity> bookings;
}
Here is my CarsResponseDto:
package com.duydev.backend.presentation.dto.response;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class CarsResponseDto {
Long id;
String brand;
String model;
Integer year;
Double pricePerHour;
public CarsResponseDto(Long id, String brand, String model, Integer year, Double pricePerHour) {
this.id = id;
this.brand = brand;
this.model = model;
this.year = year;
this.pricePerHour = pricePerHour;
}
}
This is my schema Cars:
CREATE TABLE tbl_cars (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
created_by BIGINT,
updated_by BIGINT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
owner_id BIGINT NOT NULL,
brand VARCHAR(50) NOT NULL,
model VARCHAR(50) NOT NULL,
year INT NOT NULL,
license_plate VARCHAR(20) UNIQUE NOT NULL,
price_per_hour DECIMAL(10, 2) NOT NULL,
location_id BIGINT,
images JSONB,
FOREIGN KEY (owner_id) REFERENCES tbl_user(id) ON DELETE CASCADE,
FOREIGN KEY (location_id) REFERENCES tbl_locations(id) ON DELETE SET NULL
);
How can I fix this error while keeping the CASE expressions and DTO projection?
In order for the constructor DTO syntax to work, Hibernate needs to find an exact constructor match. In your current DTO definition:
@Getter
@Setter
@NoArgsConstructor
public class CarsResponseDto {
Long id;
String brand;
String model;
Integer year;
Double pricePerHour;
// ...
}
you define the pricePerHour to be a Double. However, in CarsEntity, this field, which is what you select in the HQL query, is defined as a BigDecimal. Hibernate may not be able to handle this cast internally. Consider aligning the types as follows:
@Getter
@Setter
@NoArgsConstructor
public class CarsResponseDto {
Long id;
String brand;
String model;
Integer year;
BigDecimal pricePerHour;
// ...
}