SpringでRESTにLocalDate、LocalDateTimeを使う

# Calendar、Dateよりも、LocalDate、LocalDateTimeを使おう
SpringBoot3、kotlin、exposaed、OpenAPIでRESTのAPIサーバを作っています。

従来はjavaで日付、日時型を扱うのは
* java.util.Calendar
* java.util.Date

が一般的でしたが、java8から新しいAPIが使えるようになっています。
* java.time.LocalDatet
* java.time.LocalDateTime

Calendar、Dateクラスは既にレガシーです。
古い方のAPIは何故だか(歴史的な経緯か？)月が0から始まる(1月が0)といういびつな仕様でバグの原因になりやすいです。(よく、+1を忘れる)

しかも、JDBCでSQLを使う場合、日付型と日時型には2種類あり、これまたややこしい。お互いに相互変換が必要になります。
* java.util.Date
* java.sql.Date
* java.sql.Timestamp

# RESETでLocalDatet、LocalDateTime使ってやりとりする
RESTはjsonでやりとりしますが、jsonに日付、日時型はありません。日付、日時型は文字列として扱われます。
SpringBoot3において、RESTでやり取りする場合のRequestBody、RequestParameter、ResponseBodyにおいて、SpringBoot3が自動的にリクエストを
* 文字列(日付) → LocalDate
* 文字列(日時) → LocalDateTime

レスポンスを
* LocalDate → 文字列(日付)
* LocalDateTime → 文字列(日時)

変換してくれたら便利です。
SpringBootでJacksonのカスタムシリアライザーとデシリアライザーを使用するためには以下の依存関係が必要です。
```kotlin:build.gradle.kts
dependencies {
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
}
```
ですが、私の場合は既に依存ライブラリに含まれていました。
**org.springframework.boot:spring-boot-starter-web:3.3.0**
を依存関係のライブラリに追加すると自動的に追加されます。

application.ymlにjacksonのデフォルトのフォーマットを定義します。
```yml:application.yml
spring:
  jackson:
    date-format: yyyy-MM-dd'T'HH:mm:ss
    time-zone: Asia/Tokyo
```

## RESTでRequestをLocalDatet、LocalDateTimeで受け取る
Requestを@&#xFFF0;RequestBodyで受け取るdata classの例です
```kotlin
@Schema(description = "サンプルリクエスト")
data class SampleRequest(

    @field:JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Tokyo")
    @field:JsonProperty("sampleDate", required = true)
    @Schema(description = "サンプル日付型", type = "string", format = "date", example = "2022-03-14", required = true )
    var sampleDate: LocalDate,

    @field:JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Tokyo")
    @field:JsonProperty("sampleDateTime", required = true)
    @Schema(description = "サンプル日時型", type = "string", format = "date-time", example = "2022-03-14", required = true )
    var sampleDateTime: LocalDateTime,
}
```
@&#xFFF0;RequestBodyでdata classで受け取る場合、data classの引数はコンストラクタの引数でもあり、フィールでもあります。フィールドに付けるアノテーションは@&#xFFF0;field:で修飾してやる必要があります。

@&#xFFF0;RequestParamでQueryParameterとして受け取る場合はControllerクラスのメソッドの引数になるので、@&#xFFF0;field:で修飾は不要になります。
```kotlin
    @GetMapping("myapi/search")
    fun search(
        @Valid
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        @Schema(description = "サンプル日付", type = "string", format = "date", example = "2022-03-14", required = true )
        @RequestParam("sampleDate") sampleDate: LocalDate,

        @Valid
        @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
        @Schema(description = "サンプル日時", type = "string", format = "date-time", example = "2022-03-14T08:42:00.000Z", required = true )
        @RequestParam("sampleDateTime") sampleDateTime: LocalDateTime,

    )
    ・・・
}
```
ミソは @&#xFFF0;DateTimeFormat で日付型、日時型の文字列書式を指定してやることです。ここはISO8601形式に準じます
* 日付："yyyy-MM-dd"
* 日時："yyyy-MM-dd'T'HH: mm:ss"

OpenAPIの@&#xFFF0;Schemaアノテーションも書式を指定します。
* 日付：format = "date"
* 日時：format = "date-time"

これで、RESTのリクエストの日付、日時をLocalDate、LocalDateTimeで受け取れるようになります
## RESTでResponseにLocalDatet、LocalDateTimeで返す
ResponseBodyにdata classでjsonを返す場合はControllerのメソッドの戻り値の型にそのdata classを指定する必要があります。SpringBootが自動的にdata classをjsonにシリアライズしてくれます。
```kotlin
@RestController
class MyController(

    @GetMapping("myapi/search")
    fun search( ・・・ ): ResponseEntity<MyResponse> {
     ・・・
    }
    ・・・
}
```
data classはこのようになります
```kotlin
data class MyResponse (
    @field:JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Tokyo")
    @Schema(description = "サンプル日付", type = "string", format = "date", example = "2024-01-01", required = false )
    var sampleDate: LocalDate,

    @field:JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Tokyo")
    @Schema(description = "サンプル日時", type = "string", format = "date-time", example = "2024-01-01T00:00:00", required = true )
    var sampleDateTime: LocalDateTime,
    ・・・
}
```
 @&#xFFF0;DateTimeFormat、@&#xFFF0;Schemaの書式指定はReqestBody、RequestParamと同じです。

