やりたいこと
- 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-time
のdatetime
と一緒ですね!
さいごに
タイムゾーンの問題はあらゆる日時型においてつきまといます。例えば、システムのタイムゾーンとDBへの接続時のタイムゾーン設定などなど...
こうしたタイムゾーンの問題は、大抵のケースにおいてタイムゾーン情報付きで保管(もしくは読み書き時に必ず決まったタイムゾーンで操作を行うように)できれば解決しますが、上手い具合にExposedはこれをサポートしてないので、独自に実装をすることで解決してみました。