Skip to content

パラメータを差分の JSON だけで持つ ​

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

本体の標準機能ではありません

このページは、パラメータの持ち方を変える場合の設計メモです。1.5.8.1 のパラメータは、全項目を書いた JSON ファイルを読み込み、バージョンアップでは CodeDefiner merge でバージョンごとのパッチを当てて引き継ぎます。

前提にした 1.5.8.1 の実装は次のとおりです(詳しくは CodeDefiner の「パラメータの引き継ぎ(merge)」と パラメータ の「読み込みのしくみ」)。

  • 起動時に Read<T>() が App_Data/Parameters/{クラス名}.json を読み込みます。ほとんどが必須で、ファイルが無いと起動できません(Initializer.cs#L339-L362)。
  • パラメータのクラス(Implem.ParameterAccessor/Parts/*.cs、97 ファイル)の多くは初期値を持たず、JSON に書かれた値が実質的な既定値です。Script(ServerScript = true など)、PleasanterExtensions、QuartzClustering(コンストラクタで設定)、ParameterSetting のように初期値を持つものもあります。
  • merge は ParametersPatch.zip に入ったバージョンごとの差分を、旧バージョンから新バージョンまで順に当てます。差分の無いバージョン間やダウングレードでは使えず、途中で失敗すると一部のファイルだけ書き換わった状態で残ります(Starter.cs#L240-L347、PatchParameters.cs#L15-L60)。
  • 1.5.8.1 には、画面から変えた項目の差分だけを DB の Parameters テーブルに持ち、起動時にファイルの値へ当てる仕組みもすでにあります(EnableScreenManagement)。ただし土台はファイルの全項目で、ファイルが無い状態では動きません。

merge の問題点 ​

問題内容影響
例外処理が無いJToken.Parse() と差分の適用に try-catch が無い。コメントや末尾カンマのある JSON で例外大
ロールバックが無い約 40 のファイルを順に書き換えるため、途中の例外で一部だけ新しい形になる大
バージョンの組に依存zip に新旧両方のバージョンのフォルダが必要。開発版・ホットフィックスとの間では使えない大
ダウングレード不可新 < 旧 は InvalidVersionException大
ファイル名だけで照合サブフォルダの位置を見ないので、同じ名前のファイルに当たる中
配列の差分JsonDiffPatch の差分形式は配列をインデックスで表すため、利用者が要素を足し引きしているとずれる中
名前順の並べ替えバージョンを各桁 2 桁にそろえた文字列の昇順で並べるので、桁が 3 桁になると順序が崩れる小

これらはバージョンごとの差分に依存する方式そのものから来るため、差分を配らずに、どのバージョン間でも(戻す方向でも)そのまま使える持ち方に変えることを考えます。

方式の候補 ​

案 A: C# の既定値 + 変更した項目だけの JSON ​

パラメータのクラスに既定値(フィールドの初期値)を書き、利用者は変えたい項目だけを JSON に書きます。Newtonsoft.Json の DeserializeObject<T>() は new T() を作ってから JSON にある項目だけを上書きするので、書いていない項目はクラスの既定値のまま残ります。

csharp
// 改修後のイメージ(Api.cs)
public class Api
{
    public decimal Version = 1.1m;
    public bool Enabled = true;
    public int PageSize = 200;
    public int LimitPerSite = 0;
    public bool Compatibility_1_3_12 = false;
}
json
{
    "PageSize": 500
}

必要な改修は次の 2 点です。

  1. 各クラスに、現在の JSON の値を初期値として書き写す。
  2. Read<T>() を、ファイルが無ければ new T() を返す形(required: false と ?? new())にする。MultiTenant・Scim・PleasanterExtensions はすでにこの形です(Initializer.cs#L143-L168)。
csharp
private static T Read<T>(bool required = true) where T : new()
{
    var name = typeof(T).Name;
    var json = Files.Read(JsonFilePath(name));
    if (json.IsNullOrEmpty())
    {
        if (required) throw new ParametersNotFoundException(name + ".json");
        return new T();
    }
    return json.Deserialize<T>()
        ?? throw new ParametersIllegalSyntaxException(name + ".json");
}
観点内容
バージョンごとの差分不要。新しい DLL を置けば既定値も新しくなる
ダウングレードできる(古い DLL に戻せば既定値も戻る。JSON はそのまま)
新しい項目クラスの既定値が使われる
消えた項目JSON に残っていても無視される
型が変わった項目JSON の値が新しい型に合わないと読み込みエラーになりうる
入れ子のオブジェクト= new() で初期化しておけば一部だけ上書きできる。配列(List<T>)は丸ごと置き換わる
手間初期値の書き写しが主で、ロジックの変更は小さい。既存の全項目入りの JSON もそのまま動く

案 B: 既定のファイルと上書き用のファイルを分ける ​

Parameters/*.json はリリースに同梱の既定値として毎回差し替え、利用者の変更は Parameters/Override/*.json に変えた項目だけを書きます。起動時に JObject.Merge()(配列は MergeArrayHandling.Replace)で重ねてからデシリアライズします。

  • 仕組みが単純で、利用者の変更が別のファイルに分かれる。
  • Initializer の改修と、初回だけ現在のパラメータと既定値の差を Override に書き出す移行ツールが要る。消えた項目が Override に残骸として残る。

案 C: 既定値を同梱して差分を報告するだけ ​

Parameters/Defaults/ に既定値の原本を同梱し、利用者の設定との違い(利用者が変えた項目、新しく増えた項目、廃止された項目)を報告するツールだけを用意します。反映は利用者が判断します。本体の改修は要りませんが、反映は手作業です。

案 D: 3-way マージ ​

旧バージョンの既定値・利用者の設定・新バージョンの既定値の 3 つを比べ、利用者の差 = 利用者の設定 - 旧既定値 を新既定値に当てます。向きに依存しないのでダウングレードにも使えますが、旧バージョンの既定値を同梱する必要があり、衝突の扱いが複雑です。

旧既定値 → 利用者旧既定値 → 新既定値採る値
変更あり変更なし利用者の値
変更なし変更あり新バージョンの値
変更あり変更あり(同じ値)どちらでもよい
変更あり変更あり(違う値)利用者の値(警告を出す)
-項目が増えた新バージョンの値
-項目が消えた消す(警告を出す)

案 E: 案 B + 案 D ​

普段は案 B で運用し、バージョンアップのときだけ 3-way マージで Override の互換性を確かめて直します。最も複雑です。

比較 ​

案差分の配布ダウングレード安定性利用者の変更の保持実装の手間本体の改修
A: C# の既定値 + 部分 JSON不要○○○小〜中要
B: 上書き用ファイル不要○○○中要
C: 差分の報告のみ不要○○手作業小〜中不要
D: 3-way マージ不要○△○大要
E: B + D不要○○○大要
現行(merge)要×△△--

案 A が最も単純で、既存のデシリアライズの動きをそのまま使えます。案 C の報告ツールを併用すると、バージョンアップで既定値が変わったことに気づきやすくなります。

既定値が変わったことを知らせる ​

案 A では、バージョンアップでクラスの既定値が変わると、JSON に書いていない項目は黙って新しい値に切り替わります。多くの場合は望ましい動きですが、タイムアウト・機能の有効 / 無効・セキュリティの設定などは管理者が知っておくべきです。JSON に書いていないことが「既定値でよいと判断した」のか「項目を知らない」のかは区別できないため、変わったことを別に知らせる仕組みが要ります。

方式検知精度手間変更理由の伝達本体の改修
1: 既定値のスナップショット比較(起動時)自動高小不可要
2: CodeDefiner のサブコマンドで差分を報告手動高中不可要
3: ビルド時に既定値の一覧を埋め込む自動高大不可要
4: [DefaultValue] 属性と初期値の一致を検証自動中中不可要
5: Git のタグ間でパラメータのクラスの差分を見る手動高小不可不要
6: 既定値を変えたバージョンを属性で記録自動高小可要

方式 1 は、起動時に new T() を JSON にしたものを前回起動時の保存分と比べます。Rds のように [OnDeserialized] で値を補うクラスがあるため、new T() → シリアライズ → デシリアライズ → シリアライズの往復をしてから比べる必要があります(Rds.cs#L19-L23)。方式 4 の [DefaultValue] 属性は Script.cs などで使われていますが、1.5.8.1 では実行時に読まれていません。

方式 2 で旧バージョンの DLL を同じプロセスに読み込むと、同じ名前の型が別の型になったり、依存するライブラリのバージョンがぶつかったりします。それぞれの DLL から既定値を JSON に書き出し、JSON どうしを比べる形が現実的です。

方式 6 を主にする構成 ​

既定値を変えたバージョンと理由を、項目の属性として残します。

csharp
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]
public class DefaultChangedInAttribute : Attribute
{
    public string Version { get; }
    public string OldValue { get; set; }
    public string NewValue { get; set; }
    public string Description { get; set; }
    public DefaultChangedInAttribute(string version) => Version = version;
}

public class Script
{
    [DefaultChangedIn("x.y.z.w", OldValue = "10000", NewValue = "30000",
        Description = "サーバースクリプトのタイムアウトを延長")]
    public long ServerScriptTimeOut { get; set; } = 30000;
}

図を読み込み中…

  • 複数のバージョンを飛ばして上げても、間の変更をすべて個別に知らせられます。保存するのはバージョン番号 1 つだけです。
  • 利用者が JSON に書いた項目は、既定値が変わっても影響が無いので知らせません。通知はログの WARN だけで、動作は変えません。
  • 属性の付け忘れは検知漏れになるため、方式 1 のスナップショット比較を安全網として併用し、方式 2 のサブコマンドでバージョンアップ前に確認できるようにします。

導入は、既定値の書き写しと Read<T>() の変更(段階 1)、属性と通知(段階 2)、スナップショット・サブコマンド(段階 3)の順に分けられます。段階 3 は、段階 2 のあと付け忘れが実際に問題になるかを見てからでも遅くありません。

項目の一覧(テンプレート)を失わないために ​

案 A にすると、全項目が書かれた JSON が無くなり、どの項目があるかを JSON で確かめられなくなります。new T() をシリアライズすれば全項目と既定値の JSON を作れるので(方式 1 と同じ処理)、CodeDefiner のサブコマンドで任意のフォルダに書き出す、ビルド時に作る、リリースに参照用として同梱する、のいずれかで補えます。属性の情報をコメントとして付けた JSONC にすると読みやすくなりますが、コメント付きのままでは読み込めないため、参照用に限ります。

関連ページ ​

変更履歴

第1版CodeDefiner のデータベース作成・更新とパラメータの引き継ぎ、画面でのパラメータ管理、MCP エンドポイントのブラウザアクセスの解説と、関連する改修・設計メモを追加