Skip to content

リマインダーの改修案(日付の項目なし・タイムゾーン) ​

第1版作成 最終更新 (日本時間)
確認バージョン1.5.8.1

リマインダーについて、次の 2 つを本体の改修で実現する案です。ここにあるのは本体の標準機能ではありません。

  1. 日付の項目を指定せず、ビューの条件に合うレコードをそのまま送る
  2. Service.json の TimeZoneDefault に依存せずに送信時刻を決める

現行の動きは 通知のカスタムフォーマットとリマインダーの内部動作 を参照してください。

1. 日付の項目を必須にしない ​

前提にした現行実装(1.5.8.1) ​

日付の項目(Reminder.Column)は次の 3 か所で使われ、事実上必須です(Reminder.cs)。

メソッド使い方
GetColumn未指定なら CompletionTime にする。無ければ null(L157-L163)
GetDataTable取得列・「日付 < 今日 + Range 日」・「期限切れを除く」・「過去に完了したものも送る」・並び順のすべてにこの項目を使う(L520-L623)
GetBodyレコードをこの項目の日付でまとめ、「日付 (3 日後)」のような見出しを付ける(L455-L518)

設定画面の「項目」は日時型で作成日時・更新日時でない項目だけの選択肢で、空の選択肢はありません(SiteSettings.cs、SiteUtilities.cs)。ビューの条件(Condition)は日付の項目とは独立に WHERE 句になっています。

変更点 ​

#ファイル変更
1Models/Sites/SiteUtilities.csReminderColumn のドロップダウンに insertBlank: true を付け、空を選べるようにする
2Libraries/Settings/Reminder.cs の GetDataTableColumn が空なら、取得列に日付の項目を足さず、日付の 3 つの条件を付けない。並び順は ID の降順などにする
3Libraries/Settings/Reminder.cs の GetBodyColumn が空なら日付でまとめず、1 行ずつ Line とリンクを並べる
4設定画面のスクリプトColumn が空のときは「Range」「期限切れを除く」「過去に完了したものも送る」を無効にする(どれも日付の項目が前提)

GetColumn の CompletionTime へのフォールバックは、空を「未設定」ではなく「日付の項目なし」と区別できるように扱いを決めておく必要があります。現行は null のときだけフォールバックするので、空文字を「なし」とする方法が既存の設定と両立します。

GetDataTable・GetBody の変更イメージ
csharp
// GetDataTable
var orderByColumn = !Column.IsNullOrEmpty()
    ? ss.GetColumn(context: context, columnName: Column)
    : null;
var column = new SqlColumnCollection()
    .Add(column: ss.GetColumn(
        context: context,
        columnName: Rds.IdColumn(ss.ReferenceType)));
if (orderByColumn != null) column.Add(column: orderByColumn);
column.ItemTitle(ss.ReferenceType);
// …(宛先・件名・本文・Line の項目の追加は現行どおり)
var where = view.Where(
    context: context,
    ss: ss,
    checkPermission: false,
    requestSearchCondition: false);
if (orderByColumn != null)
{
    // 現行の「日付 < 今日 + Range」「期限切れを除く」「状況 / 過去に完了」の条件を追加
}
var orderBy = new SqlOrderByCollection().Add(
    column: orderByColumn ?? ss.GetColumn(
        context: context,
        columnName: Rds.IdColumn(ss.ReferenceType)),
    orderType: SqlOrderBy.Types.desc);
csharp
// GetBody(日付の項目なし)
dataRows.ForEach(dataRow =>
{
    sb.Append(ReplacedLine(context: context, ss: ss, dataRow: dataRow, line: Line));
    if (NotSendHyperLink != true)
    {
        sb.Append("\n\t", Locations.ItemEditAbsoluteUri(
            context: context,
            id: dataRow.Long(Rds.IdColumn(ss.ReferenceType))));
    }
    sb.Append("\n");
});

注意点 ​

  • 現行でも、日付が空のレコードは SQL の比較が偽になるため対象になりません。日付の項目なしの経路では、これらもビューの条件だけで対象になります。
  • GetBody の日付でのまとめを通さずに空の日付を扱うと、DataRow.DateTime が空の値を 1899/12/30 に変換する(DataRows.cs、Types.cs)ため、見出しに不正な日付が出ます。日付の項目なしのときは必ず 3 の分岐を通します。
  • 送る件数の上限は Reminder.json の Limit(既定 1000)のままです。日付で絞らない分、ビューの条件で対象を絞っておきます。
  • Column を設定済みの既存のリマインダーは従来どおり動きます。スキーマの変更は要りません(リマインダーはサイト設定の JSON に保存されます)。

