line / kotlin-jdsl

Kotlin library that makes it easy to build and execute queries without generated metamodel
https://kotlin-jdsl.gitbook.io/docs/
Apache License 2.0
704 stars 85 forks source link

Custom expression으로 DTO projection시 캐스팅 오류 문의 #746

Closed mkroo closed 2 months ago

mkroo commented 2 months ago

안녕하세요, Select new를 통해 DTO projection을 할 때 Custom expression(distanceFrom)을 통해 반환되는 값이 java.lang.Object이기 때문에 projection이 불가하다는 오류가 발생하지만 실제로 디버깅하엿을 때에는 Double 클래스로 캐스팅되는것으로 보여 문의드립니다.

Projection을 하려는 data class에서 로그를 찍어 캐스팅되는 클래스를 확인해보았을 때에는 kotlin의 Double 클래스로 반환되는것을 확인하였으나 실제 오류메세지에서는 java.lang.Object로 보이는것을 확인하였습니다.

부연 설명이 필요한 부분이 있다면 말씀 부탁드립니다!

좋은 라이브러리를 오픈소스로 제공해주셔서 감사합니다 :)

Case 1. Alias를 통해 필드의 이름을 지정해주었을 때의 오류 메세지

아래의 오류메세지는 필드이름이 있지만 projection class에서 distance필드에 Double로 매핑되는 값이 없어 나는것으로 추측하였습니다

Cannot set field 'distance' to instantiate 'com.mkroo.icehockeymate.domain.dto.LocationSummary'

Case 2. Alias를 제거하였을 때의 오류 메세지

Cannot instantiate class 'com.mkroo.icehockeymate.domain.dto.LocationSummary' (it has no constructor with signature [java.util.UUID, java.lang.String, java.lang.String, com.mkroo.icehockeymate.domain.Coordinate, java.lang.Long, java.lang.Object], and not every argument has an alias)

작성한 코드

쿼리

val query = jpql(LocationQueryLanguage) {
    selectNew<LocationSummary>(
        path(Location::id).alias(expression("id")),
        path(Location::name).alias(expression("name")),
        path(Location::address).alias(expression("address")),
        path(Location::coordinate).alias(expression("coordinate")),
        count(Match::id).alias(expression("scheduledMatchCount")),
        request.currentCoordinate?.let { distanceFrom(it).alias(expression("distance")) } ?: doubleLiteral(0.0)
    )
        .from(
            entity(Location::class),
            leftJoin(Match::class).on(path(Match::rink)(Location::id).eq(path(Location::id)))
        )
        .where(
            addressBelongsTo(request.region)
        )
        .groupBy(
            path(Location::id)
        )
        .orderBy(
            request.currentCoordinate?.let { distanceFrom(it).asc() },
            path(Location::name).asc()
        )
}

Custom DSL

class LocationQueryLanguage : Jpql() {
    companion object Constructor : JpqlDsl.Constructor<LocationQueryLanguage> {
        override fun newInstance(): LocationQueryLanguage = LocationQueryLanguage()
    }

    fun addressBelongsTo(region: String?): Predicate? {
        if (region.isNullOrBlank()) return null

        return customExpression(String::class, "{0}", path(Location::address)).like("%$region%")
    }

    fun categoryEquals(category: LocationCategory?): Predicatable? {
        return category?.let { path(Location::category).eq(it) }
    }

    fun distanceFrom(coordinate: Coordinate): Expression<Double> {
        return customExpression(
            Double::class,
            "ST_DISTANCE_SPHERE(point({0}, {1}), point({2}, {3}))",
            path(Location::coordinate)(Coordinate::longitude),
            path(Location::coordinate)(Coordinate::latitude),
            value(coordinate.longitude),
            value(coordinate.latitude)
        )
    }
}

Projection을 하는 data class

class LocationSummary(
    val id: UUID,
    val name: String,
    val address: Address,
    val coordinate: Coordinate,
    val scheduledMatchCount: Long,
    val distance: Double
) {
//  디버깅을 위한 secondary constructor, 없는 경우 case 1, 2의 오류가 발생
//  INFO 48990 --- [ice-hockey-mate] [nio-8080-exec-1] c.m.i.domain.dto.LocationSummary         : class kotlin.Double
    constructor(
        id: UUID,
        name: String,
        address: Address,
        coordinate: Coordinate,
        scheduledMatchCount: Long,
        distance: Any,
    ) : this(id, name, address, coordinate, scheduledMatchCount, distance.toString().toDouble()) {
        LoggerFactory.getLogger(this::class.java).info(distance::class.toString())
    }
}
shouwn commented 2 months ago

@mkroo 안녕하세요.

ST_DISTANCE_SPHERE는 DB 함수로 보입니다. customExpression은 JPQL을 직접 랜더링하기 위한 메소드로 DB 함수를 호출하기 위해 만들어지지 않았습니다.

그래서 function을 사용하는 것을 추천드립니다. 그리고 DB 함수의 경우 방언에 등록되어 있지 않으면 Hibernate가 반환타입을 추측할 수 없어 Object로 타입을 유추했다고 여겨집니다.

Hibernate를 사용하실 경우 FunctionContributor를 이용해 함수의 정보를 등록해주시면 정상적으로 동작하지 않을까 예상하고 있습니다.

mkroo commented 2 months ago

FunctionContributor를 등록하여 타입이 정상적으로 캐스팅되지 않는 이슈를 해결하였습니다 customExpression과 function의 용도에 대한 가이드도 감사합니다 🙇