ビジネス ルール検証でモデルをビルドする
提供元: Microsoft
これは、ASP.NET MVC 1 を使用して小規模で完全な Web アプリケーションをビルドする方法を説明する無料の "NerdDinner" アプリケーション チュートリアルの手順 3 です。
手順 3 では、NerdDinner アプリケーションのデータベースのクエリと更新の両方に使用できるモデルを作成する方法を示します。
ASP.NET MVC 3 を使用している場合は、MVC 3 の概要または MVC Music Store に関するチュートリアルに従うことをお勧めします。
NerdDinner ステップ 3: モデルのビルド
Model-View-Controller フレームワークでは、「モデル」という用語は、アプリケーションのデータを表すオブジェクトと、検証とビジネス ルールを統合する対応するドメイン ロジックを指します。 モデルは多くの点で MVC ベースのアプリケーションの「中核」であり、後で説明するように基本的に動作を推進するものです。
ASP.NET MVC フレームワークでは、あらゆるデータ アクセス テクノロジの使用がサポートされており、開発者は、LINQ to Entities、LINQ to SQL、NHibernate、LLBLGen Pro、SubSonic、WilsonORM、または生の ADO.NET DataReader または DataSet など、さまざまな豊富な .NET データ オプションからモデルを実装できます。
NerdDinner アプリケーションでは、LINQ to SQL を使用して、データベース設計にかなり近い単純なモデルを作成し、カスタム検証ロジックとビジネス ルールを追加します。 次に、アプリケーションの残りの部分からデータ永続化の実装を抽象化するのに役立つリポジトリ クラスを実装し、簡単に単体テストできるようにします。
LINQ to SQL
LINQ to SQL は、.NET 3.5 の一部として付属している ORM (オブジェクト リレーショナル マッパー) です。
LINQ to SQL では、コードを記述できる .NET クラスにデータベース テーブルをマップする簡単な方法が提供されます。 NerdDinner アプリケーションでは、その方法を使用して、データベース内の Dinners テーブルと RSVP テーブルを Dinner クラスと RSVP クラスにマップします。 Dinners テーブルと RSVP テーブルの列は、Dinner クラスと RSVP クラスのプロパティに対応します。 Dinner オブジェクトと RSVP オブジェクトはそれぞれ、データベース内の Dinners テーブルまたは RSVP テーブル内の個別の行を表します。
LINQ to SQL を使用すると、データベース データを使用して Dinner オブジェクトと RSVP オブジェクトを取得および更新するための SQL ステートメントを手動で構築する必要がなくなります。 代わりに、Dinner クラスと RSVP クラス、データベースとのマッピング方法、およびそれらの間のリレーションシップを定義します。 LINQ to SQL では、対話して使用するときに実行時に使用する適切な SQL 実行ロジックの生成が処理されます。
VB および C# 内の LINQ 言語サポートを使用して、データベースから Dinner オブジェクトと RSVP オブジェクトを取得する表現型クエリを記述できます。 これにより、記述する必要があるデータ コードの量が最小限に抑えられ、非常にクリーンなアプリケーションをビルドできます。
プロジェクトに LINQ to SQL クラスを追加する
まず、プロジェクト内の [モデル] フォルダーを右クリックし、[追加->新しい項目] メニュー コマンドを選択します。
[新しい項目の追加] ダイアログが表示されます。 「データ」カテゴリでフィルター処理し、その中の「LINQ to SQL クラス」テンプレートを選択します。
項目に "NerdDinner" という名前を付け、[追加] ボタンをクリックします。 Visual Studio によって、\Models ディレクトリの下に NerdDinner.dbml ファイルが追加され、LINQ to SQL オブジェクト リレーショナル デザイナーが開きます。
LINQ to SQL でデータ モデル クラスを作成する
LINQ to SQL を使用すると、既存のデータベース スキーマからデータ モデル クラスをすばやく作成できます。 これを行うには、サーバー エクスプローラーで NerdDinner データベースを開き、モデル化するテーブルを選択します。
その後、テーブルを LINQ to SQL デザイナー画面にドラッグできます。 そうすると、LINQ to SQL はテーブルのスキーマを使用して Dinner クラスと RSVP クラスを自動的に作成します (データベース テーブル列にマップされるクラス プロパティを使用)。
既定では、LINQ to SQL デザイナーは、データベース スキーマに基づいてクラスを作成するときに、テーブル名と列名を自動的に「複数形化」します。 たとえば、上の例の "Dinners" テーブルは、"Dinner" クラスになります。 このクラスの名前付けは、モデルが .NET の名前付け規則と一致させるのに役立ちます。通常、デザイナーにこの修正をしてもらうと便利です (特に多数のテーブルを追加する場合)。 ただし、デザイナーが生成するクラスまたはプロパティの名前が気に入らない場合は、いつでもオーバーライドして任意の名前に変更できます。 変更するには、デザイナー内でエンティティ/プロパティ名をインラインで編集するか、プロパティ グリッドを使用して変更します。
既定では、LINQ to SQL デザイナーはテーブルの主キーと外部キーのリレーションシップも検査し、それに基づいて、作成する異なるモデル クラス間に既定の「リレーションシップの関連付け」が自動的に作成されます。 たとえば、Dinners テーブルと RSVP テーブルを LINQ to SQL デザイナーにドラッグすると、RSVP テーブルが Dinners テーブルに外部キーを持っていたという事実に基づいて、2 つの間の一対多リレーションシップの関連付けが推論されました (これはデザイナー内の矢印で示されます)。
上記の関連付けにより、LINQ to SQL は、開発者が特定の RSVP に関連付けられている Dinner にアクセスするために使用できる、厳密に型指定された "Dinner" プロパティを RSVP クラスに追加します。 また、Dinner クラスには、開発者が特定の Dinner に関連付けられている RSVP オブジェクトを取得および更新できるようにする "RSVP" コレクション プロパティが設定されます。
新しい RSVP オブジェクトを作成し、Dinner の RSVP コレクションに追加する場合の Visual Studio 内の IntelliSense の例を次に示します。 LINQ to SQL によって、Dinner オブジェクトに "RSVP" コレクションが自動的に追加されたことに注目してください。
Dinner の RSVP コレクションに RSVP オブジェクトを追加することで、データベース内の Dinner 行と RSVP 行の間に外部キーリレーションシップを関連付けるよう LINQ to SQL に指示します。
デザイナーによるテーブルの関連付けのモデル化または名前付けの方法が気に入らない場合は、オーバーライドできます。 デザイナー内の関連付け矢印をクリックし、プロパティ グリッドを使用してそのプロパティにアクセスして、名前の変更、削除、または変更を行うだけです。 ただし、NerdDinner アプリケーションの場合、既定の関連付けルールは、ビルドしているデータ モデル クラスに対して適切に機能し、既定の動作を使用できます。
NerdDinnerDataContext クラス
Visual Studio では、LINQ to SQL デザイナーを使用して定義されたモデルとデータベースのリレーションシップを表す .NET クラスが自動的に作成されます。 ソリューションに追加された LINQ to SQL デザイナー ファイルごとに、LINQ to SQL DataContext クラスも生成されます。 LINQ to SQL クラス項目に "NerdDinner" という名前を付けたため、作成された DataContext クラスは "NerdDinnerDataContext" と呼ばれます。 この NerdDinnerDataContext クラスが、データベースと対話する主な方法となります。
NerdDinnerDataContext クラスは、データベース内でモデル化した 2 つのテーブルを表す 2 つのプロパティ "Dinner" と "RSVP" を公開します。 C# を使用して、これらのプロパティに対して LINQ クエリを記述し、データベースから Dinner オブジェクトと RSVP オブジェクトのクエリを実行し、取得できます。
次のコードは、NerdDinnerDataContext オブジェクトのインスタンスを作成し、それに対して LINQ クエリを実行して、将来発生する Dinner のシーケンスを取得する方法を示しています。 Visual Studio では、LINQ クエリを記述するときに完全な IntelliSense が提供され、そこから返されるオブジェクトは厳密に型指定され、Intellisense もサポートされます。
Dinner オブジェクトと RSVP オブジェクトのクエリを実行できるだけでなく、NerdDinnerDataContext では、取得した Dinner オブジェクトと RSVP オブジェクトに対してその後に行った変更も自動的に追跡されます。 この機能を使用すると、明示的な SQL 更新コードを記述しなくても、変更をデータベースに簡単に保存できます。
たとえば、次のコードは、LINQ クエリを使用してデータベースから 1 つの Dinner オブジェクトを取得し、2 つの Dinner プロパティを更新して、変更をデータベースに保存する方法を示しています。
NerdDinnerDataContext db = new NerdDinnerDataContext();
// Retrieve Dinner object that reprents row with DinnerID of 1
Dinner dinner = db.Dinners.Single(d => d.DinnerID == 1);
// Update two properties on Dinner
dinner.Title = "Changed Title";
dinner.Description = "This dinner will be fun";
// Persist changes to database
db.SubmitChanges();
上記のコードの NerdDinnerDataContext オブジェクトは、そこから取得した Dinner オブジェクトに対するプロパティの変更を自動的に追跡しました。 "SubmitChanges()" メソッドを呼び出すと、データベースに対して適切な SQL "UPDATE" ステートメントが実行され、更新された値が保持されます。
DinnerRepository クラスを作成する
小規模なアプリケーションの場合、コントローラーを LINQ to SQL DataContext クラスに対して直接動作させ、LINQ クエリをコントローラー内に埋め込んでも問題ない場合があります。 しかし、アプリケーションが大きくなるにつれて、このアプローチは保守とテストが煩雑になります。 また、同じ LINQ クエリが複数の場所に複製される可能性もあります。
アプリケーションの保守とテストを容易にする 1 つのアプローチは、「リポジトリ」パターンを使用することです。 リポジトリ クラスは、データクエリと永続化ロジックをカプセル化するのに役立ち、アプリケーションからデータ永続化の実装の詳細を抽象化します。 アプリケーション コードをクリーンにするだけでなく、リポジトリ パターンを使用すると、将来的にデータ ストレージの実装を簡単に変更でき、実際のデータベースを必要とせずにアプリケーションの単体テストを容易に実行できます。
NerdDinner アプリケーションでは、次のシグネチャを持つ DinnerRepository クラスを定義します。
public class DinnerRepository {
// Query Methods
public IQueryable<Dinner> FindAllDinners();
public IQueryable<Dinner> FindUpcomingDinners();
public Dinner GetDinner(int id);
// Insert/Delete
public void Add(Dinner dinner);
public void Delete(Dinner dinner);
// Persistence
public void Save();
}
注: この章の後半では、このクラスから IDinnerRepository インターフェイスを抽出し、コントローラーでの依存関係の挿入を有効にします。 しかし、まずは、単純に DinnerRepository クラスを直接操作することから開始します。
このクラスを実装するには、[モデル] フォルダーを右クリックし、[新規> - 新しい項目] メニュー コマンドを選択します。 [新しい項目の追加] ダイアログで、[クラス] テンプレートを選択し、ファイルに "DinnerRepository.cs" という名前を付けます。
その後、次のコードを使用して DinnerRepository クラスを実装します。
public class DinnerRepository {
private NerdDinnerDataContext db = new NerdDinnerDataContext();
//
// Query Methods
public IQueryable<Dinner> FindAllDinners() {
return db.Dinners;
}
public IQueryable<Dinner> FindUpcomingDinners() {
return from dinner in db.Dinners
where dinner.EventDate > DateTime.Now
orderby dinner.EventDate
select dinner;
}
public Dinner GetDinner(int id) {
return db.Dinners.SingleOrDefault(d => d.DinnerID == id);
}
//
// Insert/Delete Methods
public void Add(Dinner dinner) {
db.Dinners.InsertOnSubmit(dinner);
}
public void Delete(Dinner dinner) {
db.RSVPs.DeleteAllOnSubmit(dinner.RSVPs);
db.Dinners.DeleteOnSubmit(dinner);
}
//
// Persistence
public void Save() {
db.SubmitChanges();
}
}
DinnerRepository クラスを使用した取得、更新、挿入、および削除
DinnerRepository クラスを作成したので、それを使用して実行できる一般的なタスクを示すいくつかのコード例を見てみましょう。
クエリの例
次のコードは、DinnerID 値を使用して 1 つの Dinner を取得します。
DinnerRepository dinnerRepository = new DinnerRepository();
// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);
次のコードは、今後予定されているすべての Dinner を取得し、それらをループします。
DinnerRepository dinnerRepository = new DinnerRepository();
// Retrieve all upcoming Dinners
var upcomingDinners = dinnerRepository.FindUpcomingDinners();
// Loop over each upcoming Dinner and print out its Title
foreach (Dinner dinner in upcomingDinners) {
Response.Write("Title" + dinner.Title);
}
挿入と更新の例
次のコードは、2 つの新しい Dinner を追加する方法を示しています。 リポジトリに対する追加/変更は、"Save()" メソッドが呼び出されるまでデータベースにコミットされません。 LINQ to SQL では、データベース トランザクション内のすべての変更が自動的にラップされます。そのため、リポジトリが保存されるときにすべての変更が発生するか、または発生しないかのいずれかとなります。
DinnerRepository dinnerRepository = new DinnerRepository();
// Create First Dinner
Dinner newDinner1 = new Dinner();
newDinner1.Title = "Dinner with Scott";
newDinner1.HostedBy = "ScotGu";
newDinner1.ContactPhone = "425-703-8072";
// Create Second Dinner
Dinner newDinner2 = new Dinner();
newDinner2.Title = "Dinner with Bill";
newDinner2.HostedBy = "BillG";
newDinner2.ContactPhone = "425-555-5151";
// Add Dinners to Repository
dinnerRepository.Add(newDinner1);
dinnerRepository.Add(newDinner2);
// Persist Changes
dinnerRepository.Save();
次のコードは、既存の Dinner オブジェクトを取得し、その 2 つのプロパティを変更します。 変更は、リポジトリで "Save()" メソッドが呼び出されたときにデータベースにコミットされます。
DinnerRepository dinnerRepository = new DinnerRepository();
// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);
// Update Dinner properties
dinner.Title = "Update Title";
dinner.HostedBy = "New Owner";
// Persist changes
dinnerRepository.Save();
次のコードでは、Dinner を取得し、それに RSVP を追加します。 これは、LINQ to SQL によって作成された Dinner オブジェクトの RSVP コレクションを使用して行われます (データベース内の 2 つの間に主キー/外部キーリレーションシップがあるため)。 この変更は、リポジトリで "Save()" メソッドが呼び出されたときに、新しい RSVP テーブル行としてデータベースに保持されます。
DinnerRepository dinnerRepository = new DinnerRepository();
// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);
// Create a new RSVP object
RSVP myRSVP = new RSVP();
myRSVP.AttendeeName = "ScottGu";
// Add RSVP to Dinner's RSVP Collection
dinner.RSVPs.Add(myRSVP);
// Persist changes
dinnerRepository.Save();
削除の例
次のコードは、既存の Dinner オブジェクトを取得し、削除対象としてマークします。 リポジトリで "Save()" メソッドが呼び出されると、削除がデータベースにコミットされます。
DinnerRepository dinnerRepository = new DinnerRepository();
// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);
// Mark dinner to be deleted
dinnerRepository.Delete(dinner);
// Persist changes
dinnerRepository.Save();
検証ロジックとビジネス ルール ロジックをモデル クラスに統合する
検証ロジックとビジネス ルール ロジックの統合は、データを処理するすべてのアプリケーションの重要な部分です。
スキーマの検証
LINQ to SQL デザイナーを使用してモデル クラスを定義する場合、データ モデル クラスのプロパティのデータ型は、データベース テーブルのデータ型に対応します。 たとえば、Dinners テーブルの "EventDate" 列が "datetime" の場合、LINQ to SQL によって作成されるデータ モデル クラスの型は "DateTime" (組み込みの .NET データ型) になります。 つまり、コードから整数またはブール値を割り当てようとするとコンパイル エラーが発生し、実行時に無効な文字列型を暗黙的に変換しようとすると自動的にエラーが発生します。
また、LINQ to SQL では、文字列を使用する際のエスケープ SQL 値も自動的に処理されます。これを使用すると、SQL インジェクション攻撃から保護できます。
検証とビジネス ルール ロジック
スキーマの検証は最初の手順として役立ちますが、それでは十分ではありません。 ほとんどの実際のシナリオでは、複数のプロパティにまたがる可能性があり、コードを実行し、多くの場合、モデルの状態を認識できる、より豊富な検証ロジックを指定する機能が必要です (たとえば、作成/更新/削除中、または「アーカイブ済み」のようなドメイン固有の状態内など)。 検証ルールを定義してモデル クラスに適用するために使用できるさまざまなパターンとフレームワークがあり、これに役立つ .NET ベースのフレームワークがいくつかあります。 ASP.NET MVC アプリケーション内では、それらのほとんどを使用できます。
NerdDinner アプリケーションの目的上、比較的簡単で単純なパターンを使用します。このパターンでは、Dinner モデル オブジェクトに IsValid プロパティと GetRuleViolations() メソッドを公開します。 IsValid プロパティは、検証ルールとビジネス ルールがすべて有効かどうかに応じて true または false を返します。 GetRuleViolations() メソッドは、ルール エラーの一覧を返します。
プロジェクトに「部分クラス」を追加して、Dinner モデルに IsValid と GetRuleViolations() を実装します。 部分クラスを使用すると、VS デザイナーによって管理されるクラス (LINQ to SQL デザイナーによって生成された Dinner クラスなど) にメソッド/プロパティ/イベントを追加し、ツールがコードに干渉しないようにすることができます。 プロジェクトに新しい部分クラスを追加するには、[\モデル] フォルダーを右クリックし、[新しい項目の追加] メニュー コマンドを選択します。 [新しい項目の追加] ダイアログで [クラス] テンプレートを選択し、Dinner.cs という名前を付けることができます。
[追加] ボタンをクリックすると、Dinner.cs ファイルがプロジェクトに追加され、IDE 内で開きます。 その後、次のコードを使用して、基本的な規則/検証の適用フレームワークを実装できます。
public partial class Dinner {
public bool IsValid {
get { return (GetRuleViolations().Count() == 0); }
}
public IEnumerable<RuleViolation> GetRuleViolations() {
yield break;
}
partial void OnValidate(ChangeAction action) {
if (!IsValid)
throw new ApplicationException("Rule violations prevent saving");
}
}
public class RuleViolation {
public string ErrorMessage { get; private set; }
public string PropertyName { get; private set; }
public RuleViolation(string errorMessage, string propertyName) {
ErrorMessage = errorMessage;
PropertyName = propertyName;
}
}
上記のコードに関するいくつかの注意事項:
- Dinner クラスの先頭には "partial" キーワードが付きます。つまり、その中に含まれるコードは、LINQ to SQL デザイナーによって生成/保守されるクラスと組み合わされ、1 つのクラスにコンパイルされます。
- RuleViolation クラスはプロジェクトに追加するヘルパー クラスで、ルール違反に関する詳細が提供されます。
- Dinner.GetRuleViolations() メソッドを使用すると、検証ルールとビジネス ルールが評価されます (後で実装します)。 その後、ルール エラーの詳細を提供する RuleViolation オブジェクトのシーケンスが返されます。
- Dinner.IsValid プロパティは、Dinner オブジェクトにアクティブな RuleViolation があるかどうかを示す便利なヘルパー プロパティを提供します。 これは、開発者が Dinner オブジェクトを使用していつでも事前にチェックできます (例外は発生しません)。
- Dinner.OnValidate() 部分メソッドは LINQ to SQL が提供するフックであり、Dinner オブジェクトがデータベース内で永続化されようとしている場合はいつでも通知を受け取ることができます。 上記の OnValidate() の実装により、Dinner が保存される前に RuleViolation がないことが確認されます。 無効な状態の場合は例外が発生し、LINQ to SQL によってトランザクションが中止されます。
このアプローチでは、検証ルールとビジネス ルールを統合できる簡単なフレームワークが提供されます。 ここでは、GetRuleViolations() メソッドに次のルールを追加します。
public IEnumerable<RuleViolation> GetRuleViolations() {
if (String.IsNullOrEmpty(Title))
yield return new RuleViolation("Title required","Title");
if (String.IsNullOrEmpty(Description))
yield return new RuleViolation("Description required","Description");
if (String.IsNullOrEmpty(HostedBy))
yield return new RuleViolation("HostedBy required", "HostedBy");
if (String.IsNullOrEmpty(Address))
yield return new RuleViolation("Address required", "Address");
if (String.IsNullOrEmpty(Country))
yield return new RuleViolation("Country required", "Country");
if (String.IsNullOrEmpty(ContactPhone))
yield return new RuleViolation("Phone# required", "ContactPhone");
if (!PhoneValidator.IsValidNumber(ContactPhone, Country))
yield return new RuleViolation("Phone# does not match country", "ContactPhone");
yield break;
}
C# の "yield return" 機能を使用して、RuleViolation のシーケンスを返しています。 上記の最初の 6 つのルール チェックでは、Dinner の文字列プロパティを null または空にすることはできません。 最後のルールはもう少し興味深く、プロジェクトに追加できる PhoneValidator.IsValidNumber() ヘルパー メソッドを呼び出して、ContactPhone の番号形式が Dinner の国/地域と一致することを確認します。
.NET の正規表現サポートを使用して、この電話検証サポートを実装できます。 以下は、プロジェクトに追加して国/地域固有の正規表現パターン チェックを追加できる、単純な PhoneValidator の実装です。
public class PhoneValidator {
static IDictionary<string, Regex> countryRegex = new Dictionary<string, Regex>() {
{ "USA", new Regex("^[2-9]\\d{2}-\\d{3}-\\d{4}$")},
{ "UK", new Regex("(^1300\\d{6}$)|(^1800|1900|1902\\d{6}$)|(^0[2|3|7|8]{1}[0-9]{8}$)|(^13\\d{4}$)|(^04\\d{2,3}\\d{6}$)")},
{ "Netherlands", new Regex("(^\\+[0-9]{2}|^\\+[0-9]{2}\\(0\\)|^\\(\\+[0-9]{2}\\)\\(0\\)|^00[0-9]{2}|^0)([0-9]{9}$|[0-9\\-\\s]{10}$)")},
};
public static bool IsValidNumber(string phoneNumber, string country) {
if (country != null && countryRegex.ContainsKey(country))
return countryRegex[country].IsMatch(phoneNumber);
else
return false;
}
public static IEnumerable<string> Countries {
get {
return countryRegex.Keys;
}
}
}
検証とビジネス ロジック違反の処理
上記の検証ルールとビジネス ルールのコードを追加したので、Dinner を作成または更新しようとすると、検証ロジック ルールが評価され、適用されます。
開発者は以下のようなコードを記述して、Dinner オブジェクトが有効かどうかを事前に判断し、例外を発生させることなく、その中のすべての違反の一覧を取得することができます。
Dinner dinner = dinnerRepository.GetDinner(5);
dinner.Country = "USA";
dinner.ContactPhone = "425-555-BOGUS";
if (!dinner.IsValid) {
var errors = dinner.GetRuleViolations();
// do something to fix the errors
}
Dinner を無効な状態で保存しようとすると、DinnerRepository で Save() メソッドを呼び出したときに例外が発生します。 これは、Dinner の変更を保存する前に Dinner.OnValidate() 部分メソッドが LINQ to SQL によって自動的に呼び出されることと、Dinner にルール違反が存在する場合に例外を発生させるコードを Dinner.OnValidate() に追加したために発生します。 この例外をキャッチし、修正する違反の一覧をリアクティブに取得できます。
Dinner dinner = dinnerRepository.GetDinner(5);
try {
dinner.Country = "USA";
dinner.ContactPhone = "425-555-BOGUS";
dinnerRepository.Save();
}
catch {
var errors = dinner.GetRuleViolations();
// do something to fix errors
}
検証ルールとビジネス ルールは、UI レイヤー内ではなくモデル レイヤー内に実装されるため、アプリケーション内のすべてのシナリオで適用され、使用されます。 後でビジネス ルールを変更または追加し、Dinner オブジェクトで動作するすべてのコードでこれらを順守させることができます。
アプリケーションと UI ロジック全体でこれらの変更を波及させることなく、ビジネス ルールを 1 か所で柔軟に変更できることは、適切に記述されたアプリケーションの証であり、MVC フレームワークによって奨励される利点です。
次の手順
これで、データベースのクエリと更新の両方に使用できるモデルが作成されました。
次に、プロジェクトにコントローラーとビューをいくつか追加し、HTML UI エクスペリエンスをビルドしてみましょう。