javapostgresqlspring-boothibernatejpql

Missing constructor when using SELECT NEW DTO with query Java Persist Query Language


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?


Solution

  • 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;
    
        // ...
    }