Skip to content

Commit

Permalink
Merge pull request #707 from erie0210/docs/add-value-class-guide
Browse files Browse the repository at this point in the history
docs: add guide to using value classes
  • Loading branch information
cj848 committed Jun 4, 2024
2 parents 5a1b5b3 + 0eb4a45 commit 3bdf3ba
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ out/
### NPM ###
package-lock.json
/node_modules/

### OS generated files ###
.DS_Store
153 changes: 153 additions & 0 deletions docs/ko/faq/how-do-i-use-kotlin-value-class.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Kotlin 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)

@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()
}
}
```

하지만 추가적인 설정 없이 Hibernate를 사용해 Kotlin JDSL을 통해 조회하면 에러가 발생합니다.

```
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 ValueClassAwareJpqlValueSerializer(
private val delegate: JpqlValueSerializer,
) : JpqlSerializer<JpqlValue<*>> {
override fun handledType(): KClass<JpqlValue<*>> {
return JpqlValue::class
}

override fun serialize(
part: JpqlValue<*>,
writer: JpqlWriter,
context: RenderContext,
) {
val value = part.value

if (value::class.isValue) {
writer.writeParam(value::class.memberProperties.first().getter.call(value))
return
}

delegate.serialize(part, writer, context)
}
}
```

이제 이 클래스를 `RenderContext`에 추가해야 합니다.
추가하는 방법은 [다음 문서](../jpql-with-kotlin-jdsl/custom-dsl.md#serializer)를 참조할 수 있습니다.
만약 스프링 부트를 사용하는 경우 다음과 같은 코드를 통해 커스텀 Seriziler를 Bean으로 등록하면 됩니다.

```kotlin
@Configuration
class CustomJpqlRenderContextConfig {
@Bean
fun jpqlSerializer(): JpqlSerializer<*> {
return ValueClassAwareJpqlValueSerializer(JpqlValueSerializer())
}
}
```

### custom method 사용

JDSL에서 제공하는 [custom dsl](../jpql-with-kotlin-jdsl/custom-dsl.md#dsl) 사용해 value class 에 사용되는 매서드를 추가할 수 있습니다.

```kotlin
class CustomJpql : Jpql() {
fun Expressionable<UserId>.equalValue(value: UserId): Predicate {
return Predicates.equal(this.toExpression(), Expressions.value(value.value))
}
}

val query = jpql(CustomJpql) {
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 CustomJpql : Jpql() {
fun <T: PrimaryLongId> Expressionable<T>.equal(value: T): Predicate {
return Predicates.equal(this.toExpression(), Expressions.value(value.value))
}
}
```

### 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<ResponseDto>(
entity(User::id)
).from(
entity(User::class),
).where(
path(User::id).equalValue(userId)
)
}
```

0 comments on commit 3bdf3ba

Please sign in to comment.