DataModel (データモデル)
アプリケーションに永続化されるデータの形を表します。 RDBMSのテーブル定義を、トランザクションの境界ごとにまとめたものです。
データモデルの定義1個から、以下のモジュールが自動生成されます。 開発者は、これらの生成されたクラスやメソッドを利用して、アプリケーションのロジックを構築します。
1. Entity Framework Core を通したデータベース定義
データモデルの定義から、Entity Framework Core を使用したデータベース操作のための以下のモジュールが生成されます。
- DbContext設定: Entity Framework Coreの
DbContextにテーブル定義やリレーション設定が自動追加されます。自動生成されたあとのコードに任意のカスタマイズを加えることも可能です。特にOnModelCreatingメソッドでの Fluent API を通したDB定義は、 Entity Framework Core 独自の知識が必要となるため、ここを自動生成させることで開発者の負担を軽減します。 - Entityクラス: データベースのテーブルに対応するC#クラス(POCO)。Entity Framework Core のエンティティです。楽観排他制御用のバージョンや、最終更新時刻などのメタデータを表すカラム、ナビゲーションプロパティも自動生成されます。
2. CRUDメソッド
それぞれのデータモデルごとに、CRUD操作を行うためのメソッド群が自動生成されます。 Entity Framework Core の SaveChanges メソッドを直接使用する場合、楽観排他制御のカウントアップや基本的なエラーチェックは手動で実装する必要がありますが、これらのメソッドを使用することで、ある程度の共通処理が自動的に行われます。
- 新規登録処理 (
Create...): データをデータベースに新規登録するメソッド。 - 更新処理 (
Update...): 既存データを更新するメソッド。楽観排他制御(バージョンチェック)も自動的に行われます。 - 物理削除処理 (
Delete...): データを物理削除するメソッド。オプションで論理削除(削除データ専用の別テーブルに逃がす形)に変更可能。
上記のメソッドは、 Entity Framework Core のエンティティクラスではなく、自動生成される SaveCommandクラス を引数に取ります。これはエンティティクラスと似ていますが、保存用に最適化されています。例えば、あるデータモデルが外部参照を持つ場合、その更新処理では参照先のIDのみを扱う形になります。これにより、プログラマの実装ミスなどにより本来その更新処理で一緒に更新すべきでないデータまで更新してしまうリスクを減らすことができます。
また、開発者が独自のロジックを差し込むためのメソッドが用意されています。 以下のメソッドをオーバーライドして実装することで、上記自動生成されるCRUDメソッドに任意の処理を追加できます。
- 更新用SQL発効前処理(
OnBeforeCreate.../OnBeforeUpdate...)- データベース保存前のバリデーションや値の加工を行います。
- 主に、「アプリケーション内部のどの画面やバッチから更新されようとも、必ず満たされていなければならないそのデータモデルの制約」を実装するために利用します。
- 更新用SQL発効後、トランザクションコミット前処理(
OnAfterCreate.../OnAfterUpdate...)- CREATE 文や UPDATE 文が発行された後、トランザクションがコミットされる前に実行されます。
- 主に、メッセージング基盤やリードレプリカへの反映処理を実装するために利用することが想定されています。RDBMSのトリガーのような役割です。
更新の粒度
- 更新の範囲は必ずDataModelの集約の範囲となる。 つまり、ルート集約のテーブルがChildやChildren(子テーブルや明細テーブル)を持つ場合、必ずこれらの複数のテーブルはまとめて更新される。
- 楽観排他制御はルート集約のテーブルに対してのみ検査される。
- 更新の際は必ずテーブル全体が更新の対象となる。そのテーブルの特定のカラムのみUPDATEというSQLを発行することはない。
- 整合性のチェックは必ず集約全体の単位で行なわれるため。
- Childrenについては、1件ごとに更新前の状態とのディープイコールによる比較が行なわれる。最終更新時刻が更新されるのもディープイコールで差分があったもののみ。
トランザクション
- トランザクションの開始と終了はこれらのメソッドには含まれない。 このメソッドを呼び出す側で行なうこと。
- メソッドの内部で何らかのエラーが発生した場合、トランザクション全体のロールバックは行われない。
- そのかわり、メソッドの内部で SAVEPOINT が発行され、SAVEPOINT へのロールバックは行われる。このSAVEPOINTはこの集約の更新成功時にリリースされる。
例外
- メソッドの内部で何らかのエラーが発生した場合、 例外は送出されない。
- そのかわり、どの項目で何のエラーが起きたかの詳細な情報を持ったオブジェクトが返される。
処理概要
- 更新または物理削除の場合、このメソッド内部で、更新対象をデータベースから読みだす処理が行なわれる。更新対象が見つからなかった場合はエラーとなる。
- エラーチェック
- 新規登録または更新の場合、スキーマ定義で表現される制約の検査がこのメソッドの中で自動的に行われる。
- 必須入力チェック
- 文字列の最大長チェック
- 文字種チェック(半角英数字か否かの検査など)
- 桁数チェック(整数の桁数、小数の桁数)
- 動的列挙体の種別が不正でないかの検査
- 更新または物理削除の場合、楽観排他制御が自動的に検査される。
- EFCoreの
ConcurrencyCheckを用いて確認される。(UPDATE ... WHERE キー項目一致 AND Version = 指定されたバージョンの結果が1件か0件かで判定) - ただし、更新に使用する実際のバージョン値はこのメソッドの引数としてわたってきたものが使われる。そのため、どのバージョンをこのメソッドの引数として渡すかはよく注意すること。
- 特に、画面からの更新の場合、その画面を初期表示したときのバージョン値を渡さないと、実質的な楽観排他の意味が無くなってしまうので注意。
- EFCoreの
- 上記以外に、任意のチェック処理を差しはさむことができる。
- アプリケーションサービスの
OnBeforeCreate等3メソッドをオーバーライドして実装すること。 - ここで行なう検査は、どのユースケースから更新がかかる場合であっても必ず守られなければならないルールを実装する。
- 特定のユースケースの場合のみ行なわれる検査は、ここではなく、このメソッドを呼び出す側(主に
CommandModelで生成されるメソッド)で行なう。
- アプリケーションサービスの
- 新規登録または更新の場合、スキーマ定義で表現される制約の検査がこのメソッドの中で自動的に行われる。
- エラーチェックのみの場合、SaveChangesが実行される前に処理が中断される。
- 人間による画面操作での更新の場合に「エラーチェック → 更新を確定するかの確認 → 再度エラーチェック → 更新確定(SaveChanges)」という流れとするため。
- このメソッドに渡される引数のオプションの
ValidationOnlyがtrueか否かで判定している。
- UUIDやシーケンスの発番はこのメソッドの内部で行なわれる。
- データの更新者・更新日時等のメタデータはこのメソッドの内部で自動的に設定される。
- 更新の確定は EFCore の SaveChanges メソッドを呼び出すことで行われる。
- そのため、開発者が記述する DbContext 操作で何らかのクエリを発行する際は
.AsNoTracking()を指定すること。 - AsNoTracking が自動的にオフになるようにDbContextを構成しておくことを強く推奨する。
- そのため、開発者が記述する DbContext 操作で何らかのクエリを発行する際は
- 更新後処理
- この集約の更新をメッセージング基盤やリードレプリカへ反映するための処理を差しはさむことができる。
- アプリケーションサービスの
OnAfterCreateAsync等のメソッドをオーバーライドして実装すること。 - 更新後処理の内部で例外が発生した場合、SAVEPOINT へのロールバックが行われる。
4. ダミーデータ生成処理
デバッグ用のダミーデータ作成処理の雛形が生成される。
自動生成されたあとのソースで DummyDataGenerator またはそれを継承したクラスの GenerateAsync メソッドを呼ぶと実行される。
ダミーデータの作成はスキーマ定義の ref-to の依存関係の順番で行なわれるため、開発者はどの順番でダミーデータを作成するかを意識する必要はない。
DummyDataGenerator が作成するのはEFCoreのエンティティの配列までのため、
そのエンティティを使って実際にどの媒体に保存するかは開発者が実装する必要がある。
(そのままDbContextを使ってINSERTしてもよし、プロパティの値をExcel等に書き出して何かするもよし)
DummyDataGenerator を継承したクラスでは以下をカスタマイズできる。
- 集約毎のパターン。「この集約は100件欲しい」「この集約はこのステータスのパターンを掛け合わせた組み合わせテストのパターンが欲しい」など
- 集約毎のインスタンス1件作成処理。
- 型ごとの標準ダミー値の生成ロジック。「日付型の項目は現在時刻±1年の中からランダムな値としたい」など
5. 汎用参照テーブル
俗に汎用マスタ、区分マスタ、コードマスタなどと呼ばれるテーブル。 国コード、通貨単位、分析集計表の分類など、システムの運用中に値の追加削除が発生しうる区分が格納される。 明示的に指定したデータに対してのみ自動生成される。
データモデルを用いた更新処理の基本的な実装内容
CRUDメソッド自体は自動生成されますが、実際にアプリケーションで更新を行うには、手動実装するコマンドモデルの中でこれらのメソッドを呼び出す必要があります。 詳細な挙動はアプリケーションの仕様によって異なりますが、一般的な手動実装内容の流れは以下の通りです。
- 更新対象のデータを受け取る: DataModelの自動生成モジュールには、データの検索や取得を行うメソッド(GetやFindなど)は含まれません。データの読み込みは、ユースケースに応じて以下の方法で行います。
- 画面から受け取る: QueryModel や StructureModel を使用します。画面表示時にQueryModelや画面初期表示用CommandModelを使ってデータを取得し、ユーザーが編集した内容を更新用CommandModelの引数として受け取ります。この際、対象データには主キーや楽観排他のバージョン情報も必ず含める必要があります。
- ビジネスロジック内部で読み込む:
更新用のCommandModelの処理内部で、自動生成された
DbContextを使用し、Entity Framework Core の機能(LINQなど)を使ってデータベースから直接データを取得します。 例えば、月次処理や外部データ受信などといった、複雑な条件での抽出が必要な場合に適しています。
- 自動生成されないエラーチェックの実施: ユースケース固有のバリデーションがある場合はここで実施します。
- 更新を確定するかの確認: Webの場合、処理1巡目はエラーチェックのみ、その後ユーザーが「更新を確定しますか?」の確認に対して「はい」を選択した場合に2巡目でエラーチェックと実際の更新処理を行う、という形が一般的です。1巡目の場合はここで処理を終了します。
- トランザクションの開始:
自動生成された
DbContextを使用してトランザクションを開始します。 - データの登録・更新・削除を実行する:
- 新規登録の場合:
受け取ったデータを元に、DataModelの
SaveCommandクラスのインスタンスを作成します。構築したSaveCommandをCreate...メソッドに渡して呼び出します。 - 更新、削除の場合:
受け取ったデータの主キーを、自動生成された
Update...Delete...メソッドに渡します。Updateの場合、同メソッドの引数として、どの項目をどう更新するかも指定することができます。更新対象のデータの主キーとバージョン情報を正しく設定することが重要です。
- 新規登録の場合:
受け取ったデータを元に、DataModelの
- トランザクションのコミット:
自動生成された
DbContextを使用してトランザクションをコミット、またはロールバックします。一度に2件以上のデータを更新するケースで一部だけが失敗した場合にどうするかなどのルールは自由に決めることができます。
設計指針
1. データのまとまりとライフサイクル
DataModelは、 RDBのテーブルをライフサイクル単位でまとめたもの です。 基本的には、 楽観排他制御がかかるべき単位 で1つのDataModelを定義します。
例えば「受注」と「受注明細」がある場合、これらは通常セットで扱われ、明細だけが単独で存在することはありません。 この場合、「受注」をルートとする1つのDataModelとして定義し、「受注明細」はその子要素(Children)として定義します。 これにより、受注全体の整合性を保ちながらデータの登録・更新を行うことができます。
2. 設計のアプローチ
DataModelの設計は、以下のどちらのアプローチでも進めることができます。
- データ構造から考える : どのようなデータを管理する必要があるかを洗い出し、DataModelを定義してから、それを操作するCommandModelを設計する。
- ユースケースから考える : ユーザーが何を行いたいか(CommandModel)を定義し、そのために必要なデータの形としてDataModelを設計する。
3. キーの設計
主キー(IsKey)には、 サロゲートキー (自動採番IDなど)と ナチュラルキー (社員コード、商品コードなど)のどちらも使用可能です。
一般的なRDB設計の基準に従って、プロジェクトの要件に合わせて選択してください。
また、集約の種類によってキーの定義ルールが異なります。
- Root (ルート集約): 必ず1つ以上のメンバーに
IsKey属性を指定する必要があります。 - Child (1対1の子集約): 親のキーを自動的に継承するため、独自のキーを持つことはできません。
- Children (1対多の子集約): 親のキーを自動的に継承しますが、それに加えて Children 自身を一意に識別するためのキー(
IsKey)が必ず必要です。
4. 参照関係
他のDataModelへの参照(ref-to)は、RDBの 外部キー制約 として実装されます。
これにより、データベースレベルでの参照整合性が保証されます。
参照を主キーにするパターン
参照項目に IsKey="True" を設定することで、参照先の主キーを自分自身の主キーとして使用することができます。
これはRDB設計における「識別関係(Identifying Relationship)」に相当し、以下のような目的で使用されます。
- 1対1の関係: 参照項目のみを主キーとすることで、参照先1レコードに対して最大1レコードしか存在できないテーブルを作成できます。
- 排他制御の範囲調整:
- 通常、親子関係(
ChildやChildren)でデータを定義すると、子の更新時にも親の排他制御(バージョンチェック)が行われます。 - 一方、参照を主キーにした別のDataModelとして定義することで、キー(ID)は共有しつつも、排他制御の単位(集約)を分離することができます。
- これにより、「受注」と「出荷」のように、関連は強いが更新タイミングが異なるデータのロック競合を防ぐことができます。
- 通常、親子関係(
5. バリデーションとロジック
データの登録・更新時のバリデーションは、自動生成されるものと手動で実装するものに分かれます。
自動生成されるバリデーション
以下の基本的なバリデーションは自動生成され、データベースへの保存処理が走る前にアプリケーション側でチェックされます。
- 必須チェック:
IsRequired属性がついている項目、または主キー項目。DBのNOT NULL制約も自動的に付与されます。 - 最大長チェック:
MaxLength属性がついている文字列項目。DBの最大長制約も自動的に付与されます。 - 文字種チェック: 数値型や日付型など、型変換が可能かどうかのチェック。
手動で実装するバリデーション
上記以外の複雑なバリデーションは、生成される OnBeforeCreate や OnBeforeUpdate メソッドにロジックを記述します。
- 値の範囲チェック: 数値の範囲など(例: 0以上100以下)。
- 相関チェック: 複数項目間の整合性チェック(例: 開始日 が終了日より前であること)。
- 複雑なビジネスロジック: その他、独自の業務ルール。
また、特定のユースケース(画面や操作)でのみ必要なチェックは、CommandModel側の処理として記述することを推奨します。
注意: 循環参照について
DataModelでは、異なる集約間で相互に参照し合うようなスキーマ定義が可能です。 ただし、主キーまたは必須制約を持つ外部参照によって循環参照が発生し、かつその循環を構成する全ての外部参照が主キーまたは必須制約である場合、データの登録が不可能になるためバリデーションエラーとなります。 例えば、集約Aが集約Bを必須で参照し、かつ集約Bが集約Aを必須で参照するようなケースです。