4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Kotlin】Exposedでタイムゾーン付きのdatetimeを表現する方法

Posted at

やりたいこと

  • Exposedの exposed-java-time はタイムゾーン付きのdatetimeをサポートしていない
  • しかし、対応しているDBに対してはタイムゾーンも一緒に保存しておいて欲しい
  • 対応していないDBに対しては共通してINSERT/UPDATE時およびSELECT時にはシステムのタイムゾーン設定によらずに、UTCで読み書きして欲しい

というわけで、元々あるAPIだとこれは実現出来ないので、自分で独自にタイムゾーン対応のdatetimeをExposedのTableのAPIに生やしてあげることで対処します。

というわけで早速実装する

exposed-java-timeも内部的には似たようなことをしていますが、ColumnTypeを継承してIDateColumnTypeを実装することで独自のカラム型を実装することができます。

以下のコードは kotlinx.datetime に依存しているので、入れてない方は予め入れておいてください。

implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.3.3")
import kotlinx.datetime.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.vendors.*
import java.time.format.DateTimeFormatter

class DataTimeWithTimeZoneColumnType : ColumnType(), IDateColumnType {
  override val hasTimePart: Boolean = true

  override fun nonNullValueToString(value: Any): String {
    if (value !is Instant) {
      error("$value is not Instant type")
    }
    return "'${DateTimeFormatter.ISO_INSTANT.format(value.toJavaInstant())}'"
  }

  override fun valueFromDB(value: Any): Instant {
    return when (value) {
      is Instant -> value
      is java.time.Instant -> value.toKotlinInstant()
      // タイムゾーン情報を付けられないMySQLのようなDBの場合には強制的にタイムゾーンをUTCとして扱う
      is LocalDateTime -> value.toInstant(UtcOffset.ZERO)
      is java.time.LocalDateTime -> value.toKotlinLocalDateTime().toInstant(UtcOffset.ZERO)
      else -> error("$value is not Instant or LocalDateTime!")
    }
  }

  override fun notNullValueToDB(value: Any): Any {
    if (value !is Instant) {
      error("$value is not Instant type")
    }
    if (!currentDBSupportsTimeZone()) {
      // タイムゾーン情報を付けられないMySQLのようなDBの場合には強制的にタイムゾーンをUTCとして扱う
      return value.toLocalDateTime(TimeZone.UTC).toJavaLocalDateTime()
    }
    return value.toJavaInstant()
  }

  private fun currentDBSupportsTimeZone(): Boolean {
    return arrayOf(
      PostgreSQLDialect.dialectName, H2Dialect.dialectName, OracleDialect.dialectName, SQLServerDialect.dialectName
    ).contains(
      currentDialect.name
    )
  }

  override fun sqlType(): String {
    return when (currentDialect.name) {
      PostgreSQLDialect.dialectName, H2Dialect.dialectName, OracleDialect.dialectName -> "TIMESTAMP WITH TIME ZONE"
      SQLServerDialect.dialectName -> "DATETIMEOFFSET"
      else -> "DATETIME"
    }
  }
}

fun Table.datetimeWithTZ(name: String): Column<Instant> = registerColumn(name, DataTimeWithTimeZoneColumnType())

流れとしては、タイムゾーン付きの日時型をサポートしているDBの場合はそれにあったカラム型を使うようにして、そうでない場合には読み書きに必ずUTCを使うように実装しています。

使ってみる

object Hoge : IntIdTable("hoges") {
  // 今までこのように書いていたのを....
  val hogehoge = datetime("hogehoge")
  // 下のように変えるだけ!
  val hogehogeWithTZ = datetimeWithTZ("hogehoge_with_tz")
}

使い方はKotlin側で使用する日時型がInstantになる以外は、exposed-java-timedatetimeと一緒ですね!

さいごに

タイムゾーンの問題はあらゆる日時型においてつきまといます。例えば、システムのタイムゾーンとDBへの接続時のタイムゾーン設定などなど...

こうしたタイムゾーンの問題は、大抵のケースにおいてタイムゾーン情報付きで保管(もしくは読み書き時に必ず決まったタイムゾーンで操作を行うように)できれば解決しますが、上手い具合にExposedはこれをサポートしてないので、独自に実装をすることで解決してみました。

4
1
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
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?