0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SpringBoot3でRESTにLocalDate、LocalDateTimeを使う

Posted at

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のカスタムシリアライザーとデシリアライザーを使用するためには以下の依存関係が必要です。

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のデフォルトのフォーマットを定義します。

application.yml
spring:
  jackson:
    date-format: yyyy-MM-dd'T'HH:mm:ss
    time-zone: Asia/Tokyo

RESTでRequestをLocalDatet、LocalDateTimeで受け取る

Requestを@￰RequestBodyで受け取るdata classの例です

@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,
}

@￰RequestBodyでdata classで受け取る場合、data classの引数はコンストラクタの引数でもあり、フィールでもあります。フィールドに付けるアノテーションは@￰field:で修飾してやる必要があります。

@￰RequestParamでQueryParameterとして受け取る場合はControllerクラスのメソッドの引数になるので、@￰field:で修飾は不要になります。

    @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,

    )
    ・・・
}

ミソは @￰DateTimeFormat で日付型、日時型の文字列書式を指定してやることです。ここはISO8601形式に準じます

  • 日付:"yyyy-MM-dd"
  • 日時:"yyyy-MM-dd'T'HH: mm:ss"

OpenAPIの@￰Schemaアノテーションも書式を指定します。

  • 日付:format = "date"
  • 日時:format = "date-time"

これで、RESTのリクエストの日付、日時をLocalDate、LocalDateTimeで受け取れるようになります

RESTでResponseにLocalDatet、LocalDateTimeで返す

ResponseBodyにdata classでjsonを返す場合はControllerのメソッドの戻り値の型にそのdata classを指定する必要があります。SpringBootが自動的にdata classをjsonにシリアライズしてくれます。

@RestController
class MyController(

    @GetMapping("myapi/search")
    fun search( ・・・ ): ResponseEntity<MyResponse> {
     ・・・
    }
    ・・・
}

data classはこのようになります

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,
    ・・・
}

@￰DateTimeFormat、@￰Schemaの書式指定はReqestBody、RequestParamと同じです。

exposed側の設定

exposedにおいては、

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では困難です。

    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が必要になるケースがあります。

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のテストです

@ExtendWith(SpringExtension::class)
@AutoConfigureMockMvc
@SpringBootTest
class MyControllerTest @Autowired constructor(val mockMvc: MockMvc) {

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

    @Test
    @DisplayName("倉庫棚卸チェック、正常、200")
    fun inventoryChkTest01() {
        // 条件
        val request = MyRequest(
            myDate = LocalDate.of(2024, 1, 1),
            myDateTime = LocalDateTime.of(2024, 1, 1, 0, 0, 0)
        )
        // 実行
        val rslt = mockMvc.perform(
            MockMvcRequestBuilders
                .put(INVENTORY_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をインジェクションしてそれを使う必要があります。

@ExtendWith(SpringExtension::class)
@AutoConfigureMockMvc
@SpringBootTest
class MyControllerTest @Autowired constructor(
    val mockMvc: MockMvc,
    val objectMapper: ObjectMapper) { // LocalDate、LocalDateTimeを使う場合はこれ必要

    @Test
    @DisplayName("倉庫棚卸チェック、正常、200")
    fun inventoryChkTest01() {
        // 条件
        val request = MyRequest(
            myDate = LocalDate.of(2024, 1, 1),
            myDateTime = LocalDateTime.of(2024, 1, 1, 0, 0, 0)
        )
        // 実行
        val rslt = mockMvc.perform(
            MockMvcRequestBuilders
                .put(INVENTORY_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を一切使わずに実現できます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?