# exposed側の設定
exposedにおいては、
```kotlin:build.gradle.kts
dependencies {
    implementation("org.jetbrains.exposed:exposed-java-time:0.50.1")
}
```
exposedのDSL、DAOが扱う日付型、日時型はLocalDate、LocalDateTimeに合わされます。

exposedでSELECTする時、テーブルのjoinが1個か、2個ぐらいなら頑張って、DSL、DAOでも書けますが、何でも頑張ってSQLでやろうとする人、そのDBに依存した関数等を多用していると、DSL、DAOでは困難です。
```kotlin
    TransactionManager.current().exec(
        """
            SELECT
                SAMPLE_DATE, -- 日付型カラム
                SAMPLE_DATE_TIME, -- 日時型カラム
                たくさんのカラム、関数使用している
            FROM
                テーブル、いくつもjoinしている・・・
            WHERE
                ・・・ 
            ORDER BY
                ・・・
        """.trimIndent(), args
    ) { rs ->
        while (rs.next()) {
            ・・・
            val sampleDate = rs.getDate("SAMPLE_DATE").toLocalDate()
            val sampleDateTime = rs.getTimestamp("SAMPLE_DATE_TIME").toLocalDateTime()
        }
    }
```
TransactionManager.current().exec()の最後の引数のラムダの中はjava.sql.ResultSetが渡ってきます。なので普通に生JDBCのAPI(最近ではあまりないか？)を使ってやる必要があります。

日付型のカラムはResultSet#getDateで、一旦java.sql.Date型で取得してそれをLocalDate型に変換してやる必要があります。
日時型のカラムはResultSet#getTimestampで、一旦java.sql.Timestamp型で取得してそれをLocalDateTime型に変換してやる必要があります。

# カスタムのjackson objectMapper
以上で、ほぼほぼ、やりたいことは満たせるのですが、一部、カスタムのjackson objectMapperが必要になるケースがあります。
```kotlin:DateTimeConfig.kt
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.KotlinModule
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class DateTimeConfig {
    @Bean
    fun objectMapper(): ObjectMapper {
        return ObjectMapper()
            .registerModule(KotlinModule.Builder().build())
            .registerModule(JavaTimeModule())
    }
}
```
これをどこで使うのかというとMockMVCでのControllerのテストです
```kotlin
@ExtendWith(SpringExtension::class)
@AutoConfigureMockMvc
@SpringBootTest
class MyControllerTest @Autowired constructor(val mockMvc: MockMvc) {

    /** objectMapper */
    private val objectMapper = ObjectMapper() // ★

    @Test
    @DisplayName("正常、200")
    fun chkTest01() {
        // 条件
        val request = MyRequest(
            myDate = LocalDate.of(2024, 1, 1),
            myDateTime = LocalDateTime.of(2024, 1, 1, 0, 0, 0)
        )
        // 実行
        val rslt = mockMvc.perform(
            MockMvcRequestBuilders
                .put(CHK_PATH)
                .content(objectMapper.writeValueAsString(request))
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .characterEncoding(Charsets.UTF_8)
        ).andReturn().response
    }
}
```
上のテストケースの★印のobjectMapperは、LocalDate、LocalDateTimeをシリアライズできません。
objectMapper.writeValueAsString(request)でエラーになります。

上記、DateTimeConfig.ktで登録したbeanをインジェクションしてそれを使う必要があります。
```kotlin
@ExtendWith(SpringExtension::class)
@AutoConfigureMockMvc
@SpringBootTest
class MyControllerTest @Autowired constructor(
    val mockMvc: MockMvc,
    val objectMapper: ObjectMapper) { // LocalDate、LocalDateTimeを使う場合はこれ必要

    @Test
    @DisplayName("正常、200")
    fun chkTest01() {
        // 条件
        val request = MyRequest(
            myDate = LocalDate.of(2024, 1, 1),
            myDateTime = LocalDateTime.of(2024, 1, 1, 0, 0, 0)
        )
        // 実行
        val rslt = mockMvc.perform(
            MockMvcRequestBuilders
                .put(CHK_PATH)
                .content(objectMapper.writeValueAsString(request))
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .characterEncoding(Charsets.UTF_8)
        ).andReturn().response
    }
}
```

他、内部で独自にjson ←→ LocalDate、LocalDateTimeの変換の処理が必要であれば、カスタムのjackson objectMapperが必要となるケースがあるかもしれません。


# 最後に
以上で、SpringBoot3、kotlin、exposaed、OpenAPIでRESTのAPIサーバでレガシーなjava.util.Date、Calenarを一切使わずに実現できます。