KRowMapper
はKotlin
向けのRowMapper
であり、以下の機能を提供します。
BeanPropertyRowMapper
と同等の、最小限の労力でのオブジェク関係トマッピング(ORM
)BeanPropertyRowMapper
より高速なマッピング- リフレクションを用いた関数呼び出しベースの柔軟で安全なマッピング
SpringFramework 5.3
/SpringBoot 2.4
にて、コンストラクタ呼び出しでマッピングを行うDataClassRowMapper
が追加されました。
外部ライブラリを利用する程でもない場合はこちらの利用も検討するようお願いします。
手動でマッピングコードを書いた場合とKRowMapper
を用いた場合を比較します。
手動で書く場合引数が多ければ多いほど記述がかさみますが、KRowMapper
を用いることで殆どコードを書かずにマッピングを行えます。
また、外部の設定ファイルは一切必要ありません。
ただし、引数の命名規則とDBのカラムの命名規則が異なる場合は命名変換関数を渡す必要が有る点にご注意ください(後述)。
// マップ対象クラス
data class Dst(
foo: String,
bar: String,
baz: Int?,
...
)
// 手動でRowMapperを書いた場合
val dst: Dst = jdbcTemplate.query(query) { rs, _ ->
Dst(
rs.getString("foo"),
rs.getString("bar"),
rs.getInt("baz"),
...
)
}
// KRowMapperを用いた場合
val dst: Dst = jdbcTemplate.query(query, KRowMapper(::Dst, /* 必要に応じた命名変換関数 */))
KRowMapper
はJitPack
にて公開しており、Maven
やGradle
といったビルドツールから手軽に利用できます。
各ツールでの正確なインストール方法については下記をご参照ください。
以下はMaven
でのインストール例です。
1. JitPackのリポジトリへの参照を追加する
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
2. dependencyを追加する
<dependency>
<groupId>com.github.ProjectMapK</groupId>
<artifactId>KRowMapper</artifactId>
<version>Tag</version>
</dependency>
KRowMapper
は以下のように動作します。
- 呼び出し対象の
KFunction
を取り出す KFunction
を解析し、必要な引数とその取り出し方・デシリアライズ方法を決定するResultSet
からそれぞれの引数に対応する値の取り出し・デシリアライズを行い、KFunction
を呼び出す
最終的にはコンストラクタやcompanion object
に定義したファクトリーメソッドなどを呼び出してマッピングを行うため、結果はKotlin
上の引数・nullability
等の制約に従います。
つまり、Kotlin
のnull
安全が壊れることによる実行時エラーは発生しません(デシリアライズ方法によっては、型引数のnullability
に関してnull
安全が壊れる場合が有ります)。
また、Kotlin
特有の機能であるデフォルト引数等にも対応しています。
KRowMapper
は呼び出し対象のmethod reference(KFunction)
、またはマッピング先のKClass
から初期化できます。
また、KRowMapper
はデフォルトでは引数名によってカラムとの対応を見るため、「引数がキャメルケースでカラムはスネークケース」というような場合、引数名を変換する関数も渡す必要が有ります。
必要に応じて値の変換のためにConversionService
を渡すこともできます。
渡さなかった場合、DefaultConversionService.sharedInstance
がデフォルトとして利用されます。
KRowMapper
はmethod reference
から初期化できます。
data class Dst(
foo: String,
bar: String,
baz: Int?,
...
)
// コンストラクタのメソッドリファレンスを取得
val dstConstructor: KFunction<Dst> = ::Dst
// KFunctionからKRowMapperを初期化
val mapper: KRowMapper<Dst> = KRowMapper(dstConstructor)
ユースケースとしては特に以下の3種類のmethod reference
を利用することが大半だと思われます。
- コンストラクタのメソッドリファレンス:
::Dst
companion object
からのメソッドリファレンス:(Dst)::factoryMethod
this
に定義されたメソッドのメソッドリファレンス:this::factoryMethod
KRowMapper
はKClass
からも初期化できます。
デフォルトではプライマリーコンストラクタが呼び出し対象になります。
data class Dst(...)
val mapper: KRowMapper<Dst> = KRowMapper(Dst::class)
ダミーコンストラクタを用いることで以下のようにも書けます。
val mapper: KRowMapper<Dst> = KRowMapper<Dst>()
KClass
から初期化を行う場合、KConstructor
アノテーションを用いて呼び出し対象の関数を指定することができます。
以下の例ではセカンダリーコンストラクタが呼び出されます。
data class Dst(...) {
@KConstructor
constructor(...) : this(...)
}
val mapper: KRowMapper<Dst> = KRowMapper(Dst::class)
同様に、以下の例ではファクトリーメソッドが呼び出されます。
data class Dst(...) {
companion object {
@KConstructor
fun factory(...): Dst {
...
}
}
}
val mapper: KRowMapper<Dst> = KRowMapper(Dst::class)
KRowMapper
は、デフォルトでは引数名に対応するカラムをそのまま探すという挙動になります。
data class Dst(
fooFoo: String,
barBar: String,
bazBaz: Int?
)
// fooFoo, barBar, bazBazの3引数が要求される
val mapper: KRowMapper<Dst> = KRowMapper(::Dst)
// 挙動としては以下と同等
val rowMapper: RowMapper<Dst> = { rs, _ ->
Dst(
rs.getString("fooFoo"),
rs.getString("barBar"),
rs.getInt("bazBaz"),
)
}
一方、引数の命名規則がキャメルケースかつDBのカラムの命名規則がスネークケースというような場合、このままでは一致を見ることができません。
このような状況ではKRowMapper
の初期化時に命名変換関数を渡す必要が有ります。
val mapper: KRowMapper<Dst> = KRowMapper(::Dst) { fieldName: String ->
/* 命名変換処理 */
}
また、当然ながらラムダ内で任意の変換処理を行うこともできます。
KRowMapper
では命名変換処理を提供していませんが、Spring
やそれを用いたプロジェクトの中で用いられるライブラリでは命名変換処理が提供されている場合が有ります。
Jackson
、Guava
の2つのライブラリで実際に「キャメルケース -> スネークケース」の変換処理を渡すサンプルコードを示します。
import com.fasterxml.jackson.databind.PropertyNamingStrategy
val parameterNameConverter: (String) -> String = PropertyNamingStrategy.SnakeCaseStrategy()::translate
val mapper: KRowMapper<Dst> = KRowMapper(::Dst, parameterNameConverter)
import com.google.common.base.CaseFormat
val parameterNameConverter: (String) -> String = { fieldName: String ->
CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, fieldName)
}
val mapper: KRowMapper<Dst> = KRowMapper(::Dst, parameterNameConverter)
ここまでに記載した内容を用いることでBeanPropertyRowMapper
以上の柔軟で安全なマッピングを行えますが、KRowMapper
の提供する豊富な機能を使いこなすことで、更なる労力の削減が可能です。
ただし、よりプレーンなKotlin
に近い書き方をしたい場合にはこれらの機能を用いず、呼び出し対象メソッドで全ての初期化処理を書くことをお勧めします。
KRowMapper
はBeanPropertyRowMapper
同様ConversionService
(デフォルトではDefaultConversionService.sharedInstance
)を用いたデシリアライズをサポートしています。
これに加え、より明示的で柔軟性の高いデシリアライズ方法として、KRowMapper
では以下の3種類のデシリアライズ方法を提供しています。
KColumnDeserializer
アノテーションを利用したデシリアライズ- デシリアライズアノテーションを自作してのデシリアライズ
- 複数引数からのデシリアライズ
これらのデシリアライズ方法はConversionService
によるデシリアライズより優先的に適用されます。
自作のクラスで、かつ単一引数から初期化できる場合、KColumnDeserializer
アノテーションを用いたデシリアライズが利用できます。
KColumnDeserializer
アノテーションは、コンストラクタ、もしくはcompanion object
に定義したファクトリーメソッドに対して付与できます。
// プライマリーコンストラクタに付与した場合
data class FooId @KColumnDeserializer constructor(val id: Int)
// セカンダリーコンストラクタに付与した場合
data class FooId(val id: Int) {
@KColumnDeserializer
constructor(id: String) : this(id.toInt())
}
// ファクトリーメソッドに付与した場合
data class FooId(val id: Int) {
companion object {
@KColumnDeserializer
fun of(id: String): FooId = FooId(id.toInt())
}
}
KColumnDeserializer
アノテーションが設定されているクラスは、特別な記述をしなくても引数としてマッピングが可能です。
// fooIdにKColumnDeserializerが付与されていればDstでは何もせずに正常にマッピングができる
data class Dst(
fooId: FooId,
bar: String,
baz: Int?,
...
)
KColumnDeserializer
を用いることができない場合、デシリアライズアノテーションを自作してパラメータに付与することでデシリアライズを行うことができます。
デシリアライズアノテーションの自作はデシリアライズアノテーションとデシリアライザーの組を定義することで行います。
例としてString
からLocalDateTime
にデシリアライズを行うLocalDateTimeDeserializer
の作成の様子を示します。
@Target(AnnotationTarget.VALUE_PARAMETER)
とKColumnDeserializeBy
アノテーション、他幾つかのアノテーションを付与することで、デシリアライズアノテーションを定義できます。
KColumnDeserializeBy
アノテーションの引数は、後述するデシリアライザーのKClass
を渡します。
この例ではLocalDateTimeDeserializerImpl
がそれです。
また、この例ではアノテーションに引数を定義していますが、この値はデシリアライザーから参照することができます。
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Target(AnnotationTarget.VALUE_PARAMETER)
@KColumnDeserializeBy(LocalDateTimeDeserializerImpl::class)
annotation class LocalDateTimeDeserializer(val pattern: String = "yyyy-MM-dd'T'HH:mm:ss")
デシリアライザーはAbstractKColumnDeserializer<A, S, D>
を継承して定義します。
ジェネリクスA
,S
,D
はそれぞれ以下の意味が有ります。
A
: デシリアライズアノテーションのType
S
: デシリアライズ前のType
D
: デシリアライズ後のType
class LocalDateTimeDeserializerImpl(
annotation: LocalDateTimeDeserializer
) : AbstractKColumnDeserializer<LocalDateTimeDeserializer, String, LocalDateTime>(annotation) {
private val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern(annotation.pattern)
override val srcClass: Class<String> = String::class.javaObjectType
override fun deserialize(source: String): LocalDateTime = LocalDateTime.parse(source, formatter)
}
デシリアライザーのプライマリコンストラクタの引数はデシリアライズアノテーションのみ取る必要が有ります。
これはKRowMapper
の初期化時に呼び出されます。
例の通り、アノテーションに定義した引数は適宜参照することができます。
ここまでで定義したデシリアライズアノテーションとデシリアライザーをまとめて書くと以下のようになります。
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Target(AnnotationTarget.VALUE_PARAMETER)
@KColumnDeserializeBy(LocalDateTimeDeserializerImpl::class)
annotation class LocalDateTimeDeserializer(val pattern: String = "yyyy-MM-dd'T'HH:mm:ss")
class LocalDateTimeDeserializerImpl(
annotation: LocalDateTimeDeserializer
) : AbstractKColumnDeserializer<LocalDateTimeDeserializer, String, LocalDateTime>(annotation) {
private val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern(annotation.pattern)
override val srcClass: Class<String> = String::class.javaObjectType
override fun deserialize(source: String): LocalDateTime = LocalDateTime.parse(source, formatter)
}
これを付与すると以下のようになります。
pattern
には任意の引数が渡せるため、柔軟性が高いことが分かります。
data class Dst(
@LocalDateTimeDeserializer(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val createTime: LocalDateTime
)
以下のように、InnerDst
が複数引数を要求している場合、そのままではKRwoMapper
を用いてDst
をマッピングすることはできません。
このように複数引数を要求するようなクラスは、KParameterFlatten
アノテーションを用いることでデシリアライズできます。
data class InnerDst(val fooFoo: Int, val barBar: String)
data class Dst(val bazBaz: InnerDst, val quxQux: LocalDateTime)
DB
のカラム名がスネークケースであり、引数名をプレフィックスに指定する場合、以下のように付与します。
ここで、KParameterFlatten
を指定されたクラスは、前述のKConstructor
アノテーションで指定した関数またはプライマリコンストラクタから初期化されます。
data class InnerDst(val fooFoo: Int, val barBar: String)
data class Dst(
@KParameterFlatten(nameJoiner = NameJoiner.Snake::class)
val bazBaz: InnerDst,
val quxQux: LocalDateTime
)
// baz_baz_foo_foo, baz_baz_bar_bar, qux_quxの3引数が要求される
val mapper: KRowMapper<Dst> = KRowMapper(::Dst) { /* キャメル -> スネークの命名変換関数 */ }
KParameterFlatten
アノテーションはネストしたクラスの引数名の扱いについて2つのオプションを持ちます。
KParameterFlatten
アノテーションはデフォルトでは引数名をプレフィックスに置いた名前で一致を見ようとします。
引数名をプレフィックスに付けたくない場合はfieldNameToPrefix
オプションにfalse
を指定します。
data class InnerDst(val fooFoo: Int, val barBar: String)
data class Dst(
@KParameterFlatten(fieldNameToPrefix = false)
val bazBaz: InnerDst,
val quxQux: LocalDateTime
)
// foo_foo, bar_bar, qux_quxの3引数が要求される
val mapper: KRowMapper<Dst> = KRowMapper(::Dst) { /* キャメル -> スネークの命名変換関数 */ }
fieldNameToPrefix = false
を指定した場合、nameJoiner
オプションは無視されます。
nameJoiner
は引数名と引数名の結合方法の指定で、デフォルトではcamelCase
が指定されており、snake_case
とkebab-case
のサポートも有ります。
NameJoiner
クラスを継承したobject
を作成することで自作することもできます。
KParameterFlatten
アノテーションを付与した場合も、これまでに紹介したデシリアライズ方法は全て機能します。
また、InnerDst
の中で更にKParameterFlatten
アノテーションを利用することもできます。
ここまでの内容をまとめたデシリアライズ方法の早見です。
- 1つの値から複数の引数に変換したい
- コンストラクタ/ファクトリーメソッドで変換処理を書く
- 1つの値から1つの引数に変換したい
- コンストラクタ/ファクトリーメソッドで変換処理を書く
KColumnDeserializer
アノテーションを用いる- デシリアライズアノテーションを自作して付与する
- (
KParameterFlatten
アノテーションを用いる)
- 複数の値から1つの引数に変換したい
- コンストラクタ/ファクトリーメソッドで変換処理を書く
KParameterFlatten
アノテーションを用いる
以下のように、引数名とカラム名とで名前の定義が食い違う場合が有ります。
// idフィールドはDB上ではfoo_idという名前で登録されている
data class Foo(val id: Int)
このような場合、KParameterAlias
アノテーションを用いることで、DB
上のカラム名に合わせたマッピングが可能になります。
data class Foo(
@param:KParameterAlias("fooId")
val id: Int
)
KParameterAlias
で設定したエイリアスにも引数名の変換が適用されます。
KRowMapper
では、特定の場面においてデフォルト引数を用いることができます。
DBから取得した値を用いず、必ずデフォルト引数を用いたい場合、KUseDefaultArgument
アノテーションを利用できます。
class Foo(
...,
@KUseDefaultArgument
val description: String = ""
)
KRowMapper
でResultSet
に存在しないフィールドを取得しようとした場合、通常では例外で落ちますが、KUseDefaultArgument
アノテーションを付与している場合取得処理そのものが行われません。
これを応用することで、正常にマッピングを行うこともできます。
取得結果がnull
であればデフォルト引数を用いたいという場合、KParameterRequireNonNull
アノテーションを利用できます。
class Foo(
...,
@KParameterRequireNonNull
val description: String = ""
)