From 49aaaaed1bb0b50a139f79119512c4a7f13fd513 Mon Sep 17 00:00:00 2001 From: erie0210 <2j00923@gmail.com> Date: Fri, 17 May 2024 09:42:35 +0900 Subject: [PATCH 1/3] docs: add guide to using value classes Co-authored-by: Jake Son --- .gitignore | 3 + .../value-class-informal-guidance..md | 141 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 docs/ko/jpql-with-kotlin-jdsl/value-class-informal-guidance..md diff --git a/.gitignore b/.gitignore index 37bd9a042..8f594cb57 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ out/ ### NPM ### package-lock.json /node_modules/ + +### OS generated files ### +.DS_Store diff --git a/docs/ko/jpql-with-kotlin-jdsl/value-class-informal-guidance..md b/docs/ko/jpql-with-kotlin-jdsl/value-class-informal-guidance..md new file mode 100644 index 000000000..9a2e2eb97 --- /dev/null +++ b/docs/ko/jpql-with-kotlin-jdsl/value-class-informal-guidance..md @@ -0,0 +1,141 @@ +## Value Class + +엔티티의 프로퍼티를 kotlin의 [`value class`](https://kotlinlang.org/docs/inline-classes.html)로 선언할 수 있습니다. + +```kotlin +@Entity +class User( + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + val id: UserId = UserId(0), +) + +@JvmInline +value class UserId(private val value: Long) + +``` + +hibernate를 사용해 Kotlin JDSL을 통해 조회 시 에러가 발생합니다. + +```kotlin +@Service +class UserService( + private val jpqlRenderContext: JpqlRenderContext, + private val entityManager: EntityManager, +) { + + fun findById(userId: UserId): User? { + val query = jpql { + select( + entity(User::class) + ).from( + entity(User::class), + ).where( + path(User::id).equal(userId) // 에러 발생 지점 + ) + } + + return entityManager.createQuery(query, jpqlRenderContext).apply { maxResults = 1 }.resultList.firstOrNull() + } +} +``` + +``` +org.hibernate.type.descriptor.java.CoercionException: Cannot coerce value 'UserId(value=1)' [com.example.entity.UserId] to Long +... +``` + +이를 해결하려면 Kotlin JDSL이 매개 변수로 전달되는 `value class`의 unboxing이 필요합니다. +unboxing은 다음 방안 중 하나를 선택해서 수행할 수 있습니다. + +### JpqlValue용 커스텀 JpqlSerializer + +에러를 해결하기 위해 `EntityManager`에 인자들을 `value class` 그 자체로 넘기지 않고 unboxing한 값을 넘겨야합니다. +Kotlin JDSL은 `JpqlValueSerializer` 클래스에서 인자들을 추출하는 역할을 담당합니다. +따라서 기본 제공하는 클래스 대신 커스텀 Seriailzer를 등록해야 합니다. + +먼저 다음과 같은 커스텀 Seriailzer를 생성합니다. + +```kotlin +class CustomJpqlValueSerializer : JpqlSerializer> { + override fun handledType(): KClass> { + return JpqlValue::class + } + + override fun serialize( + part: JpqlValue<*>, + writer: JpqlWriter, + context: RenderContext, + ) { + val value = part.value + + // value class이면 relfection을 사용해 내부 값을 꺼내서 전달 + if (value::class.isValue) { + val property = value::class.memberProperties.first() + val propertyValue = property.getter.call(value) + + writer.writeParam(propertyValue) + return + } + + if (value is KClass<*>) { + val introspector = context.getValue(JpqlRenderIntrospector) + val entity = introspector.introspect(value) + + writer.write(entity.name) + } else { + writer.writeParam(part.value) + } + } +} +``` + +이제 이 클래스를 `RenderContext`에 추가해야 합니다. +추가하는 방법은 [다음 문서](custom-dsl.md#serializer)를 참조할 수 있습니다. +만약 스프링 부트를 사용하는 경우 다음과 같은 코드를 통해 커스텀 Seriziler를 Bean으로 등록하면 됩니다. + +```kotlin +@Configuration +class CustomJpqlRenderContextConfig { + @Bean + fun jpqlSerializer(): JpqlSerializer<*> { + return CustomJpqlValueSerializer() + } +} +``` + +### custom method 사용 + +JDSL에서 제공하는 [custom dsl](custom-dsl.md#dsl) 사용해 value class 에 사용되는 매서드를 추가할 수 있습니다. + +```kotlin +class JDSLConfig : Jpql() { + fun Expressionable.equalValue(value: UserId): Predicate { + return Predicates.equal(this.toExpression(), Expressions.value(value.value)) + } +} + +val query = jpql(JDSLConfig) { + select( + entity(User::class) + ).from( + entity(User::class), + ).where( + path(User::id).equalValue(userId) + ) +} +``` + +interface 도입과 오버로딩을 통해 다양한 value class에 대응할 수 있습니다. + +```kotlin +interface PrimaryLongId { val value: Long } + +value class UserId(override val value: Long) : PrimaryLongId + +class JDSLConfig : Jpql() { + fun Expressionable.equal(value: T): Predicate { + return Predicates.equal(this.toExpression(), Expressions.value(value.value)) + } +} +``` From 512a600d75d3e9f4064760a373c0cb1abbb83022 Mon Sep 17 00:00:00 2001 From: erie0210 <2j00923@gmail.com> Date: Wed, 22 May 2024 11:39:38 +0900 Subject: [PATCH 2/3] fix: content reinforcement Co-authored-by: Jake Son --- .../how-do-i-use-kotlin-value-class.md} | 42 +++++++------------ 1 file changed, 15 insertions(+), 27 deletions(-) rename docs/ko/{jpql-with-kotlin-jdsl/value-class-informal-guidance..md => faq/how-do-i-use-kotlin-value-class.md} (72%) diff --git a/docs/ko/jpql-with-kotlin-jdsl/value-class-informal-guidance..md b/docs/ko/faq/how-do-i-use-kotlin-value-class.md similarity index 72% rename from docs/ko/jpql-with-kotlin-jdsl/value-class-informal-guidance..md rename to docs/ko/faq/how-do-i-use-kotlin-value-class.md index 9a2e2eb97..7a38e2245 100644 --- a/docs/ko/jpql-with-kotlin-jdsl/value-class-informal-guidance..md +++ b/docs/ko/faq/how-do-i-use-kotlin-value-class.md @@ -1,4 +1,4 @@ -## Value Class +# Kotlin value class 를 사용하려면 어떻게 해야할까요? 엔티티의 프로퍼티를 kotlin의 [`value class`](https://kotlinlang.org/docs/inline-classes.html)로 선언할 수 있습니다. @@ -13,11 +13,6 @@ class User( @JvmInline value class UserId(private val value: Long) -``` - -hibernate를 사용해 Kotlin JDSL을 통해 조회 시 에러가 발생합니다. - -```kotlin @Service class UserService( private val jpqlRenderContext: JpqlRenderContext, @@ -31,7 +26,7 @@ class UserService( ).from( entity(User::class), ).where( - path(User::id).equal(userId) // 에러 발생 지점 + path(User::id).equal(userId) ) } @@ -40,6 +35,8 @@ class UserService( } ``` +하지만 추가적인 설정 없이 Hibernate를 사용해 Kotlin JDSL을 통해 조회하면 에러가 발생합니다. + ``` org.hibernate.type.descriptor.java.CoercionException: Cannot coerce value 'UserId(value=1)' [com.example.entity.UserId] to Long ... @@ -57,7 +54,9 @@ Kotlin JDSL은 `JpqlValueSerializer` 클래스에서 인자들을 추출하는 먼저 다음과 같은 커스텀 Seriailzer를 생성합니다. ```kotlin -class CustomJpqlValueSerializer : JpqlSerializer> { +class ValueClassAwareJpqlValueSerializer( + private val delegate: JpqlValueSerializer, +) : JpqlSerializer> { override fun handledType(): KClass> { return JpqlValue::class } @@ -69,29 +68,18 @@ class CustomJpqlValueSerializer : JpqlSerializer> { ) { val value = part.value - // value class이면 relfection을 사용해 내부 값을 꺼내서 전달 if (value::class.isValue) { - val property = value::class.memberProperties.first() - val propertyValue = property.getter.call(value) - - writer.writeParam(propertyValue) + writer.writeParam(value::class.memberProperties.first().getter.call(value)) return } - if (value is KClass<*>) { - val introspector = context.getValue(JpqlRenderIntrospector) - val entity = introspector.introspect(value) - - writer.write(entity.name) - } else { - writer.writeParam(part.value) - } + delegate.serialize(part, writer, context) } } ``` 이제 이 클래스를 `RenderContext`에 추가해야 합니다. -추가하는 방법은 [다음 문서](custom-dsl.md#serializer)를 참조할 수 있습니다. +추가하는 방법은 [다음 문서](../jpql-with-kotlin-jdsl/custom-dsl.md#serializer)를 참조할 수 있습니다. 만약 스프링 부트를 사용하는 경우 다음과 같은 코드를 통해 커스텀 Seriziler를 Bean으로 등록하면 됩니다. ```kotlin @@ -99,23 +87,23 @@ class CustomJpqlValueSerializer : JpqlSerializer> { class CustomJpqlRenderContextConfig { @Bean fun jpqlSerializer(): JpqlSerializer<*> { - return CustomJpqlValueSerializer() + return ValueClassAwareJpqlValueSerializer(JpqlValueSerializer()) } } ``` ### custom method 사용 -JDSL에서 제공하는 [custom dsl](custom-dsl.md#dsl) 사용해 value class 에 사용되는 매서드를 추가할 수 있습니다. +JDSL에서 제공하는 [custom dsl](../jpql-with-kotlin-jdsl/custom-dsl.md#dsl) 사용해 value class 에 사용되는 매서드를 추가할 수 있습니다. ```kotlin -class JDSLConfig : Jpql() { +class CustomJpql : Jpql() { fun Expressionable.equalValue(value: UserId): Predicate { return Predicates.equal(this.toExpression(), Expressions.value(value.value)) } } -val query = jpql(JDSLConfig) { +val query = jpql(CustomJpql) { select( entity(User::class) ).from( @@ -133,7 +121,7 @@ interface PrimaryLongId { val value: Long } value class UserId(override val value: Long) : PrimaryLongId -class JDSLConfig : Jpql() { +class CustomJpql : Jpql() { fun Expressionable.equal(value: T): Predicate { return Predicates.equal(this.toExpression(), Expressions.value(value.value)) } From 0eb4a45edbcce6383729c530c02e0e718acf07a5 Mon Sep 17 00:00:00 2001 From: erie0210 <2j00923@gmail.com> Date: Fri, 31 May 2024 18:56:13 +0900 Subject: [PATCH 3/3] docs: add dto projection docs for value class Co-authored-by: Jake Son --- .../ko/faq/how-do-i-use-kotlin-value-class.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/ko/faq/how-do-i-use-kotlin-value-class.md b/docs/ko/faq/how-do-i-use-kotlin-value-class.md index 7a38e2245..b84a34db0 100644 --- a/docs/ko/faq/how-do-i-use-kotlin-value-class.md +++ b/docs/ko/faq/how-do-i-use-kotlin-value-class.md @@ -127,3 +127,27 @@ class CustomJpql : Jpql() { } } ``` + +### DTO Projection 시 주의사항 + +DTO Projection 에서 value class를 사용하는 경우 해당 프로퍼티가 nullable 한 경우에 지원되지 않습니다. +따라서 DTO Projection에서 직접 value class를 사용하는 것보다, 기본 자료형을 사용하고 조회 후에 변환하는 것을 권장합니다. + +```kotlin +data class ResponseDto( + private val rawId: Long, +) { + val id: UserId + get() = UserId(rawId) +} + +val query = jpql(CustomJpql) { + selectNew( + entity(User::id) + ).from( + entity(User::class), + ).where( + path(User::id).equalValue(userId) + ) +} +```