0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

プリザンターのタイムゾーンの考え方を詳しくみてみる[第1回:アーキテクチャ編]

0
Posted at

はじめに

プリザンターの 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.jsonTimeZoneDefault で、システム全体のデフォルトタイムゾーンを設定します。

App_Data/Parameters/Service.json
{
    "TimeZoneDefault": "Tokyo Standard Time"
}

この値は起動時に Environments.TimeZoneInfoDefault に解決され、以下の場面で使われます。

利用場面 説明
ユーザー新規作成 TimeZone カラムのデフォルト値
ユーザー一括登録 タイムゾーン 未指定時のフォールバック
Context 初期化 未認証リクエスト時の タイムゾーン
バックグラウンド SS スケジュール タイムゾーン 未設定時のフォールバック

TimeZoneDefault はサーバーローカル タイムゾーン(TimeZoneInfo.Local)とは別物です。サーバーローカル タイムゾーン は OS のタイムゾーン設定によって自動決定されます。

2. ユーザーごとのタイムゾーン

各ユーザーの管理画面でタイムゾーンを個別に設定できます。設定値は Users テーブルの TimeZone カラムに文字列で保存されます。

Implem.Pleasanter/Models/Users/UserModel.cs
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 に
Implem.Pleasanter/Libraries/Server/Times.cs
// サーバーローカル タイムゾーン → ユーザー タイムゾーン
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 つを保持します。

Implem.Pleasanter/Libraries/DataTypes/Time.cs
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 ではなくサーバーローカル タイムゾーン への変換である
  • 通常サーバスクリプトだけが独自の変換方式を使用する
0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?