Skip to content
This repository has been archived by the owner on Jan 20, 2023. It is now read-only.

Commit

Permalink
Merge pull request #33 from k163377/conversion_service
Browse files Browse the repository at this point in the history
Add ConversionService support.
  • Loading branch information
k163377 authored Apr 25, 2021
2 parents a15c934 + 44061df commit 84ed582
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 46 deletions.
13 changes: 8 additions & 5 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ val dst: Dst = jdbcTemplate.query(query, KRowMapper(::Dst, /* 必要に応じた

また、`KRowMapper`はデフォルトでは引数名によってカラムとの対応を見るため、「引数がキャメルケースでカラムはスネークケース」というような場合、引数名を変換する関数も渡す必要が有ります。

必要に応じて値の変換のために`ConversionService`を渡すこともできます。
渡さなかった場合、`DefaultConversionService.sharedInstance`がデフォルトとして利用されます。

### method reference(KFunction)からの初期化
`KRowMapper``method reference`から初期化できます。

Expand Down Expand Up @@ -230,13 +233,16 @@ val mapper: KRowMapper<Dst> = KRowMapper(::Dst, parameterNameConverter)
ただし、よりプレーンな`Kotlin`に近い書き方をしたい場合にはこれらの機能を用いず、呼び出し対象メソッドで全ての初期化処理を書くことをお勧めします。

### 値のデシリアライズ
`KRowMapper``java.sql.ResultSet`から値の取得を行うため、デフォルトではこの実装でサポートされていない型を取得することはできません。
この問題に対応するため、`KRowMapper`ではデフォルトの変換機能に加え以下の3種類のデシリアライズ方法を提供しています。
`KRowMapper``BeanPropertyRowMapper`同様`ConversionService`(デフォルトでは`DefaultConversionService.sharedInstance`)を用いたデシリアライズをサポートしています。

これに加え、より明示的で柔軟性の高いデシリアライズ方法として、`KRowMapper`では以下の3種類のデシリアライズ方法を提供しています。

1. `KColumnDeserializer`アノテーションを利用したデシリアライズ
2. デシリアライズアノテーションを自作してのデシリアライズ
3. 複数引数からのデシリアライズ

これらのデシリアライズ方法は`ConversionService`によるデシリアライズより優先的に適用されます。

#### KColumnDeserializerアノテーションを利用したデシリアライズ
自作のクラスで、かつ単一引数から初期化できる場合、`KColumnDeserializer`アノテーションを用いたデシリアライズが利用できます。
`KColumnDeserializer`アノテーションは、コンストラクタ、もしくは`companion object`に定義したファクトリーメソッドに対して付与できます。
Expand Down Expand Up @@ -470,6 +476,3 @@ class Foo(
val description: String = ""
)
```

#### Enumをデシリアライズする
DBに格納された値と`Enum::name`プロパティが一致する場合、特別な記述無しに`Enum`をデシリアライズすることができます。
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ Also, it supports the default arguments which are peculiar to `Kotlin`.
Also, by default, `KRowMapper` compares argument names and column names to see if they correspond.
Therefore, in the case of "argument name is `camelCase` and column name is `snake_case`", it is necessary to pass a function that appropriately converts the naming convention of the argument name.

You can also pass a `ConversionService` for value conversion if needed.
If you don't pass it, `DefaultConversionService.sharedInstance` will be used as default.

### Initialization from method reference(KFunction)
You can initialize `KRowMapper` from `method reference(KFunction)` as follows It is.

Expand Down Expand Up @@ -235,13 +238,16 @@ By using the contents described so far, you can perform more flexible and safe m
In addition, by making full use of the abundant functions provided by `KRowMapper`, further labor saving is possible.

### Deserialization
Since `KRowMapper` gets the value from `java.sql.ResultSet`, by default it is not possible to get the type which is not supported by this implementation.
To deal with this problem, `KRowMapper` provides the following three types of deserialization methods in addition to the default conversion function.
`KRowMapper` supports deserialization using the `ConversionService` (`DefaultConversionService.sharedInstance` by default).

In addition to this, as a more explicit and flexible deserialization method, `KRowMapper` provides the following three deserialization methods.

1. Deserialization by using the `KColumnDeserializer` annotation.
2. Deserialization by creating your own custom deserialization annotations.
3. Deserialization from multiple arguments.

These deserialization methods take precedence over deserialization by `ConversionService`.

#### Deserialization by using the KColumnDeserializer annotation
If it is a self-made class and can be initialized from a single argument, deserialization using the `KColumnDeserializer` annotation can be used.
`KColumnDeserializer` annotation can be used to `constructor` or `factory method` defined in `companion object`.
Expand Down Expand Up @@ -481,6 +487,3 @@ class Foo(
val description: String = ""
)
```

#### Deserialize Enum
If the value stored in the DB and the `Enum::name` property of the map destination are the same, it will be automatically converted to You can deserialize the `Enum`.
7 changes: 7 additions & 0 deletions src/main/kotlin/com/mapk/krowmapper/DummyConstructor.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package com.mapk.krowmapper

import org.springframework.core.convert.ConversionService
import com.mapk.krowmapper.KRowMapper as Mapper

@Suppress("FunctionName")
inline fun <reified T : Any> KRowMapper(
conversionService: ConversionService,
noinline parameterNameConverter: ((String) -> String)? = null
) = Mapper(T::class, conversionService, parameterNameConverter)

@Suppress("FunctionName")
inline fun <reified T : Any> KRowMapper(
noinline parameterNameConverter: ((String) -> String)? = null
Expand Down
36 changes: 28 additions & 8 deletions src/main/kotlin/com/mapk/krowmapper/KRowMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,42 @@ package com.mapk.krowmapper

import com.mapk.core.KFunctionForCall
import com.mapk.core.toKConstructor
import org.springframework.core.convert.ConversionService
import org.springframework.core.convert.support.DefaultConversionService
import org.springframework.jdbc.core.RowMapper
import java.sql.ResultSet
import kotlin.reflect.KClass
import kotlin.reflect.KFunction

class KRowMapper<T : Any> private constructor(private val function: KFunctionForCall<T>) : RowMapper<T> {
constructor(function: KFunction<T>, parameterNameConverter: ((String) -> String)? = null) : this(
KFunctionForCall(function, parameterNameConverter)
)
class KRowMapper<T : Any> private constructor(
private val function: KFunctionForCall<T>,
conversionService: ConversionService?
) : RowMapper<T> {
constructor(
function: KFunction<T>,
conversionService: ConversionService,
parameterNameConverter: ((String) -> String)? = null
) : this(KFunctionForCall(function, parameterNameConverter), conversionService)

constructor(clazz: KClass<T>, parameterNameConverter: ((String) -> String)? = null) : this(
clazz.toKConstructor(parameterNameConverter)
)
constructor(
function: KFunction<T>,
parameterNameConverter: ((String) -> String)? = null
) : this(KFunctionForCall(function, parameterNameConverter), null)

constructor(
clazz: KClass<T>,
conversionService: ConversionService,
parameterNameConverter: ((String) -> String)? = null
) : this(clazz.toKConstructor(parameterNameConverter), conversionService)

constructor(
clazz: KClass<T>,
parameterNameConverter: ((String) -> String)? = null
) : this(clazz.toKConstructor(parameterNameConverter), null)

private val conversionService: ConversionService = conversionService ?: DefaultConversionService.getSharedInstance()
private val parameters: List<ParameterForMap<*, *>> =
function.requiredParameters.map { ParameterForMap.newInstance(it) }
function.requiredParameters.map { ParameterForMap.newInstance(it, this.conversionService) }

override fun mapRow(rs: ResultSet, rowNum: Int): T {
val adaptor = function.getArgumentAdaptor()
Expand Down
40 changes: 24 additions & 16 deletions src/main/kotlin/com/mapk/krowmapper/ParameterForMap.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package com.mapk.krowmapper

import com.mapk.annotations.KColumnDeserializer
import com.mapk.core.EnumMapper
import com.mapk.core.KFunctionWithInstance
import com.mapk.core.ValueParameter
import com.mapk.core.getAnnotatedFunctions
import com.mapk.core.getAnnotatedFunctionsFromCompanionObject
import com.mapk.core.getKClass
import com.mapk.deserialization.AbstractKColumnDeserializer
import com.mapk.deserialization.KColumnDeserializeBy
import org.springframework.core.convert.ConversionException
import org.springframework.core.convert.ConversionService
import java.lang.IllegalArgumentException
import java.sql.ResultSet
import kotlin.reflect.KClass
Expand All @@ -23,12 +24,21 @@ internal sealed class ParameterForMap<S, D> {
abstract val name: String
abstract fun getObject(rs: ResultSet): D?

private class Plain<T>(override val name: String, val requiredClazz: Class<T>) : ParameterForMap<T, T>() {
override fun getObject(rs: ResultSet): T? = rs.getObject(name, requiredClazz)
}

private class Enum<D>(override val name: String, val enumClazz: Class<D>) : ParameterForMap<String, D>() {
override fun getObject(rs: ResultSet): D? = EnumMapper.getEnum(enumClazz, rs.getString(name))
private class Default<D>(
override val name: String,
val requiredClazz: Class<D>,
private val conversionService: ConversionService
) : ParameterForMap<Any, D>() {
override fun getObject(rs: ResultSet): D? = rs.getObject(name)?.let {
if (requiredClazz.isInstance(it))
@Suppress("UNCHECKED_CAST")
it as D?
else try {
conversionService.convert(it, requiredClazz)
} catch (ex: ConversionException) {
throw IllegalStateException("Could not find a method to deserialize for '$name' parameter.", ex)
}
}
}

private class Deserializer<S : Any, D>(
Expand All @@ -45,22 +55,20 @@ internal sealed class ParameterForMap<S, D> {
}

companion object {
fun <T : Any> newInstance(param: ValueParameter<T>): ParameterForMap<*, T> {
fun <T : Any> newInstance(
param: ValueParameter<T>,
conversionService: ConversionService
): ParameterForMap<*, T> {
param.getDeserializer()?.let {
return Deserializer(param.name, it)
}

param.requiredClazz.getDeserializer()?.let {
val targetClass = it.parameters.single().getKClass().javaObjectType
return Deserializer(param.name, targetClass, it)
val srcClass = it.parameters.single().getKClass().javaObjectType
return Deserializer(param.name, srcClass, it)
}

return param.requiredClazz.javaObjectType.let {
when (it.isEnum) {
true -> Enum(param.name, it)
false -> Plain(param.name, it)
}
}
return Default(param.name, param.requiredClazz.javaObjectType, conversionService)
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/test/kotlin/com/mapk/krowmapper/DefaultValueTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ class DefaultValueTest {
@DisplayName("デフォルト値を用いたマッピングテスト")
fun test() {
val resultSet = mockk<ResultSet>()
every { resultSet.getObject("foo_id", any<Class<*>>()) } returns 1
every { resultSet.getObject("foo_id") } returns 1
every { resultSet.getObject("bar_value", any<Class<*>>()) } returns "From result set."

val result = KRowMapper(Dst::class, this::camelToSnake).mapRow(resultSet, 0)

Assertions.assertEquals(1, result.fooId)
Assertions.assertEquals("default", result.barValue)

verify(exactly = 1) { resultSet.getObject("foo_id", Integer::class.java) }
verify(exactly = 1) { resultSet.getObject("foo_id") }
verify(exactly = 0) { resultSet.getObject("bar_value", String::class.java) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.mapk.krowmapper

import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.core.convert.ConversionService
import java.sql.ResultSet
import java.time.YearMonth

@DisplayName("ConversionServiceによるデシリアライズのテスト")
private class DeserializeByConversionServiceTest {
data class Dst(val yearMonth: YearMonth)

@Test
fun test() {
val conversionService = mockk<ConversionService> {
every { convert(202101, YearMonth::class.java) } returns (YearMonth.of(2021, 1))
}
val mapper = KRowMapper<Dst>(conversionService)
val resultSet = mockk<ResultSet> {
every { getObject("yearMonth") } returns 202101
}

assertEquals(Dst(YearMonth.of(2021, 1)), mapper.mapRow(resultSet, 0))
verify(exactly = 1) { conversionService.convert(202101, YearMonth::class.java) }
}
}
12 changes: 6 additions & 6 deletions src/test/kotlin/com/mapk/krowmapper/KParameterFlattenTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,16 @@ class KParameterFlattenTest {
@DisplayName("スネークケースsrc -> キャメルケースdst")
fun test() {
val resultSet = mockk<ResultSet>() {
every { getObject("baz_baz_foo_foo", any<Class<*>>()) } returns 1
every { getObject("baz_baz_bar_bar", any<Class<*>>()) } returns "str"
every { getObject("qux_qux", any<Class<*>>()) } returns LocalDateTime.MIN
every { getObject("baz_baz_foo_foo") } returns 1
every { getObject("baz_baz_bar_bar") } returns "str"
every { getObject("qux_qux") } returns LocalDateTime.MIN
}

val result = KRowMapper<Dst>(this::camelToSnake).mapRow(resultSet, 0)
assertEquals(expected, result)

verify(exactly = 1) { resultSet.getObject("baz_baz_foo_foo", Integer::class.java) }
verify(exactly = 1) { resultSet.getObject("baz_baz_bar_bar", String::class.java) }
verify(exactly = 1) { resultSet.getObject("qux_qux", LocalDateTime::class.java) }
verify(exactly = 1) { resultSet.getObject("baz_baz_foo_foo") }
verify(exactly = 1) { resultSet.getObject("baz_baz_bar_bar") }
verify(exactly = 1) { resultSet.getObject("qux_qux") }
}
}
8 changes: 4 additions & 4 deletions src/test/kotlin/com/mapk/krowmapper/SimpleMappingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ class SimpleMappingTest {
@DisplayName("スネークケースsrc -> キャメルケースdst")
fun test() {
val resultSet = mockk<ResultSet>()
every { resultSet.getObject("foo_id", any<Class<*>>()) } returns 1
every { resultSet.getObject("str_value", any<Class<*>>()) } returns "str"
every { resultSet.getObject("foo_id") } returns 1
every { resultSet.getObject("str_value") } returns "str"

val result = KRowMapper(::Dst, this::camelToSnake).mapRow(resultSet, 0)

assertEquals(1, result.fooId)
assertEquals("str", result.strValue)

verify(exactly = 1) { resultSet.getObject("foo_id", Integer::class.java) }
verify(exactly = 1) { resultSet.getObject("str_value", String::class.java) }
verify(exactly = 1) { resultSet.getObject("foo_id") }
verify(exactly = 1) { resultSet.getObject("str_value") }
}
}

0 comments on commit 84ed582

Please sign in to comment.