パラメータを差分の JSON だけで持つ
本体の標準機能ではありません
このページは、パラメータの持ち方を変える場合の設計メモです。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 にある項目だけを上書きするので、書いていない項目はクラスの既定値のまま残ります。
// 改修後のイメージ(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;
}{
"PageSize": 500
}必要な改修は次の 2 点です。
- 各クラスに、現在の JSON の値を初期値として書き写す。
Read<T>()を、ファイルが無ければnew T()を返す形(required: falseと?? new())にする。MultiTenant・Scim・PleasanterExtensionsはすでにこの形です(Initializer.cs#L143-L168)。
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 を主にする構成
既定値を変えたバージョンと理由を、項目の属性として残します。
[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 にすると読みやすくなりますが、コメント付きのままでは読み込めないため、参照用に限ります。