はじめに
システムを開発するうえで時間との関係は切っても切れないモノでありながら、その扱い方は難しいものとなっています。時間の取り扱いを間違えると重大なバグに繋がる可能性がありますが、エンジニア初心者で知らない方がいらっしゃるかもしれません。トラブルが発生することを未然に防ぐためにも、システム開発における時刻について整理しようと思います。
まず最初に時間の基礎知識についてご紹介したのち、開発での時間の扱い方をWebアプリケーションの構成の流れに沿って説明していきます。
時間の知識
最初に、開発するうえでこれは知っておくべき時間の知識をまとめました。
標準時
標準時とは、特定の子午線に基づき、国や地域ごとに使用する共通の時刻のことです。
GMT
GMT (Greenwich Mean Time) は、イギリスにあるグリニッジ天文台を基準とした平均太陽時を指します。日本では「グリニッジ標準時」とも呼ばれます。
太陽時とは...
太陽が南中してから次に南中するまでの時間の事ですが、地球は地軸が傾いている影響で太陽の南中時刻は一定ではありません。そのため、地球の自転をもとに1日の平均の長さを定義したものが平均太陽時となります。
以前、世界の標準時として使われていた時刻の基準です。
GMTは地球の自転にもとづく時刻ですが、地球の自転は常に一定ではなくわずかな揺れがあるために厳密な「24時間」ではないことが分かりました。そこで、より正確に一秒を刻める原子時計を世界の標準時として定めることとなりました (→UTC) 。
UTC
UTC (Coordinated Universal Time) とは、現在の世界の標準時間の基準として使用されている時刻系です。セシウム原子の振動数を元に定められた原子時計からうるう秒を補正した時刻であり、協定世界時とも呼ばれます。
UTCは経度0 (イギリスのグリニッジ) の時刻であり、経度15度ずれるごとに1時間の時差が生じ、その時差とUTCを足した時刻がその国や地域の標準時となります。
JST
JST (Japanese Standard Time) は、日本の標準時を指します。 日本は、UTCに9時間 (東経135度分の時差に相当) を足した時刻が日本の標準時となります。
タイムゾーン
タイムゾーン (Time Zone) は、同じ標準時を使用する地域全体を指します。 日本は標準時が1つ設定されており、北海道に居ても沖縄に居ても日本の1つのタイムゾーンを共有します。複数の標準時を持つ国や地域もあります。 日本の標準時はUTCより9時間進んでいるため、日本のタイムゾーンは「UTC+9」「UTC+09:00」「+0900」と表記されます。
サマータイム
サマータイムは、日照時間が長い時期に標準時より1時間早める制度を指します。夏時間とも呼ばれます。
1時間前倒しにすることで、前倒しする前よりも1日の活動時間を日照時間と合わせることができます。外が明るい時間が増えることで、外出時間が増える/電気の使用開始が遅れる事が見込まれ、経済的効果及びエネルギー需要の削減を目的としています。 主に中高緯度に位置する欧米を中心とした国や地域で導入されています。
サマータイムの開始の日は1時間早まります。例として、アメリカではサマータイム開始日は3月第2日曜となっており、01:59:59の次が03:00:00となります。つまりその日の1日の長さは1時間短い23時間となります。反対にサマータイム終了日の11月第1日曜は01:59:59の次が01:00:00となり、1時間長くなります。
そのため、サマータイム導入中は時差が変わります。日本から見たとき、イギリスは9時間の時差が8時間に縮まり、オーストラリアのシドニーは1時間の時差が2時間に広がります。
サマータイム以外の期間はウィンタータイム (冬時間) とも呼びます。
うるう秒
地球の自転は一定ではなく、加速や遅延をしています。 ですが、UTCで使用される原子時計は一定に時を刻みますので、原子時 (原子時計の時間) と世界時 (太陽が南中してから次に南中するまでの時間) にずれが生じてしまいます。 そこで、1秒の長さは原子時を採用しつつ、世界時と原子時のずれが1秒を超えそうになった際に時間を1秒調整するという「うるう秒」が作られました。
うるう秒の調整は挿入と削除の2パターンあり、挿入は「59分60秒」が追加され、削除は「59分59秒」が無くなります。
うるう秒は自転速度により決定されますが、自転速度の予測は出来ないため、半年毎にうるう秒を調整するか判断されています。前回のうるう秒の調整は日本時間2017年1月1日9時に行われました。 しかし、うるう秒は2035年までに廃止が決定されています。
システム開発における日付・時刻の扱い方
現在、システム開発で時刻を扱う際はタイムゾーンを付けることが一般的です。
タイムゾーンを付けないことで起こる問題や、タイムゾーンを付けることのメリットをまとめていきます。
タイムゾーンの必要性
ここで一例をあげてみます。
あなたは日本に在住している前提で、同じ日本在住の友人Aに「明日15時に電話して」と言われたとします。その際、あなたはなにもひねることなく、明日の15時に電話をかければ問題ありません。
ですが、ハワイ在住の友人Bに同じことを言われたらどうでしょうか?世界には時差があり、ハワイと日本では19時間の時刻のずれがあります。「明日15時に電話して」だけでは、日本時間の明日の15時なのかハワイ時間の明日の15時なのか判断が付きません。つまり、世界で見たときに「明日15時」という言葉は明確な時刻をあらわさないということになります。
システム開発でも同じことが言えます。「2021年1月1日 00:00:00」というような時刻を渡されても、システム側は何の時刻なのか判断が出来ません。 サーバとクライアントのタイムゾーンが同じであれば、時差がないことは暗黙の了解でタイムゾーンの有無を気にしなくても構いません。 ですが、タイムゾーンが異なる場合は時差が生じます。 タイムゾーンを付けた時刻を使わないということは時差は考慮されないわけですから、想定の動きをしないことになりバグを引き起こします。
それに加え、今日は世界へのサービス展開を行うのが一般的になりつつあり、国内のみから海外へも展開させようとなる可能性や、国内で運用するシステムでもクラウド経由で海外のサーバを使用する事もあります。 特別な理由がない限りはタイムゾーンを付けた方が安全です。
タイムゾーンを付けることのデメリット
反対にタイムゾーンを付けることのデメリットとしては、
- タイムゾーン分のデータサイズが膨らむ
- 夏時間を考慮する必要がある
の二点ほどが挙げられます。
タイムゾーン分のデータサイズが膨らむ
タイムゾーンを付けるとその分のサイズが膨らんではしまいますが、「+0900
」が付くか付かないかの微々たるものです。
今は昔ほどストレージやメモリが高価な時代ではないので、そこまで憂慮すべき点ではないとは思います。
サマータイムを考慮する必要がある
「サマータイム」で説明した通り、存在しない時刻/重複する時刻や時差の変更が発生します。タイムゾーン付きで保存された時刻を使用する場合、使用期限切れまでの期間や指定時刻の自動実行等が意図した時刻とずれる可能性があります。また、OSやDBではタイムゾーンの解釈が一致していない場合もあります。開発するシステムにサマータイムがどう影響を与えるか考慮しないといけません。
日付・時刻のオブジェクトの使い方やタイムゾーンの設定
ここからは、開発での日付・時刻のオブジェクトの使い方や変換等をご紹介します。
Spring Bootを用いたWebアプリケーションを想定し、「REST API」 -> 「 (REST APIで送られてきた文字列化された日時情報を、サーバ側でJavaのオブジェクトとして保持できるように) 変換」 -> 「 (Java等の) オブジェクト」 -> 「O/R Mapper」 -> 「リレーショナルデータベース」の流れで説明していきます。 (大まかな流れですので、省いている部分もありますがご了承ください。フレームワークや言語が変わっても流れや考え方はほぼ同様です。)
また、日付・時刻を受け取り、それを判断/加工してリレーショナルデータベースに格納させるのはアプリケーションサーバになります。ここでは、アプリケーションサーバでよく使われる言語の1つ、Javaについて重点的にお話ししていきます。
(システム開発でJava言語を使用するにあたり、Spring Frameworkというフレームワークを使用しました。Spring Frameworkはシステム開発に必要な機能の豊富さ、拡張性/セキュリティの高さがあり、使用するのに大変お勧めです。Spring Frameworkは複数の機能を有するフレームワークですが、その複数の機能を使いやすくするためのフレームワークがSpring Bootです。Spring Bootは、アノテーションを使用することでコードの短縮化を図ることができる、Bean定義やXML設定を自動設定する事ができる等、便利な機能があります。)
REST APIでの時刻について
「タイムゾーンの必要性」でも説きましたように、サーバーが受け取るREST APIに格納されている時刻にはタイムゾーンを付けましょう。
日付・時刻のフォーマットはいくつか存在しますが、REST APIにおいてよく使われるフォーマットはISO 8601
です。ISO (国際標準化機構) が定めた、日付・時刻のフォーマット形式の国際基準です。
ISO 8601
は下記の決まりがあります。
- 年月日の区切りの記号は「- (ハイフン)」のみが使用できる (区切り記号無でも可)
- 日付と時刻は「T」で区切る
- 時刻 (秒) の後にタイムゾーンを表記する (UTCの場合は「Z」、UTC以外は「+/-hh:mm」)
(参考サイト : https://zenn.dev/khasunuma/articles/introduction-to-iso8601 ⧉ )
しかし、ISO 8601
は表現の幅が広く、RFC 3339
を掛け合わせた表記が良いと言えるでしょう。
RFC 3339
は、RFC (インターネット技術の標準化を進める団体)が定めたインターネット上の時刻の基準であり、ISO 8601
が使用されています。
RFC 3339
は下記の決まりがあります。
- 年月日の区切りの記号は「- (ハイフン)」を使用する
- 時分秒の区切りの記号は「: (コロン)」を使用する
- 日付と時刻は「T」で区切る
- 必ず、年は4桁、月・日付・時間・分・秒は2桁
- 時刻 (秒) の後にタイムゾーンを付ける (UTCの場合は「Z」、UTC以外は「+/-hh:mm」)
(参考サイト : https://datatracker.ietf.org/doc/html/rfc3339#section-5.6 ⧉ )
表記例
フォーマット | タイムゾーン | 使用例 |
---|---|---|
yyyy-MM-ddThh:mm:ss.SSSZ | 世界協定時 (UTC) | 2023-11-13T03:00:00.000Z |
yyyy-MM-ddThh:mm:ss.SSS±hh:mm | 日本標準時 (JST) | 2023-11-13T12:00:00.000+09:00 |
Javaでの日付・時刻の扱い方
Json文字列の時刻とオブジェクトの変換
Jsonで時刻を受け取ることは多々あるため、JsonとJavaオブジェクトの変換を行えるJacksonについても紹介します。
JsonとJavaオブジェクトの変換では、JacksonのObjectMapperを使用します。以下、サンプルコードを示します。
Json -> オブジェクトのサンプルコード
// 変換するJSON
String json = """
{
"sqlTimestamp" : "2023-11-13 12:00:00.789",
"sqlTimestampWithTimezone" : "2023-11-13 12:00:00.789+09:00",
"localDateTime" : "2023-11-13T12:00:00.123456",
"offsetDateTime" : "2023-11-13T12:00:00.123456+09:00"
}""";
// Json -> オブジェクト ~1回目~
ObjectMapper mapper = new ObjectMapper();
Sample sample = mapper
.registerModule(new JavaTimeModule()).readValue(json, Sample.class);
System.out.println(sample.sqlTimestamp); // 2023-11-13 12:00:00.789
System.out.println(sample.sqlTimestampWithTimezone); // 2023-11-13 12:00:00.789
System.out.println(sample.localDateTime); // 2023-11-13T12:00:00.123456
System.out.println(sample.offsetDateTime); // 2023-11-13T03:00:00.123456Z
// Json -> オブジェクト ~2回目~
ObjectMapper mapperWithTimezone = new ObjectMapper().setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
Sample sample2 = mapperWithTimezone
.registerModule(new JavaTimeModule()).readValue(json, Sample.class);
System.out.println(sample2.sqlTimestamp); // 2023-11-13 12:00:00.789
System.out.println(sample2.sqlTimestampWithTimezone); // 2023-11-13 12:00:00.789
System.out.println(sample2.localDateTime); // 2023-11-13T12:00:00.123456
System.out.println(sample2.offsetDateTime); // 2023-11-13T12:00:00.123456+09:00
Sampleクラス
JSONから変換するオブジェクトクラス。
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
public class Sample {
@JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss.SSS", timezone = "Asia/Tokyo")
public java.sql.Timestamp sqlTimestamp;
@JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss.SSSXXX", timezone = "Asia/Tokyo")
public java.sql.Timestamp sqlTimestampWithTimezone;
@JsonFormat(pattern = "yyyy-MM-dd'T'hh:mm:ss.SSSSSS", timezone = "Asia/Tokyo")
public LocalDateTime localDateTime;
@JsonFormat(pattern = "yyyy-MM-dd'T'hh:mm:ss.SSSSSSxxx", timezone = "Asia/Tokyo")
public OffsetDateTime offsetDateTime;
}
注意すべきこと
// Json -> オブジェクト ~1回目~
ObjectMapper mapper = new ObjectMapper();
(略)
System.out.println(sample.offsetDateTime); // 2023-11-13T03:00:00.123456Z
// Json -> オブジェクト ~2回目~
ObjectMapper mapperWithTimezone = new ObjectMapper().setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
(略)
System.out.println(sample2.offsetDateTime); // 2023-11-13T12:00:00.123456+09:00
(サンプルコードから一部抜粋)
サンプルコードの「// Json -> オブジェクト ~1回目~」では、 ObjectMapper を何も指定せずインスタンス生成していますが、そのインスタンスを使用して変換した変数「offsetDateTime」は9時間巻き戻った値が格納されています。 一方、「// Json -> オブジェクト ~2回目~」では、タイムゾーンを指定して ObjectMapper をインスタンス生成しており、そちらのインスタンスを使用して変換した変数「offsetDateTime」は JSON に格納された時刻のまま格納されています。
Jackson は TimeZone をデフォルトのUTCで扱うようになっており、 UTC でないタイムゾーンを使用したい場合は ObjectMapper の生成時に指定してあげないといけないので注意が必要です。
日付時刻クラス
Javaには日付・時刻を取り扱うクラスがいくつか存在します。 初期から日付・時刻に関するクラスは存在していますが、JDK8以降に一新されました。JDK7以前のクラスは使いにくいものや非推奨となったものもあるため、JDK8で追加された Date and Time API のなかから説明していきます。
Date and Time API の中で、日付と時刻 (年, 月, 日, 時, 分, 秒, ナノ秒) を表すクラスは、「java.time.LocalDateTime」,「java.time.OffsetDateTime」,「java.time.ZonedDateTime」の3つとなります。その3つのクラスで使用されるタイムゾーン情報を表すクラスは、「java.time.ZoneId」,「java.time.ZoneOffset」の2つです。
【日付時刻クラス】
- java.time.LocalDateTime
- タイムゾーンのない日時
- 例:2024-01-01T00:00:00.0
- java.time.OffsetDateTime
- オフセット付きの日時
- 例:2024-01-01T00:00:00.0+09:00
- java.time.ZonedDateTime
- タイムゾーン付きの日時
- 例:2024-01-01T00:00:00.0+09:00[Asia/Tokyo]
【日付時刻クラスで使用するクラス】
- java.time.ZoneId
- 地域と時差のルールを定義している
- 夏時間・冬時間の考慮あり
- 例: "Asia/Tokyo"
- java.time.ZoneOffset
- UTCからの時差
- 夏時間・冬時間の考慮なし
- 例: +09:00
LocalDateTime
LocalDateTime は、タイムゾーン情報を持たない日付・時刻を扱うクラスです。タイムゾーン情報を持たないので、世界共通時間軸上におけるどこの国・地域の時刻を指しているかは特定することができません。
ユーザー等の外部入力されたデータにはタイムゾーンがない場合は多々あり、そこで LocalDateTime として受け取る/渡すことがあります。ただ、「タイムゾーンの必要性」でも述べた通り、タイムゾーンの情報のない時刻のまま運用するとバグを引き起こす可能性があります。 LocalDateTime として受け取った場合、早い段階で LocalDateTime タイムゾーンを特定してその時刻にタイムゾーン情報を付与しましょう。
OffsetDateTime
OffsetDateTime は、オフセット ( ZoneOffset ) と呼ばれるUTCからの時差のみをタイムゾーン情報として持つ日付・時刻を扱うクラスです。
OffsetDateTime は ZonedDateTime とは違い、サマータイム (ウィンタータイム) を考慮しません。存在しない時刻や重複時刻を気にする必要がなく、UNIX時間に変換することが可能となります。
UNIX時間とは...
コンピューターにおける時刻表現方法の1つです。エポックタイムとも呼ばれます。 UTCで「1970年1月1日午前0時0分0秒」を基準にして、そこからの経過秒数で日時を表現します。 1970年1月1日午前0時0分1秒の場合、UNIX時間は「1」となります。 UNIX時間はうるう秒を考慮しません。
サマータイム (ウィンタータイム) を考慮しなくてよいという理由から、DB等への時刻の保存は OffsetDateTime の使用が推奨されます。
サマータイムの開始前と開始後でタイムゾーンがずれてないことが分かります。
// ※2023/11/13に実施しました
// サマータイム開始前
OffsetDateTime newYork = OffsetDateTime.now(ZoneId.of("America/New_York"));
// サマータイムが開始済みの4月まで時間を足す
OffsetDateTime newYorkPlus5M = newYork.plusMonths(5);
System.out.println(newYork); // 2023-11-13T21:54:38.209701-05:00
System.out.println(newYorkPlus5M); // 2024-04-13T21:54:38.209701-05:00
ZonedDateTime
ZonedDateTime は、地域と時差のルール定義 ( ZoneId ) をタイムゾーン情報として持つ日付・時刻を扱うクラスです。
ZonedDateTime はサマータイム (ウィンタータイム) を考慮して計算を行います。
サマータイムの開始前と開始後でタイムゾーンが1時間ずれている (「-05:00」が「-04:00」に変化) ことが分かります。
// ※2023/11/13に実施しました
// サマータイム開始前
ZonedDateTime newYork = ZonedDateTime.now(ZoneId.of("America/New_York"));
// サマータイムが開始済みの4月まで時間を足す
ZonedDateTime newYorkPlus5M = newYork.plusMonths(5);
System.out.println(newYork); // 2023-11-13T21:54:38.209701-05:00[America/New_York]
System.out.println(newYorkPlus5M); // 2024-04-13T21:54:38.209701-04:00[America/New_York]
ZonedDateTime の注意
ZonedDateTime はサマータイム (ウィンタータイム) を考慮されますので、サマータイム開始前の ZonedDateTime インスタンスにサマータイム開始後の状態へ時間を経過させると UNIX時間にずれが生じます。それらを使用する際はご注意ください。
実際に試したのが下記です。 3月1日の OffsetDateTime インスタンスに1か月足すと、UNIX時間では2678400秒 (31(日)×24(時間)×60(分)×60(秒)) が足された事になります。ですが、3月1日の ZonedDateTime インスタンスに1か月足すと、UNIX時間では2674800秒 (31(日)×24(時間)×60(分)×60(秒) - 1(時間)×60(分)×60(秒)) となり、その差は3600秒です。 OffsetDateTime と比べて1時間短い値となってしまいます。
// OffsetDateTime
// サマータイム開始前の3月1日の UNIX時間(秒)を出す(ニューヨークとUTCの時差は-5時間)
OffsetDateTime odt = OffsetDateTime.of(2023,3,1,0,0,0,0,ZoneOffset.ofHours(-5));
long odtEpoch = odt.toEpochSecond();
// 1か月足してサマータイム開始済みにし、UNIX時間(秒)を出す
OffsetDateTime odtPlus = odt.plusMonths(1);
long odtPlusEpoch = odtPlus.toEpochSecond();
// UNIX時間(秒)を比較する
System.out.println("OffsetDateTime:" + (odtPlusEpoch - odtEpoch)); // 2678400
// ZonedDateTime
// サマータイム開始前の3月1日の UNIX時間(秒)を出す
ZonedDateTime zdt = ZonedDateTime.of(2023,3,1,0,0,0,0,ZoneId.of("America/New_York"));
long zdtEpoch = zdt.toEpochSecond();
// 1か月足してサマータイム開始済みにし、UNIX時間(秒)を出す
ZonedDateTime zdtPlus = zdt.plusMonths(1);
long zdtPlusEpoch = zdtPlus.toEpochSecond();
// UNIX時間(秒)を比較する
System.out.println("ZonedDateTime:" + (zdtPlusEpoch - zdtEpoch)); // 2674800
日付・時刻クラス(JDK8以降)で有用なメソッド
・ 現在時刻の取得 (nowメソッド)
日付時刻 (LocalDateTime, OffsetDateTime, ZonedDateTime) 共通のメソッド (代表でLocalDateTimeを記す)
// デフォルトのタイムゾーンを使用して現在時刻を返却する。
LocalDateTime now = LocalDateTime.now();
// 引数のタイムゾーンを使用して現在時刻を返却する。
LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Tokyo"));
・ インスタンス生成 (ofメソッド)
// LocalDateTime (年月日時分秒を指定)
LocalDateTime now = LocalDateTime.of(2023, 11, 13, 0, 0, 0);
// OffsetDateTime (年月日時分秒ナノ秒、オフセットを指定)
OffsetDateTime now = OffsetDateTime.of(2023, 11, 13, 0, 0, 0, 0, ZoneOffset.ofHours(9));
// ZonedDateTime (年月日時分秒ナノ秒、タイムゾーンを指定)
ZonedDateTime now = ZonedDateTime.of(2023, 11, 13, 0, 0, 0, 0, ZoneId.of("Asia/Tokyo"));
・ LocalDateTime型から、ZonedDateTime型 や OffsetDateTime型 に変換する
LocalDateTime ldt = LocalDateTime.now();
//LocalDateTime -> ZoneDateTime
ZonedDateTime zdt = ldt.atZone(ZoneId.systemDefault());
//LocalDateTime -> OffsetDateTime
OffsetDateTime odt = ldt.atOffset(ZoneOffset.ofHours(9));
うるう秒について
現在のJavaの標準APIではうるう秒を扱うことができず、ZonedDateTime 型や LocalDateTime 型で60秒を指定できません。 よって、クライアントやフロントエンドから60秒が送られてきた場合、Javaの標準APIではエラーとなってしまいます。 ですが、2012年にうるう秒挿入によるシステム障害が起きてから多くのOSやタイムサーバーではうるう秒の対策がなされており、現在60秒を挿入しない場合が多いようです。
// 60秒を指定する
LocalDateTime ldt = LocalDateTime.of(2023,11,13,23,59,60);
// 下記のエラーが出る
Exception in thread "main" java.time.DateTimeException: Invalid value for SecondOfMinute (valid values 0 - 59): 60
(略)
外部ライブラリ ( ThreeTen-Extra ) を使用すればうるう秒を扱うことが可能です。
Javaで日付・時刻を扱う時に注意すべき点のまとめ
ユーザーの入出力の時刻 ( LocalDateTime ) を受け取った際は、タイムゾーン付き時刻 ( ZonedDateTime ) へ変換すること
ある1つのタイムゾーンのなかで生活する方であればタイムゾーンや時差を意識することはないため、Webサービスでユーザーに時刻を入力/表示させるときには、タイムゾーンや時差も入力/表示させることは基本ありません。
故に、ユーザーからはタイムゾーンや時差の情報がない日付時刻が送られてくることがあります。それらを受け取った際に、こちらはユーザーのタイムゾーンの時刻として解釈しなければなりません。ユーザーのタイムゾーンを特定したら、すぐタイムゾーン情報を格納して処理するようにしましょう。
時刻の保存やシステム間のやり取りは、OffsetDateTime の型で行うこと
ZonedDateTime はサマータイム (ウィンタータイム) が考慮されて時間計算が行われており、サマータイム (ウィンタータイム) の開始前後と終了前後では時間のずれが生じます。その結果、トラブルが発生してしまうことがあります。
例えば、あるシステムにおいて3月頭から1か月のライセンス(3月1日0時0分0秒から3月31日23時59分59秒まで)を付与したとします。日本は2023年現在サマータイムが導入されていないので、使用可能時間は2678399秒 (31(日)×24(時間)×60(分)×60(秒)-1(秒))となりますが、ニューヨークはサマータイムが3月第2日曜日に導入されて1時間早まっているため、2674799秒 (31(日)×24(時間)×60(分)×60(秒) - 1(時間)×60(分)×60(秒)-1(秒))となり、単純なライセンス使用可能時間が短くなってしまいます。
このように意図しない挙動やトラブルの回避のため、時刻の保存は何か特別な理由がない限り UTC からのオフセット情報が付いた日付・時刻で扱う方が良いでしょう。
O/Rマッパーでの日付・時刻の扱い方
オブジェクトとデータベース間のデータ形式を変換するために使われるのが、O/Rマッパーです。 O/Rマッパーにもタイムゾーンの概念はあるので、タイムゾーンを意識しなければなりません。
ここでは、HibernateとMyBatisを紹介します。
Hibernate
Hibernateは、Javaでデータベースを扱うためのO/Rマッパーです。
yamlで下記のように書くことでタイムゾーンを指定することができます。
spring:
jpa:
properties:
hibernate:
jdbc:
time_zone: UTC
(引用サイト : https://www.baeldung.com/mysql-jdbc-timezone-spring-boot ⧉ )
MyBatis
MyBatisは、Javaでデータベースを扱うためのO/Rマッパーで、SQL文とオブジェクトをマッピングします。 MyBatis自体にタイムゾーンの設定があるわけではなく、SQL文とオブジェクト間で変換される時に必ず通る Handler 部で時刻のタイムゾーンの設定を変えて値をセットするのが良いかと思われます。
Javaのオブジェクトへ変換する際に、Date and Time API で提供されている型を使用したい場合も、Handler で実現できます。Type Handler
というモジュールを使用します。
例えば、JDBCの TIMESTAMP 型を Java の java.time.OffsetDateTime 型に変換したいとき、OffsetDateTimeTypeHandler
を使用するとマッピングしてくれます。(※ Ver3.4.5以前は、別ライブラリ( mybatis-typehandlers-jsr310
)が必要です)
リレーショナルデータベースでの日付・時刻の扱い方
次にリレーショナルデータベース (以下「データベース」とする) の日付・時刻のデータ型やタイムゾーンの変更の仕方です。 時刻の取り扱いはデータベース毎に異なりますので、解釈を間違わないように使用する前に公式HP等を確認しましょう。
また、Javaの章で「時刻の保存は、OffsetDateTime の型で行うこと」と述べたように、オフセットが付いた日付・時刻でデータベースに保存しましょう。オフセットをつけるためには、データ型はタイムゾーンと一緒に保存できるものでなければいけません。使用するデータ型がタイムゾーンをセットできるか確認してから使いましょう。
そして、データベースは独自でデフォルトのタイムゾーンを持っていることがほとんどです。初期設定がどこのタイムゾーンなのか確認し、必要があれば設定を変えましょう。
こちらでは、PostgreSQL, MySQLをご紹介します。
PostgreSQL
PostgreSQLで日付・時刻を扱えるデータ型は下記です。
型名 | 格納サイズ | 説明 | 最遠の過去 | 最遠の未来 | 精度 |
---|---|---|---|---|---|
timestamp [ (p) ] [ without time zone ] | 8 バイト | 日付と時刻両方(時間帯なし) | 4713 BC | 294276 AD | 1マイクロ秒 |
timestamp [ (p) ] with time zone | 8バイト | 日付と時刻両方、時間帯付き | 4713 BC | 294276 AD | 1マイクロ秒 |
(引用サイト:PostgreSQL 15.4文書 ⧉)
タイムゾーンの変更は、postgresql.conf
から行うことができます。 (デフォルト設定は GMT ですが、postgresql.conf
で上書きされます。変更後は、PostgreSQL の再起動が必要です。)
timezone = 'UTC'
# logのタイムゾーン
log_timezone = 'UTC'
データベース毎にタイムゾーンを変更する場合は、クエリ実行を行います。
ALTER DATABASE <DB名> SET timezone TO 'Asia/Tokyo';
MySQL
MySQLで日付・時刻を扱えるデータ型は下記です。MySQL 8.0.19 から下記の値をテーブルに挿入時にタイムゾーン・オフセットを指定できるようになりました。
型名 | 説明 | 最遠の過去 | 最遠の未来 | 精度 |
---|---|---|---|---|
DATETIME | 日付と時刻 | 1000-01-01 00:00:00.000000 | 9999-12-31 23:59:59.999999 | マイクロ秒(6桁) |
TIMESTAMP | 日付と時刻 | 1970-01-01 00:00:01.000000 | 2038-01-19 03:14:07.999999 | マイクロ秒(6桁) |
(参考サイト : MySQL documentation ⧉)
タイムゾーンの変更は、my.cnf
から行うことができます。 (デフォルト設定はホストマシンのタイムゾーンです。)
default-time-zone = 'UTC'
まとめ
ここまで、それぞれタイムゾーンの設定の仕方や時刻の取り扱い方について触れてきました。 以下でまとめます。
- REST APIに格納する時刻にはタイムゾーン情報を付与すること
- フォーマットは、
ISO 8601
及びRFC 3339
の基準を使用し、サーバ側と表記の解釈を合わせること
- フォーマットは、
- サーバ側で時刻を扱う際はタイムゾーン情報の付いたデータ型を利用すること
Webアプリケーションの時刻の扱いにおいて重要なことは、一連の流れで見たときにタイムゾーンの設定や形式が一貫していることです。もし、タイムゾーンが一貫しておらず、JavaではJSTで扱っているがDBではUTCで格納しているとなると意図しない値が保存され、その値を取り出したときに正常な値として使えなくなり、のちに大きなトラブルに繋がります。タイムゾーンの設定が適したものになっているか、変換の際にタイムゾーン・オフセット情報が抜け落ちていないか確認しましょう。
終わりに
この記事では、システム開発での日付・時刻の扱い方について紹介しました。
これから開発で時刻を取り扱うという方の一助になりましたら幸いです。
参考サイト
- 国立研究開発法人情報通信研究機構「NICT NEWS No.451」 ⧉
- 暦Wiki ⧉
- Java公式HP (LocalDateTime ⧉, OffsetDateTime ⧉, ZonedDateTime ⧉)
- PostgreSQL公式HP (8.5. 日付/時刻データ型 ⧉)
- MySQL公式HP (11.2.2 DATE、DATETIME、および TIMESTAMP 型 ⧉, 5.1.15 MySQL Server でのタイムゾーンのサポート ⧉)
- MyBatis公式HP( タイプハンドラーについて ⧉ )