Apexの日付/時間の扱い

SalesforceのApexで日時を扱う時、見た目の表記と実際のデータ(メモリやデータベースに格納される日時)のTimezoneを常に意識する必要がある。

String txt='2018-12-01 01:23:45';
Datetime dt=Datetime.valueOf(txt);

System.debug(dt);
System.debug('dt.format() = ' + dt.format());
System.debug('dt.format(\'yyyy/MM/dd HH:mm:ss\', \'Asia/Tokyo\') = ' + 
    dt.format('yyyy/MM/dd HH:mm:ss', 'Asia/Tokyo'));
System.debug('dt.formatGmt(\'yyyy/MM/dd HH:mm:ss\') = ' + 
    dt.formatGmt('yyyy/MM/dd HH:mm:ss'));

とやると、ログには以下のように出力される(TimezoneがJSTのユーザの場合)。

2018-11-30 16:23:45
dt.format() = 2018/12/01 1:23
dt.format('yyyy/MM/dd HH:mm:ss', 'Asia/Tokyo') = 2018/12/01 01:23:45
dt.formatGmt('yyyy/MM/dd HH:mm:ss') = 2018/11/30 16:23:45

これはバグでもなんでもなくて、txtに格納された日時にTimezoneが指定されていなかったのでシステムがGMTとして扱い、GMTでの2018-12-01 01:23:45としてメモリに格納、それをログに取り出す際、シリアル値を日付表記に変換する過程でローカルタイム(JP=GMT+0900 )にするために9時間がマイナスされてしまっている。 (2021/01/05訂正)

これはバグでもなんでもなくて、valueOf する段階で突っ込んだ String がローカル扱いで解釈されて、GMTの 2018-11-30 01:23:45 としてDatetime型変数 dt に格納され、それが System.debug で出力されている。
System.debug(dt) で出力される日時はローカルタイム変換されていないデータ実体そのまま(GMT)なので、単純にこれだけを見るとセットした日時文字列と異なる日時データが保存されたように錯覚しがち。
システムのデバッグログを見ると、ちゃんとそのような動作になっていることが分かる。

10:57:16.0 (3077741)|VARIABLE_SCOPE_BEGIN|[4]|txt|String|false|false
10:57:16.0 (3130984)|VARIABLE_ASSIGNMENT|[4]|txt|"2018-12-01 01:23:45"
10:57:16.0 (3137938)|STATEMENT_EXECUTE|[5] 10:57:16.0 (3234397)|HEAP_ALLOCATE|[5]|Bytes:56
10:57:16.0 (3257496)|VARIABLE_SCOPE_BEGIN|[5]|dt|Datetime|false|false
10:57:16.0 (3303902)|VARIABLE_ASSIGNMENT|[5]|dt|"2018-11-30T16:23:45.000Z"
10:57:16.0 (3310258)|STATEMENT_EXECUTE|[6]
10:57:16.0 (3349951)|USER_DEBUG|[6]|DEBUG|2018-11-30 16:23:45

よく同様の問題を扱うブログ等の記事を見ると、9時間を加算したりローカル時刻を valueOfgmt で処理したりして辻褄を合わせようとするパターンを見かけるが、それを行うと最初の表記上の時刻(時間軸上の絶対的な位置)と実際にメモリやDBに格納される時刻が一致しないことになる。

なので、次のようなシーンでは日付がどの Timezone で扱われるかを常に考慮しておく必要がある。

  • String型とDateTime型を変換するとき
  • Date型とDateTime型を変換するとき
  • 年月日時分秒を別々な項目で入出力し、内部や集計ではDateTime型で扱うとき
  • データローダで日付を扱うとき
  • ゲストユーザ(コミュニティやSiteなど)を使うとき
  • グローバル企業などで異なるタイムゾーンのユーザが混在するとき

これらはグローバルなクラウドサービスで日付を扱う時の基本とも言える。
特に金額の集計などで月を跨いでしまうような事態になるとクリティカルなバグになる。

ちなみに valueOfGmt を使うと下のような結果に。
文字列見たままの日時が GMT として解釈されて、出力結果のローカルタイムは +9(JST) されているのが分かる。

String txt='2018-12-01 01:23:45';
Datetime dt=Datetime.valueOfGmt(txt);

System.debug(dt);
System.debug('dt.format() = ' + dt.format());
System.debug('dt.format(\'yyyy/MM/dd HH:mm:ss\', \'Asia/Tokyo\') = ' + 
    dt.format('yyyy/MM/dd HH:mm:ss', 'Asia/Tokyo'));
System.debug('dt.formatGmt(\'yyyy/MM/dd HH:mm:ss\') = ' + 
    dt.formatGmt('yyyy/MM/dd HH:mm:ss'));
2018-12-01 01:23:45
dt.format() = 2018/12/01 10:23
dt.format('yyyy/MM/dd HH:mm:ss', 'Asia/Tokyo') = 2018/12/01 10:23:45
dt.formatGmt('yyyy/MM/dd HH:mm:ss') = 2018/12/01 01:23:45

Salesforce Apex Datetime クラスリファレンス

データローダで日付データをインポートすると1日前の日付が登録される

 

この記事が気に入ったら
いいね!しよう

最新情報をお届けします

Twitter でそらみみをフォローしよう!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です