2. タイムゾーンに依存しない送信時刻 ​

前提にした現行実装(1.5.8.1) ​

リマインダーを送る処理の context はログインを経ないため、タイムゾーンは TimeZoneDefault です(Context.cs)。

  • 送るかどうかは ScheduledTime <= DateTime.Now.ToLocal(context) で判定する(ReminderScheduleUtilities.cs)
  • 次回の ScheduledTime は、開始日時(入力値のまま保存)から TimeZoneDefault の「今」以降で最初の回(Times.cs)
  • 保存時の最初の ScheduledTime だけは、保存したユーザーのタイムゾーンの「今」を使う(SiteModel.cs)
  • 対象レコードの「今日 + Range 日」は TimeZoneDefault の今日を、サーバーローカルで格納された日付とそのまま比べる。「期限切れを除く」の基準(ScheduledTime)は ToUniversal でサーバーローカルに戻してから比べる(Reminder.cs)

つまり開始日時は「TimeZoneDefault の時計で何時か」として扱われ、TimeZoneDefault とサーバーの OS のタイムゾーン・利用者のタイムゾーンが違うと、送信時刻がその時差だけずれます。判定と次回の計算は同じ context で行うため、1.5.8.1 のソースでは、この時差で同じリマインダーが続けて送られる経路は確認できませんでした。

運用で避けるなら、TimeZoneDefault をサーバーの OS のタイムゾーンに合わせます。変換が恒等変換になり、改修は要りません。

案の比較 ​

案内容変更量複数タイムゾーン
1リマインダー用の context の TimeZoneInfo を TimeZoneInfo.Local にする小(ReminderScheduleUtilities の 2 か所の context 生成の後に 1 行ずつ)サーバーの時計に揃うだけ
2Reminder にタイムゾーン ID を持たせ、そのリマインダーの処理だけその context で行う中(プロパティ・設定画面・保存処理)リマインダーごとに指定できる

案 1 は、判定・次回の計算・対象レコードの範囲がすべてサーバーローカルの時計に揃い、ToLocal / ToUniversal が恒等変換になります。開始日時の意味は「サーバーの時計で何時か」に変わるため、TimeZoneDefault を UTC のまま運用していた環境では、既存のリマインダーの送信時刻が変わります。

ScheduledTime を選ぶ最初の判定(Remind(Context context) の中)は、外側の context を使います。ここも同じタイムゾーンにしないと、判定と次回の計算の時計がずれて、送ったのに ScheduledTime が「今」より前に残る状態になり得ます。案 1 では、外側(ReminderBackgroundTimer の CreateContext と、URL 呼び出しの ReminderSchedulesController)と内側(ReminderScheduleUtilities.Remind(Context, DataRow))の両方で揃えます。

csharp
// ReminderScheduleUtilities.Remind(Context context, DataRow dataRow) の context 生成の後
context.TimeZoneInfo = TimeZoneInfo.Local;

案 2 は、Reminder クラスに TimeZoneId を足し、GetRecordingData で保存、設定画面に選択欄を足します。リマインダーはサイト設定の JSON に保存されるので、テーブルの変更は要りません。未設定の既存のリマインダーは現行どおり TimeZoneDefault にすれば、送信時刻は変わりません。ScheduledTime の判定を SQL で一括に行っている部分は、リマインダーごとにタイムゾーンが違うと 1 本の比較で済まなくなるため、ScheduledTime を保存するときにサーバーローカルへ変換して持つ、などの設計が要ります。

確かめること ​

  • 変更前に登録済みのリマインダーの ScheduledTime を再計算するか(サイトを更新すると全リマインダーの ScheduledTime が作り直される。SiteModel.cs)
  • 他のバックグラウンド処理(DeleteSysLogsTimer など)には影響しないこと(リマインダーの context だけを変える)
  • 複数台構成で OS のタイムゾーンが揃っていること(案 1 はサーバーの時計に依存する)

関連ページ ​

変更履歴

第1版通知とリマインダーの書式・置き換わらない書き方・タイムゾーンの影響、システムログの外部通知の解説と、関連する改修・設計メモを追加