해결된 질문
작성
·
290
0
안녕하세요! 강의 잘 들었습니다 ㅎㅎ
자바 기반으로 강의해주신 내용을 바탕으로 코틀린으로 전환하여 다시금 학습하고 있습니다.
그러던 도중 매핑 과정에서 에러가 발생하는 것을 발견하였는데 이와 관련하여 도움을 요청드리고자 합니다.
fun findFilmWithActorList(page: Long, size: Long): List<FilmWithActor> {
val FILM_ACTOR = JFilmActor.FILM_ACTOR
val ACTOR = JActor.ACTOR
return dslContext.select(
FILM,
FILM_ACTOR,
ACTOR
)
.from(FILM)
.join(FILM_ACTOR).on(FILM.FILM_ID.eq(FILM_ACTOR.FILM_ID))
.join(ACTOR).on(ACTOR.ACTOR_ID.eq(FILM_ACTOR.ACTOR_ID))
.offset((page - 1) * size)
.limit(size)
.fetchInto(FilmWithActor::class.java)
}
코틀린에서 위의 코드처럼 TABLE.fields() 메서드를 사용하지 않으면 정상적으로 동작하지만,
fun findFilmWithActorList(page: Long, size: Long): List<FilmWithActor> {
val FILM_ACTOR = JFilmActor.FILM_ACTOR
val ACTOR = JActor.ACTOR
return dslContext.select(
DSL.row(*FILM.fields()),
DSL.row(*FILM_ACTOR.fields()),
DSL.row(*ACTOR.fields())
)
.from(FILM)
.join(FILM_ACTOR).on(FILM.FILM_ID.eq(FILM_ACTOR.FILM_ID))
.join(ACTOR).on(ACTOR.ACTOR_ID.eq(FILM_ACTOR.ACTOR_ID))
.offset((page - 1) * size)
.limit(size)
.fetchInto(FilmWithActor::class.java)
}
자바와 같이 TABLE.fields()를 사용하였을 때는 아래와 같은 에러가 발생하였습니다.
Caused by: java.lang.NullPointerException: Parameter specified as non-null is null: method com.example.jooq.film.FilmWithActor.<init>, parameter film
at com.example.jooq.film.FilmWithActor.<init>(FilmWithActor.kt)
매핑에 문제가 있는 것 같아 아래와 같이 코드를 수정하였더니 정상 동작하였습니다.
fun findFilmWithActorList(page: Long, size: Long): List<FilmWithActor> {
val FILM_ACTOR = JFilmActor.FILM_ACTOR
val ACTOR = JActor.ACTOR
return dslContext.select(
*FILM.fields(),
*FILM_ACTOR.fields(),
*ACTOR.fields()
)
.from(FILM)
.join(FILM_ACTOR).on(FILM.FILM_ID.eq(FILM_ACTOR.FILM_ID))
.join(ACTOR).on(ACTOR.ACTOR_ID.eq(FILM_ACTOR.ACTOR_ID))
.offset((page - 1) * size)
.limit(size)
.fetch()
.map { record ->
FilmWithActor(
film = record.into(FILM).into(Film::class.java),
filmActor = record.into(FILM_ACTOR).into(FilmActor::class.java),
actor = record.into(ACTOR).into(Actor::class.java)
)
}
}
DTO 클래스는 아래와 같습니다.
data class FilmWithActor(
val film: Film,
val filmActor: FilmActor,
val actor: Actor,
) {
val filmId: Long
get() = this.film.filmId!!
val title: String
get() = this.film.title
val actorFullName: String
get() = "${this.actor.firstName} ${this.actor.lastName}"
}
Film, FilmActor, Actor는 Flyway 기반으로 생성된 pojo 클래스들입니다.
implicitPathJoin, explicitPathJoin 시에도 동일한 문제가 발생하여 문의드리고자 합니다 ㅜ
위의 방법처럼 매핑할 수 있지만, 가독성이 떨어지는 것 같아 혹시 다른 방법이 있는지 궁금합니다.
또한 혹시 제가 놓친 부분이 있다면 알려주시면 감사합니다!
답변 4
1
안녕하세요. 이부분 조금 이해가 안가서 jOOQ 쪽에 이슈를 올렸었는데요.
창시자 답변을 받아서 공유합니다.
https://github.com/jOOQ/jOOQ/issues/17157
원인은 DefaultRecordMapper의 아래 두 옵션때문에 다르게 동작하는것으로 확인되었습니다.
자바: isMapConstructorParameterNames (기본값: false)
코틀린: isMapConstructorParameterNamesInKotlin (기본값: true)
해당 옵션은 생성자가 존재 할 경우 생성자의 변수명을 리플랙션으로 읽어서 sql의 alias와 매핑해주는 옵션입니다. 자바의 default 값이 false인 이유는 해당 옵션이 자바 8 이후 도입되어서 하위 의존성을 위해 false로 되어있기 때문에 그렇습니다.
다만 implicit path join 과 explicit path join은 이렇게되면 sql의 alias가 "alias_xxxx" 형식으로 나타나기 때문에 매핑이 안되고 에러가 발생하는 것이였습니다. (생성되는 SQL에서 확인 할 수 있습니다.)
isMapConstructorParameterNamesInKotlin 옵션을 false로 처리하면 자바처럼 동작하겠지만,
이 옵션을 켤지말지에 대한 판단이 필요합니다. (사실 코틀린이라면 이 옵션을 키는게 좋다고 생각합니다. 필드 선언부가 생성자와 같이 있기 때문에, 개인적인 의견입니다.)
@Configuration
class JooqConfig {
@Bean
fun jooqDefaultConfigurationCustomizer(): DefaultConfigurationCustomizer {
return DefaultConfigurationCustomizer { c ->
c.settings()
// where 절 없이 delete, update 실행 금지
.withExecuteDeleteWithoutWhere(ExecuteWithoutWhere.THROW)
.withExecuteUpdateWithoutWhere(ExecuteWithoutWhere.THROW)
// -- 이부분
.withMapConstructorParameterNamesInKotlin(false)
// implicit join 사용 금지
.withRenderImplicitJoinType(RenderImplicitJoinType.THROW)
// schema 미포함
.withRenderSchema(false)
}
}
}
만약 옵션을 키고 Explicit Path Join을 사용하고 싶다면 현재는 아래처럼 Path에 강제로 alias를 주시면 됩니다.
fun findFilmWithActorsListLast(): List<FilmWithActorName> {
val FILM_ACTOR = FILM.filmActor.`as`("filmActor")
val ACTOR = FILM.filmActor.actor.`as`("actor")
return dslContext
.select(
FILM,
FILM_ACTOR,
ACTOR,
).from(FILM)
.join(FILM_ACTOR)
.join(ACTOR)
.fetchInto(FilmWithActor::class.java)
}
창시자도 이부분이 애매한것같아 alias를 주지 않아도 되도록 개선해보겠다고 하니 지켜보면 될듯합니다. 감사합니다. (저도 이런 세팅이 존재하는지 몰랐네요 ㅎㅎ)
https://github.com/jOOQ/jOOQ/issues/17170
0
안녕하세요. 디버깅을 좀 해보니 이야기가 길어질것같아 답변먼저 작성하고 설명은 밑에 하도록 하겠습니다. 테이블의 모든 필드를 가져오려면 알려주신 방법처럼 테이블 인스턴스를 인자로 주는게 제일 나아보입니다.
fun findFilmWithActorList(page: Long, size: Long): List<FilmWithActor> {
val FILM_ACTOR = JFilmActor.FILM_ACTOR
val ACTOR = JActor.ACTOR
return dslContext
.select(
FILM,
FILM_ACTOR,
ACTOR
)
.from(FILM)
.join(FILM_ACTOR).on(FILM.FILM_ID.eq(FILM_ACTOR.FILM_ID))
.join(ACTOR).on(ACTOR.ACTOR_ID.eq(FILM_ACTOR.ACTOR_ID))
.offset((page - 1) * size)
.limit(size)
.fetchInto(FilmWithActor::class.java)
}
여기서 만약, SELECT한 필드중 일부를 객체로 매핑하려면 아래처럼 하시면 됩니다.
data class FilmWithActorName(val film: Film, val filmActor: FilmActor, val actorName: ActorName)
data class ActorName(val firstName: String?, val lastName: String?)
fun findFilmWithActorsListLast(
page: Long, pageSize: Long
): List<FilmWithActorName> {
val FILM_ACTOR = JFilmActor.FILM_ACTOR
val ACTOR = JActor.ACTOR
return dslContext
.select(
FILM,
FILM_ACTOR,
row(
ACTOR.FIRST_NAME,
ACTOR.LAST_NAME
).mapping(::ActorName).`as`("actor_name")
// 문자열이 부담된다면 .`as`(ActorName::class.simpleName) 로도 가능
).from(FILM)
.join(FILM_ACTOR).on(FILM.FILM_ID.eq(FILM_ACTOR.FILM_ID))
.join(ACTOR).on(FILM_ACTOR.ACTOR_ID.eq(ACTOR.ACTOR_ID))
.limit(pageSize)
.offset((page - 1) * pageSize)
.fetchInto(FilmWithActorName::class.java)
}
테이블 인스턴스를 인자로 넘기는 것과 DSL.row(*TABLE.fields()) 를 인자로 넘기는 것의 차이는 테이블로부터 생성된 레코드인지 아니면 커스텀하게 생성된 레코드 인지 여부입니다.
테이블 객체를 넘기면 clllickme 님이 올려주신 fetch 후 map 하는 작업을 내부적으로 진행합니다.
다만, 코틀린에서는 커스텀하게 생성된 레코드들에 대해서 매칭을 하지 못해 내부적으로 null을 할당하다 에러가 발생한 것으로 보입니다.
(자세한 동작을 보고싶다면. jOOQ 내부의 DefaultRecordMapper의 ImmutablePOJOMapper를 보시면 됩니다.)
그래도 이부분은 크게 문제 없어보입니다. 테이블 컬럼 전체를 select 하는게 아니면 보통 제가 올린 답변의 예제처럼 .mapping으로 매핑 할 객체를 명시하기 때문에 답변대로 진행하면 큰 문제 없어보입니다.
안녕하세요.
안녕하세요. 확인해보니 코틀린에서 ExplicitPathJoin을 사용하면 jOOQ의 리플랙션을 사용한 레코드 매퍼가 의도한대로 매핑되지 않는것으로 보입니다.
코틀린에서 ExplicitPathJoin을 사용하려면 아래와 같이 리플랙션 없이 매핑하면 됩니다.
(전 flat하게 받지 않고 오브젝트로 묶는다면 리플랙션 없는 이 방식도 좋다고 생각합니다 ㅎㅎ)
fun findFilmWithActorsListExplicitPathJoin(page: Long, size: Long): List<FilmWithActor> {
val FILM = JFilm.FILM
// fetch 에서는 이 레코드 매퍼가 요구됨
val mapping: RecordMapper<Record3<FilmRecord, FilmActorRecord, ActorRecord>, FilmWithActor> = mapping(::FilmWithActor)
// 실제 우리가 사용하려는 레코드 매퍼 - 컴파일에러가 나기때문에
val mapping2: RecordMapper<Record3<Film, FilmActor, Actor>, FilmWithActor> = mapping(::FilmWithActor)
return dslContext.select(
FILM,
FILM.filmActor,
FILM.filmActor.actor
)
.from(FILM)
.join(FILM.filmActor())
.join(FILM.filmActor().actor())
.offset((page - 1) * size)
.limit(size)
.fetch(mapping(::filmWithActorMapper))
}
// 리플랙션 없이 별도의 컨터버 구현
private fun filmWithActorMapper(filmRecord: FilmRecord, filmActorRecord: FilmActorRecord, actorRecord: ActorRecord): FilmWithActor {
return FilmWithActor(
film = Film().apply { filmRecord.into(this) },
filmActor = FilmActor().apply { filmActorRecord.into(this) },
actor = Actor().apply { actorRecord.into(this) }
)
}
만약 이방식이 부담이 된다면, ExplicitPathJoin 을 사용하지말고, onKey()를 통해 join의 in 절을 생략 할 수 도 있습니다. (fk가 존재하는 경우, [참고])
(현재는 이게 최선으로 보이고, 추가사항 있으면 답글 남기도록 하겠습니다.)
fun findFilmWithActorsListExplicitPathJoinOnKey(page: Long, size: Long): List<FilmWithActor> {
val FILM = JFilm.FILM
val FILM_ACTOR = JFilmActor.FILM_ACTOR
val ACTOR = JActor.ACTOR
return dslContext.select(
FILM,
FILM_ACTOR,
ACTOR
)
.from(FILM)
.join(FILM_ACTOR).onKey() // on 절 생략
.join(ACTOR).onKey() // on 절 생략
.offset((page - 1) * size)
.limit(size)
.fetchInto(FilmWithActor::class.java)
}
-1
안녕하세요, 인프런 AI 인턴입니다.
강의 내용을 코틀린으로 전환하시며 발생한 에러와 관련해 문의하신 내용은 코틀린에서 데이터 클래스 매핑 과정에서 주의해야 할 부분과 관련이 있습니다. 코틀린에서는 불변성(immutable)을 기본으로 하여, 모든 클래스의 프로퍼티는 기본적으로 변경 불가능하게 설계됩니다. 데이터 클래스를 사용하여 객체를 생성할 때, 모든 프로퍼티는 생성자를 통해 한 번에 초기화되어야 합니다.
문의하신 코드에서는 FilmWithActor
클래스의 객체를 fetchInto()
메서드를 통해 직접 매핑하려 하셨고, 문제가 발생했습니다. fetchInto()
는 결과 집합의 각 행을 지정된 클래스의 인스턴스로 매핑합니다. 이 과정에서 코틀린의 널 안정성(null safety)과 기본값(default values) 사용을 고려해야 합니다.
에러 메시지는 FilmWithActor
클래스의 생성자에 전달되어야 하는 film
파라미터가 null로 전달되었다는 것을 가리키고 있습니다. 코틀린에서는 생성자나 함수 파라미터가 널이 될 수 없는 경우(널 가능성이 명시되지 않은 경우), 자동으로 널 체크를 수행하여 NullPointerException
을 발생시킵니다.
문제 해결 방법으로는, 마지막으로 시도하신 방법처럼 fetch()
메서드를 사용하여 결과 집합을 수동으로 FilmWithActor
객체로 매핑하는 접근 방식이 있습니다. 이 방식은 더 세밀한 제어를 가능하게 하지만 코드의 복잡성이 증가할 수 있습니다.
코틀린에서 Spring과 함께 작업할 때 객체 매핑과 관련된 주의 사항으로는, 클래스가 올바른 생성자를 갖추고 있어야 하고, 필요한 경우 기본값을 제공하거나 널 가능성을 명확히 해야 합니다. @PersistenceCreator
어노테이션을 사용하여 적절한 생성자를 명시적으로 지정하는 것도 좋은 방법입니다. 이는 Spring Data가 올바른 생성자를 선택하도록 도와줍니다.
Spring Data JDBC의 Kotlin 지원 문서에서는 Kotlin 클래스가 인스턴스화되는 방법, 불변 클래스 지원, 생성자 감지 방법 등에 대해 자세히 설명하고 있어 참고하시면 좋을 것 같습니다.
도움이 되기를 바랍니다!
답변 감사합니다!
위의 질문과 더불어 고민이 되었던 부분은 PathJoin을 사용하는 경우였습니다.
아래와 같이 작성하였을 때, 정상적으로 동작하기를 기대하였습니다. 하지만 위와 원인이 비슷할 것으로 생각되지만, 매핑이 제대로 되지 않았습니다.
여러가지 케이스를 작성해보다 아래와 같이 작성해야 정상 동작하는 것을 확인하였는데, 해당 경우도 아래의 경우가 최선의 방법일지 여쭙고 싶습니다 ..