【SpringJdbc】DataClassRowMapperを用いコンストラクタ呼び出しでマッピングを行う【Java/Kotlin】

`SpringFramework 5.3`/`SpringBoot 2.4`にて、コンストラクタ呼び出しでマッピングを行う`DataClassRowMapper`が追加されました。

https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/DataClassRowMapper.html

これを用いることで、`BeanPropertyRowMapper`と同様の書き味で`Kotlin`の`data class`へのマッピングも行うことができます。
実装としても`BeanPropertyRowMapper`を継承したものであるため、移行はシームレスに行えると思います。

# 実際にマッピングしてみる
簡単な検証コードで動作を紹介します。

以下が検証用コードの全体です（長いため折りたたみます）。
内容としては、`Kotlin`の`data class`である`Foo`に関してマッピングを行っています。

<details><summary>テストコード全体</summary><div>

```kotlin
import org.h2.jdbcx.JdbcDataSource
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows
import org.springframework.beans.BeanInstantiationException
import org.springframework.jdbc.core.BeanPropertyRowMapper
import org.springframework.jdbc.core.DataClassRowMapper
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource
import org.springframework.jdbc.core.simple.SimpleJdbcInsert
import javax.sql.DataSource

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@DisplayName("DBを用いてマッピングを行うテスト")
class UseDBMappingTest {
    enum class FooStatus {
        active, archive, deleted
    }

    data class Foo(
        val fooId: Int,
        val fooName: String,
        val fooStatus: FooStatus,
        val description: String?
    )

    data class FooInsert(
        val fooId: Int,
        val fooName: String,
        private val fooStatus: FooStatus,
        val description: String?
    ) {
        fun getFooStatus(): String = fooStatus.name
    }

    lateinit var jdbcTemplate: JdbcTemplate

    @BeforeAll
    fun beforeAll() {
        val dataSource: DataSource = JdbcDataSource().apply {
            setUrl("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS APP\\;SET SCHEMA APP;")
        }

        jdbcTemplate = JdbcTemplate(dataSource)

        jdbcTemplate.execute(
            """
            CREATE TABLE IF NOT EXISTS `foo_table` (
              `foo_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
              `foo_name` VARCHAR(255) NOT NULL,
              `foo_status` ENUM('active', 'archive', 'deleted') NOT NULL,
              `description` VARCHAR(1023) NULL DEFAULT NULL,
              PRIMARY KEY (`foo_id`)
            );
            """.trimIndent()
        )

        val data = FooInsert(10, "Foo", FooStatus.archive, null)

        SimpleJdbcInsert(jdbcTemplate).withTableName("foo_table").execute(BeanPropertySqlParameterSource(data))
    }

    @Test
    fun testBeanPropertyRowMapper() {
        assertThrows<BeanInstantiationException> {
            jdbcTemplate.queryForObject("SELECT * FROM foo_table", BeanPropertyRowMapper(Foo::class.java))
        }
    }

    @Test
    fun testDataClassRowMapper() {
        val result = jdbcTemplate.queryForObject("SELECT * FROM foo_table", DataClassRowMapper(Foo::class.java))

        assertEquals(Foo(10, "Foo", FooStatus.archive, null), result)
    }

    @AfterAll
    fun afterAll() {
        jdbcTemplate.dataSource!!.connection.close()
    }
}
```
</div></details>

重要なのは以下の部分です。

`Foo`はコンストラクタしか定義していないため、`BeanPropertyRowMapper`ではマッピングできず[^reflect_tool]、`BeanInstantiationException`になります。
一方、`DataClassRowMapper`を利用した場合、正常にマッピングを行うことができています。

[^reflect_tool]: [【Kotlin】Javaを前提としたリフレクションによるマッピングライブラリは基本的にKotlinで動かない \- Qiita](https://qiita.com/wrongwrong/items/b21e1d887cc926893352)

```kotlin
@Test
fun testBeanPropertyRowMapper() {
    assertThrows<BeanInstantiationException> {
        jdbcTemplate.queryForObject("SELECT * FROM foo_table", BeanPropertyRowMapper(Foo::class.java))
    }
}

@Test
fun testDataClassRowMapper() {
    val result = jdbcTemplate.queryForObject("SELECT * FROM foo_table", DataClassRowMapper(Foo::class.java))

    assertEquals(Foo(10, "Foo", FooStatus.archive, null), result)
}
```