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

Latest commit

 

History

History
478 lines (361 loc) · 20.5 KB

README.ja.md

File metadata and controls

478 lines (361 loc) · 20.5 KB

License CircleCI codecov

KRowMapper

KRowMapperKotlin向けのRowMapperであり、以下の機能を提供します。

  • BeanPropertyRowMapperと同等の、最小限の労力でのオブジェク関係トマッピング(ORM
  • BeanPropertyRowMapperより高速なマッピング
  • リフレクションを用いた関数呼び出しベースの柔軟で安全なマッピング

Notes.

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, /* 必要に応じた命名変換関数 */))

インストール方法

KRowMapperJitPackにて公開しており、MavenGradleといったビルドツールから手軽に利用できます。
各ツールでの正確なインストール方法については下記をご参照ください。

Mavenでのインストール方法

以下は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は以下のように動作します。

  1. 呼び出し対象のKFunctionを取り出す
  2. KFunctionを解析し、必要な引数とその取り出し方・デシリアライズ方法を決定する
  3. ResultSetからそれぞれの引数に対応する値の取り出し・デシリアライズを行い、KFunctionを呼び出す

最終的にはコンストラクタやcompanion objectに定義したファクトリーメソッドなどを呼び出してマッピングを行うため、結果はKotlin上の引数・nullability等の制約に従います。
つまり、Kotlinnull安全が壊れることによる実行時エラーは発生しません(デシリアライズ方法によっては、型引数のnullabilityに関してnull安全が壊れる場合が有ります)。

また、Kotlin特有の機能であるデフォルト引数等にも対応しています。

KRowMapperの初期化

KRowMapperは呼び出し対象のmethod reference(KFunction)、またはマッピング先のKClassから初期化できます。

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

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

method reference(KFunction)からの初期化

KRowMappermethod 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

KClassからの初期化

KRowMapperKClassからも初期化できます。
デフォルトではプライマリーコンストラクタが呼び出し対象になります。

data class Dst(...)

val mapper: KRowMapper<Dst> = KRowMapper(Dst::class)

ダミーコンストラクタを用いることで以下のようにも書けます。

val mapper: KRowMapper<Dst> = KRowMapper<Dst>()

KConstructorアノテーションによる呼び出し対象指定

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やそれを用いたプロジェクトの中で用いられるライブラリでは命名変換処理が提供されている場合が有ります。
JacksonGuavaの2つのライブラリで実際に「キャメルケース -> スネークケース」の変換処理を渡すサンプルコードを示します。

Jackson
import com.fasterxml.jackson.databind.PropertyNamingStrategy

val parameterNameConverter: (String) -> String = PropertyNamingStrategy.SnakeCaseStrategy()::translate
val mapper: KRowMapper<Dst> = KRowMapper(::Dst, parameterNameConverter)
Guava
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に近い書き方をしたい場合にはこれらの機能を用いず、呼び出し対象メソッドで全ての初期化処理を書くことをお勧めします。

値のデシリアライズ

KRowMapperBeanPropertyRowMapper同様ConversionService(デフォルトではDefaultConversionService.sharedInstance)を用いたデシリアライズをサポートしています。

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

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

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

KColumnDeserializerアノテーションを利用したデシリアライズ

自作のクラスで、かつ単一引数から初期化できる場合、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アノテーションのオプション

KParameterFlattenアノテーションはネストしたクラスの引数名の扱いについて2つのオプションを持ちます。

fieldNameToPrefix

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

nameJoinerは引数名と引数名の結合方法の指定で、デフォルトではcamelCaseが指定されており、snake_casekebab-caseのサポートも有ります。
NameJoinerクラスを継承したobjectを作成することで自作することもできます。

他のデシリアライズ方法との併用

KParameterFlattenアノテーションを付与した場合も、これまでに紹介したデシリアライズ方法は全て機能します。
また、InnerDstの中で更に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 = ""
)

KRowMapperResultSetに存在しないフィールドを取得しようとした場合、通常では例外で落ちますが、KUseDefaultArgumentアノテーションを付与している場合取得処理そのものが行われません。
これを応用することで、正常にマッピングを行うこともできます。

取得結果がnullの場合デフォルト引数を用いる

取得結果がnullであればデフォルト引数を用いたいという場合、KParameterRequireNonNullアノテーションを利用できます。

class Foo(
    ...,
    @KParameterRequireNonNull
    val description: String = ""
)