はじめに
プリザンターの DB には、日時がタイムゾーン情報なしの DateTime 型で格納されています。タイムゾーン情報を持たない値をどうやって正しく変換・表示しているのか、気になったことはないでしょうか。
実は内部では「サーバーローカル タイムゾーン」「ユーザー タイムゾーン」「UTC」の 3 つのタイムゾーンを使い分けて変換処理を行っています。本連載では、この仕組みを 4 回に分けて紹介します。
| 回 | テーマ |
|---|---|
| 第 1 回(本記事) | アーキテクチャ編 — 全体設計と変換の仕組み |
| 第 2 回 | サーバスクリプト編 — SS 固有の変換方式と注意点 |
| 第 3 回 | API 編 — API でのタイムゾーンの取り扱い |
| 第 4 回 | タイムゾーン混在環境編 — ユーザーのタイムゾーンが混在する場合の問題と対策 |
バージョン 1.5.1.0 を対象にしています
プリザンターに登場する 3 つのタイムゾーン
プリザンターの日時処理には、以下の 3 つのタイムゾーンが関係しています。
| 名称 | 実体 | 決定方法 | 用途 |
|---|---|---|---|
| サーバーローカル タイムゾーン | TimeZoneInfo.Local |
OS から自動取得 | DB 格納、内部処理の基準 |
| ユーザー タイムゾーン | context.TimeZoneInfo |
ユーザー設定 / パラメータ | 画面表示、フォーム入力、API 入出力 |
| UTC | DateTimeKind.Utc |
固定 | JavaScript エンジン(V8)の内部形式 |
ポイントは、DB にはサーバーローカル タイムゾーン の日時が格納されるという点です。UTC ではありません。
タイムゾーン設定の階層
タイムゾーンは 3 つの階層で設定され、優先順位に従って解決されます。
1. システムデフォルト(Service.json)
App_Data/Parameters/Service.json の TimeZoneDefault で、システム全体のデフォルトタイムゾーンを設定します。
{
"TimeZoneDefault": "Tokyo Standard Time"
}
この値は起動時に Environments.TimeZoneInfoDefault に解決され、以下の場面で使われます。
| 利用場面 | 説明 |
|---|---|
| ユーザー新規作成 |
TimeZone カラムのデフォルト値 |
| ユーザー一括登録 | タイムゾーン 未指定時のフォールバック |
Context 初期化 |
未認証リクエスト時の タイムゾーン |
| バックグラウンド SS | スケジュール タイムゾーン 未設定時のフォールバック |
TimeZoneDefault はサーバーローカル タイムゾーン(TimeZoneInfo.Local)とは別物です。サーバーローカル タイムゾーン は OS のタイムゾーン設定によって自動決定されます。
2. ユーザーごとのタイムゾーン
各ユーザーの管理画面でタイムゾーンを個別に設定できます。設定値は Users テーブルの TimeZone カラムに文字列で保存されます。
public string TimeZone = "UTC"; // DB カラム
public TimeZoneInfo TimeZoneInfo
{
get
{
return TimeZoneInfo.GetSystemTimeZones()
.FirstOrDefault(o => o.Id == TimeZone);
}
}
無効な値が設定されている場合のフォールバック順序は、設定値 → 東京標準時 → サーバーローカル タイムゾーン です。
3. Context.TimeZoneInfo の決定
実行時のタイムゾーンは Context.TimeZoneInfo として保持されます。
ログイン時や API 認証時にユーザーの タイムゾーン で上書きされ、未認証の場合は TimeZoneDefault がそのまま使われます。
DB 格納形式
プリザンターはすべての日時をサーバーローカル タイムゾーン で DB に格納します。
| RDBMS | 現在日時の取得 | 格納される タイムゾーン |
|---|---|---|
| SQL Server | getdate() |
サーバーローカル |
| PostgreSQL | CURRENT_TIMESTAMP |
サーバーローカル |
| MySQL | CURRENT_TIMESTAMP |
サーバーローカル |
サーバーの OS タイムゾーンを変更すると、既存データとの整合性が崩れます。運用中の変更は避けてください。
変換ヘルパー(ToLocal / ToUniversal)
日時変換の中核は Times.cs に定義された 2 つの拡張メソッドです。
| メソッド | 変換方向 | 用途 |
|---|---|---|
ToLocal(context) |
サーバーローカル → ユーザー タイムゾーン | DB 値を画面表示・API レスポンスに |
ToUniversal(context) |
ユーザー タイムゾーン → サーバーローカル | フォーム入力・API リクエストを DB に |
// サーバーローカル タイムゾーン → ユーザー タイムゾーン
public static DateTime ToLocal(this DateTime value, Context context)
{
var timeZoneInfo = context.TimeZoneInfo;
if (timeZoneInfo == null || timeZoneInfo.Id == TimeZoneInfo.Local.Id)
return value;
return TimeZoneInfo.ConvertTime(value, timeZoneInfo);
}
// ユーザー タイムゾーン → サーバーローカル タイムゾーン
public static DateTime ToUniversal(this DateTime value, Context context)
{
var timeZoneInfo = context.TimeZoneInfo;
if (timeZoneInfo == null || timeZoneInfo.Id == TimeZoneInfo.Local.Id)
return value;
return TimeZoneInfo.ConvertTime(value, timeZoneInfo, TimeZoneInfo.Local);
}
ToUniversal という名前ですが、UTC への変換ではありません。ユーザー タイムゾーン からサーバーローカル タイムゾーン への変換です。名前に惑わされないようにしましょう。
ユーザー タイムゾーン とサーバーローカル タイムゾーン が同一の場合は変換がスキップされるため、単一タイムゾーン環境では実質的に何も起きません。
Time クラスの二重保持
プリザンターの Time クラスは、1 つの日時に対してサーバーローカル タイムゾーン 値と表示用値の 2 つを保持します。
public class Time : IConvertable
{
public DateTime Value = 0.ToDateTime(); // サーバーローカル タイムゾーン(DB 保存値)
public DateTime DisplayValue = 0.ToDateTime(); // ユーザー タイムゾーン(表示用)
}
DB から読み取った時は Value(サーバーローカル)を ToLocal して DisplayValue(ユーザー タイムゾーン)にセットし、フォーム入力時は DisplayValue(ユーザー タイムゾーン)を ToUniversal して Value(サーバーローカル)にセットします。
フォーム入力から画面表示までの変換フロー
実際の変換フローを、サーバー タイムゾーン が UTC、ユーザー タイムゾーン が JST(+9)の例で見てみましょう。
入力と表示で対称的な変換が行われるため、同じユーザーが操作する限り日時のズレは発生しません。
入力経路ごとの変換方式
プリザンターには複数の入力経路があり、それぞれ異なる変換方式が使われます。
| 入力経路 | 変換メソッド | タイムゾーン 基準 |
|---|---|---|
| フォーム入力 | ToUniversal(context) |
ユーザー タイムゾーン |
| API リクエスト | ToUniversal(context) |
ユーザー タイムゾーン |
| CSV インポート | ToUniversal(context) |
ユーザー タイムゾーン |
| 計算式サーバスクリプト | ToUniversal(context) |
ユーザー タイムゾーン |
| 通常サーバスクリプト | ConvertTimeFromUtc(v, Local) |
UTC 固定 |
| 出力経路 | 変換メソッド | タイムゾーン 基準 |
|---|---|---|
| 画面表示 | ToLocal(context) |
ユーザー タイムゾーン |
| API レスポンス | ToLocal(context) |
ユーザー タイムゾーン |
| 計算式サーバスクリプト |
ToLocal(context) → 文字列化 |
ユーザー タイムゾーン |
| 通常サーバスクリプト | 変換なし(生の DateTime) | なし |
通常サーバスクリプトだけが独自の変換方式を使っている点が重要です。この詳細は第 2 回で解説します。
全体の変換フロー図
まとめ
プリザンターのタイムゾーン処理のアーキテクチャをまとめます。
- DB にはサーバーローカル タイムゾーン で日時が格納される(UTC ではない)
- タイムゾーン設定は
Service.json→ ユーザー設定の優先順位で解決される -
ToLocal/ToUniversalが変換の中核を担い、フォーム入力・API・CSV では対称的な変換が行われる -
ToUniversalは名前に反して UTC ではなくサーバーローカル タイムゾーン への変換である - 通常サーバスクリプトだけが独自の変換方式を使用する