モードレス自動保存とデータ整合性
問題: NOT NULL 制約と自動保存の矛盾
TASHIKA はモードレス自動保存(GL-007)を採用しており、画面遷移時に自動的にデータを保存する。 一方、エンティティ設計の原則として カラムは原則 NOT NULL とし、不完全なデータを NULL で表現しない。
この2つの方針は、複数画面にまたがる入力で矛盾する:
例: 配偶者情報の入力が3画面に分かれている場合
画面1: 氏名・生年月日を入力 → 自動保存
画面2: 所得見積額を入力 → 自動保存
画面3: 障害者区分を入力 → 自動保存
画面1の時点で正規化テーブルに保存しようとすると、
画面2・3のカラムが NOT NULL 制約に違反する。解決策: JSONB Draft + 正規化テーブル Submitted
Draft 状態の自動保存は 申告スナップショット(JSONB)に対して行い、 正規化テーブルへの展開は Draft → Submitted のライフサイクル遷移時に行う。
Draft 状態:
┌─────────────────────────────────────┐
│ DeclarationSnapshot (JSONB) │
│ → スキーマレスな構造 │
│ → 部分的・不完全なデータを許容 │
│ → 画面遷移ごとに自動保存 │
│ → バリデーションは UI レベルのみ │
└─────────────────────────────────────┘
Draft → Submitted 遷移時:
┌─────────────────────────────────────┐
│ JSONB → 正規化テーブルへの展開 │
│ → FluentValidation で全項目を検証 │
│ → NOT NULL 制約を完全に満たす │
│ → 検証失敗 → Submitted 不可 │
└─────────────────────────────────────┘
Submitted 以降:
┌─────────────────────────────────────┐
│ 正規化テーブル │
│ → FamilyMember, InsuranceDeduction │
│ 等の各テーブルにデータが存在 │
│ → 全カラム NOT NULL(原則) │
│ → RLS、FK 等の制約が有効 │
└─────────────────────────────────────┘この設計の利点
NOT NULL 原則との両立
- 正規化テーブルのカラムは原則 NOT NULL を維持できる
- 不完全なデータは JSONB の中にのみ存在し、正規化テーブルに漏れない
入力順序の自由度
- 従業員は任意の順序で画面を進められる(人的控除→保険料控除でも、逆でも可)
- 途中で中断し、後日再開しても問題ない
- JSONB はスキーマレスなので、部分的な更新が自然にできる
データモデルが UI に引きずられない
- 「画面1用テーブル」「画面2用テーブル」のような分割は不要
- 正規化テーブルの設計はドメインモデルに基づく(UI の画面分割とは独立)
- 画面構成が変わっても、正規化テーブルの構造は変わらない
既存の CQRS スナップショット方式との親和性
- 申告スナップショット(DM-406)は既に JSONB での状態保存として設計されている
- Draft 中の自動保存も同じ 申告スナップショット の仕組みに乗せられる
- スナップショットの蓄積→確定後凍結のライフサイクルと整合
バリデーションの二段階化
- Draft 中: UI レベルの軽いバリデーション(形式チェック、即座のフィードバック)
- Submitted 遷移時: FluentValidation による網羅的バリデーション(業務ルール、整合性チェック)
- 「入力途中でエラーを出さない」UX と「提出データの完全性保証」を両立
制約と注意点
- Draft 中の 税額計算エンジン 実行: Draft 中もリアルタイムで控除額を仮計算し UI に表示するが、 入力は JSONB から読み取る。計算結果は参考値であり、確定的な TaxCalculationResult は Submitted 以降で生成する
- JSONB のスキーマ進化: JSONB はスキーマレスだが、アプリケーション側で読み取る際のデシリアライズ互換性は管理が必要。 → バージョンフィールドを JSONB 内に持たせ、マイグレーション時の互換性を確保する
- 検索・集計: Draft 状態のデータは JSONB 内にあるため、SQL での検索・集計が困難。 管理者向けの進捗ダッシュボード等では JSONB 内の特定フィールドに GIN インデックスを設定するか、 Read Model に投影する設計が必要