ASP.NET MVC アプリケーションでの リポジトリ パターンおよび Unit of Work パターンの実装 (9/10)
著者: Tom Dykstra
Contoso University のサンプル Web アプリケーションでは、Entity Framework 5 Code First と Visual Studio 2012 を使用して ASP.NET MVC 4 アプリケーションを作成する方法を示します。 チュートリアル シリーズについては、シリーズの最初のチュートリアルを参照してください。
Note
解決できない問題が発生した場合は、完了した章をダウンロードして、問題を再現してみてください。 通常、完成したコードと自分のコードを比較することで、問題の解決策を見つけることができます。 一般的なエラーとその解決方法については、「エラーと回避策」をご覧ください。
前のチュートリアルでは、継承を使用して、Student
エンティティ クラスと Instructor
エンティティ クラスの冗長コードを減らしました。 このチュートリアルでは、CRUD 操作に リポジトリ パターンおよび Unit of Work パターンを使用するいくつかの方法について説明します。 前のチュートリアルと同様に、このチュートリアルでは、新しいページを作成するのではなく、既に作成したページでのコードの動作方法を変更します。
リポジトリ パターンおよび Unit of Work パターン
リポジトリ パターンおよび Unit of Work パターンは、アプリケーションのデータ アクセス層とビジネス ロジック層の間に抽象化レイヤーを作成するためのものです。 これらのパターンを実装すると、データ ストアの変更からアプリケーションを隔離でき、自動化された単体テストやテスト駆動開発 (TDD) を円滑化できます。
このチュートリアルでは、エンティティ型ごとにリポジトリ クラスを実装します。 Student
エンティティ型の場合は、リポジトリ インターフェイスとリポジトリ クラスを作成します。 コントローラーでリポジトリをインスタンス化するときは、コントローラーがリポジトリ インターフェイスを実装する任意のオブジェクトへの参照を受け入れるようにするために、インターフェイスを使用します。 コントローラーが Web サーバーで実行されると、Entity Framework で動作するリポジトリを受け取ります。 コントローラーが単体テスト クラスで実行されると、メモリ内コレクションなど、テスト用に簡単に操作できる方法で格納されたデータを操作するリポジトリを受け取ります。
チュートリアルの後半では、Course
コントローラー内の Course
エンティティ型と Department
エンティティ型に対して、複数の リポジトリ クラスおよび Unit of Work クラスを使用します。 Unit of Work クラスは、すべてのリポジトリが共有する単一のデータベース コンテキスト クラスを作成することで、複数のリポジトリの作業を調整します。 自動単体テストを実行できるようにする場合は、Student
リポジトリの場合と同じ方法で、これらのクラスのインターフェイスを作成して使用します。 ただし、チュートリアルをシンプルにするために、ここではインターフェイスなしでこれらのクラスを作成して使用します。
次の図は、リポジトリまたは Unit of Work パターンをまったく使用しない場合と比較して、コントローラーとコンテキスト クラスの関係を概念化する 1 つの方法を示しています。
このチュートリアル シリーズでは単体テストを作成しません。 リポジトリ パターンを使用する MVC アプリケーションでの TDD の概要については、「チュートリアル: ASP.NET MVC での TDD の使用」を参照してください。 リポジトリ パターンの詳細については、次のリソースを参照してください:
- MSDN の「リポジトリ パターン」。
- Julie Lerman のブログに投稿された「アジャイル Entity Framework 4 リポジトリ」シリーズ。
- Dan Wahlin のブログの、「HTML5/jQuery アプリケーションでアカウントを構築するための概要」。
Note
リポジトリ パターンと Unit of Work パターンを実装するには、さまざまな方法があります。 リポジトリ クラスは、Unit of Work クラスの有無にかかわらず使用できます。 すべてのエンティティ型に対して 1 つのリポジトリを実装することも、型ごとにそれぞれ実装することもできます。 型ごとにそれぞれ実装する場合は、個別のクラス、ジェネリック 基底クラスと派生クラス、または抽象基底クラスと派生クラスを使用できます。 ビジネス ロジックをリポジトリに含めたり、データ アクセス ロジックに制限したりできます。 エンティティ セットで DbSet 型の代わりに IDbSet インターフェイスを使用して、抽象化レイヤーをデータベース コンテキスト クラスに構築することもできます。 このチュートリアルで示す抽象化レイヤーを実装する方法は、すべてのシナリオと環境の推奨事項ではなく、検討すべき 1 つのオプションです。
Student リポジトリ クラスの作成
DAL フォルダーで、IStudentRepository.cs という名前のクラス ファイルを作成し、既存のコードを次のコードに置き換えます:
using System;
using System.Collections.Generic;
using ContosoUniversity.Models;
namespace ContosoUniversity.DAL
{
public interface IStudentRepository : IDisposable
{
IEnumerable<Student> GetStudents();
Student GetStudentByID(int studentId);
void InsertStudent(Student student);
void DeleteStudent(int studentID);
void UpdateStudent(Student student);
void Save();
}
}
このコードでは、2 つの読み取りメソッド (すべての Student
エンティティを返すメソッドと ID で 1 つの Student
エンティティを検索するメソッド) を含む、一般的な CRUD メソッドのセットを宣言します。
DAL フォルダーに、StudentRepository.cs ファイルという名前のクラス ファイル作成します。 既存のコードを、IStudentRepository
インターフェイスを実装する次のコードに置き換えます:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using ContosoUniversity.Models;
namespace ContosoUniversity.DAL
{
public class StudentRepository : IStudentRepository, IDisposable
{
private SchoolContext context;
public StudentRepository(SchoolContext context)
{
this.context = context;
}
public IEnumerable<Student> GetStudents()
{
return context.Students.ToList();
}
public Student GetStudentByID(int id)
{
return context.Students.Find(id);
}
public void InsertStudent(Student student)
{
context.Students.Add(student);
}
public void DeleteStudent(int studentID)
{
Student student = context.Students.Find(studentID);
context.Students.Remove(student);
}
public void UpdateStudent(Student student)
{
context.Entry(student).State = EntityState.Modified;
}
public void Save()
{
context.SaveChanges();
}
private bool disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
context.Dispose();
}
}
this.disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}
データベース コンテキストはクラス変数で定義され、コンストラクターは呼び出し元オブジェクトがコンテキストのインスタンスで渡される必要があります:
private SchoolContext context;
public StudentRepository(SchoolContext context)
{
this.context = context;
}
リポジトリで新しいコンテキストをインスタンス化することもできますが、1 つのコントローラーで複数のリポジトリを使用した場合、それぞれが個別のコンテキストになります。 後で Course
コントローラーで複数のリポジトリを使用します。また、Unit of Work クラスで、すべてのリポジトリが同じコンテキストを確実に使用できるようにする方法を確認します。
リポジトリは IDisposable を実装し、コントローラーで前に確認したようにデータベース コンテキストを破棄します。また CRUD メソッドは、前に見たのと同じ方法でデータベース コンテキストを呼び出します。
リポジトリを使用するように Student コントローラーを変更する
StudentController.cs で、クラス内の現在のコードを次のコードに置き換えます。 変更が強調表示されます。
using System;
using System.Data;
using System.Linq;
using System.Web.Mvc;
using ContosoUniversity.Models;
using ContosoUniversity.DAL;
using PagedList;
namespace ContosoUniversity.Controllers
{
public class StudentController : Controller
{
private IStudentRepository studentRepository;
public StudentController()
{
this.studentRepository = new StudentRepository(new SchoolContext());
}
public StudentController(IStudentRepository studentRepository)
{
this.studentRepository = studentRepository;
}
//
// GET: /Student/
public ViewResult Index(string sortOrder, string currentFilter, string searchString, int? page)
{
ViewBag.CurrentSort = sortOrder;
ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";
if (searchString != null)
{
page = 1;
}
else
{
searchString = currentFilter;
}
ViewBag.CurrentFilter = searchString;
var students = from s in studentRepository.GetStudents()
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
|| s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
}
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default: // Name ascending
students = students.OrderBy(s => s.LastName);
break;
}
int pageSize = 3;
int pageNumber = (page ?? 1);
return View(students.ToPagedList(pageNumber, pageSize));
}
//
// GET: /Student/Details/5
public ViewResult Details(int id)
{
Student student = studentRepository.GetStudentByID(id);
return View(student);
}
//
// GET: /Student/Create
public ActionResult Create()
{
return View();
}
//
// POST: /Student/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(
[Bind(Include = "LastName, FirstMidName, EnrollmentDate")]
Student student)
{
try
{
if (ModelState.IsValid)
{
studentRepository.InsertStudent(student);
studentRepository.Save();
return RedirectToAction("Index");
}
}
catch (DataException /* dex */)
{
//Log the error (uncomment dex variable name after DataException and add a line here to write a log.
ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
}
return View(student);
}
//
// GET: /Student/Edit/5
public ActionResult Edit(int id)
{
Student student = studentRepository.GetStudentByID(id);
return View(student);
}
//
// POST: /Student/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
[Bind(Include = "LastName, FirstMidName, EnrollmentDate")]
Student student)
{
try
{
if (ModelState.IsValid)
{
studentRepository.UpdateStudent(student);
studentRepository.Save();
return RedirectToAction("Index");
}
}
catch (DataException /* dex */)
{
//Log the error (uncomment dex variable name after DataException and add a line here to write a log.
ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
}
return View(student);
}
//
// GET: /Student/Delete/5
public ActionResult Delete(bool? saveChangesError = false, int id = 0)
{
if (saveChangesError.GetValueOrDefault())
{
ViewBag.ErrorMessage = "Delete failed. Try again, and if the problem persists see your system administrator.";
}
Student student = studentRepository.GetStudentByID(id);
return View(student);
}
//
// POST: /Student/Delete/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(int id)
{
try
{
Student student = studentRepository.GetStudentByID(id);
studentRepository.DeleteStudent(id);
studentRepository.Save();
}
catch (DataException /* dex */)
{
//Log the error (uncomment dex variable name after DataException and add a line here to write a log.
return RedirectToAction("Delete", new { id = id, saveChangesError = true });
}
return RedirectToAction("Index");
}
protected override void Dispose(bool disposing)
{
studentRepository.Dispose();
base.Dispose(disposing);
}
}
}
コントローラーは、コンテキスト クラスの代わりに IStudentRepository
インターフェイスを実装するオブジェクトのクラス変数を宣言するようになりました:
private IStudentRepository studentRepository;
既定の (パラメーターなしの) コンストラクターは新しいコンテキスト インスタンスを作成し、省略可能なコンストラクターを使用すると、呼び出し元はコンテキスト インスタンスを渡すことができます。
public StudentController()
{
this.studentRepository = new StudentRepository(new SchoolContext());
}
public StudentController(IStudentRepository studentRepository)
{
this.studentRepository = studentRepository;
}
(依存関係の挿入 または DI を使用していた場合は、DI ソフトウェアによって正しいリポジトリ オブジェクトが常に提供されるため、既定のコンストラクターは必要ありません。)
CRUD メソッドでは、コンテキストの代わりにリポジトリが呼び出されるようになりました:
var students = from s in studentRepository.GetStudents()
select s;
Student student = studentRepository.GetStudentByID(id);
studentRepository.InsertStudent(student);
studentRepository.Save();
studentRepository.UpdateStudent(student);
studentRepository.Save();
studentRepository.DeleteStudent(id);
studentRepository.Save();
そして、Dispose
メソッドはコンテキストの代わりにリポジトリを破棄するようになりました:
studentRepository.Dispose();
サイトを実行し、[Student] タブをクリックします。
ページはリポジトリを使用するようにコードを変更する前と同じように表示され、動作し、他の Student ページも同じように動作します。 ただし、コントローラーの Index
メソッドがフィルター処理と順序付けを行う方法には重要な違いがあります。 このメソッドの元のバージョンには、次のコードが含まれていました:
var students = from s in context.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
|| s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
}
更新された Index
メソッドには、次のコードが含まれています:
var students = from s in studentRepository.GetStudents()
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
|| s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
}
強調表示されているコードのみが変更されています。
元のバージョンのコードでは、 students
は IQueryable
オブジェクトとして型指定されています。 クエリは、ToList
などのメソッドを使用してコレクションに変換されるまで、データベースに送信されません。これはインデックス ビューが Student モデルにアクセスするまで発生しません。 上記の元のコードの Where
メソッドは、データベースに送信される SQL クエリの WHERE
句になります。 つまり、選択したエンティティのみがデータベースによって返されます。 ただし、context.Students
を studentRepository.GetStudents()
に変更した結果として、このステートメントの後の students
変数は、データベース内のすべての Student を含む IEnumerable
コレクションです。 Where
メソッドを適用した結果は同じですが、データベースではなく、Web サーバー上のメモリで作業が行われます。 大量のデータを返すクエリの場合、これは非効率的な場合があります。
ヒント
IQueryable vs.IEnumerable
ここに示すようにリポジトリを実装した後、[検索] ボックスに何かを入力した場合でも、SQL Server に送信されたクエリでは検索条件が含まれていないため、すべての Student 行が返されます:
SELECT
'0X0X' AS [C1],
[Extent1].[PersonID] AS [PersonID],
[Extent1].[LastName] AS [LastName],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[EnrollmentDate] AS [EnrollmentDate]
FROM [dbo].[Person] AS [Extent1]
WHERE [Extent1].[Discriminator] = N'Student'
リポジトリが検索条件を知らずにクエリを実行したため、このクエリはすべての Student データを返します。 並べ替え、検索条件の適用、ページング用のデータのサブセットの選択 (この場合は 3 行のみ表示) のプロセスは、後でメソッドがコレクションで呼び出されたときに ToPagedList
メモリ内で IEnumerable
実行されます。
以前のバージョンのコード (リポジトリを実装する前) では、IQueryable
オブジェクトに対して ToPagedList
が呼び出されたときに、検索条件を適用するまで、クエリはデータベースに送信されません。
ToPagedList が IQueryable
オブジェクトで呼び出されると、SQL Server に送信されるクエリによって検索文字列が指定され、その結果、検索条件を満たす行のみが返され、メモリ内でフィルター処理を実行する必要はありません。
exec sp_executesql N'SELECT TOP (3)
[Project1].[StudentID] AS [StudentID],
[Project1].[LastName] AS [LastName],
[Project1].[FirstName] AS [FirstName],
[Project1].[EnrollmentDate] AS [EnrollmentDate]
FROM ( SELECT [Project1].[StudentID] AS [StudentID], [Project1].[LastName] AS [LastName], [Project1].[FirstName] AS [FirstName], [Project1].[EnrollmentDate] AS [EnrollmentDate], row_number() OVER (ORDER BY [Project1].[LastName] ASC) AS [row_number]
FROM ( SELECT
[Extent1].[StudentID] AS [StudentID],
[Extent1].[LastName] AS [LastName],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[EnrollmentDate] AS [EnrollmentDate]
FROM [dbo].[Student] AS [Extent1]
WHERE (( CAST(CHARINDEX(UPPER(@p__linq__0), UPPER([Extent1].[LastName])) AS int)) > 0) OR (( CAST(CHARINDEX(UPPER(@p__linq__1), UPPER([Extent1].[FirstName])) AS int)) > 0)
) AS [Project1]
) AS [Project1]
WHERE [Project1].[row_number] > 0
ORDER BY [Project1].[LastName] ASC',N'@p__linq__0 nvarchar(4000),@p__linq__1 nvarchar(4000)',@p__linq__0=N'Alex',@p__linq__1=N'Alex'
(次のチュートリアルでは、SQL Server に送信されたクエリを調べる方法について説明します。)
次のセクションでは、データベースが作業を行う必要があることを指定できるようにするリポジトリ メソッドを実装する方法を示します。
これで、コントローラーと Entity Framework データベース コンテキストの間に抽象化レイヤーが作成されました。 このアプリケーションで自動単体テストを実行する場合は、IStudentRepository
を実装する単体テスト プロジェクトに代替リポジトリ クラスを作成できます。このモック リポジトリ クラスは、コンテキストを呼び出してデータの読み取りと書き込みを行う代わりに、コントローラー関数をテストするためにメモリ内コレクションを操作できます。
汎用リポジトリと Unit of Work クラスを実装する
エンティティの種類ごとにリポジトリ クラスを作成すると、多くの冗長なコードが生成され、部分的な更新が発生する可能性があります。 たとえば、同じトランザクションの一部として、2 つの異なるエンティティの種類を更新する必要があるとします。 それぞれが個別のデータベース コンテキスト インスタンスを使用している場合、1 つは成功し、もう 1 つは失敗する可能性があります。 冗長なコードを最小限に抑える方法の 1 つは、汎用リポジトリを使用することです。また、すべてのリポジトリが同じデータベース コンテキストを使用 (したがって、すべての更新を調整する) ようにする 1 つの方法は、Unit of Work クラスを使用することです。
チュートリアルのこのセクションでは、GenericRepository
クラスと UnitOfWork
クラスを作成し、Course
コントローラーでそれらを使用して、Department
エンティティ セットと Course
エンティティ セットの両方にアクセスします。 前に説明したように、チュートリアルのこの部分をシンプルにするために、これらのクラスのインターフェイスは作成しません。 ただし、TDD を支援するためにそれらを使用する場合は、通常、Student
リポジトリと同じ方法でインターフェイスを使用して実装します。
汎用リポジトリを作成する
DAL フォルダーで、GenericRepository.cs を作成し、既存のコードを次のコードに置き換えます:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using System.Data.Entity;
using ContosoUniversity.Models;
using System.Linq.Expressions;
namespace ContosoUniversity.DAL
{
public class GenericRepository<TEntity> where TEntity : class
{
internal SchoolContext context;
internal DbSet<TEntity> dbSet;
public GenericRepository(SchoolContext context)
{
this.context = context;
this.dbSet = context.Set<TEntity>();
}
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
{
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
}
public virtual TEntity GetByID(object id)
{
return dbSet.Find(id);
}
public virtual void Insert(TEntity entity)
{
dbSet.Add(entity);
}
public virtual void Delete(object id)
{
TEntity entityToDelete = dbSet.Find(id);
Delete(entityToDelete);
}
public virtual void Delete(TEntity entityToDelete)
{
if (context.Entry(entityToDelete).State == EntityState.Detached)
{
dbSet.Attach(entityToDelete);
}
dbSet.Remove(entityToDelete);
}
public virtual void Update(TEntity entityToUpdate)
{
dbSet.Attach(entityToUpdate);
context.Entry(entityToUpdate).State = EntityState.Modified;
}
}
}
クラス変数は、データベース コンテキストと、リポジトリがインスタンス化されるエンティティ セットに対して宣言されます:
internal SchoolContext context;
internal DbSet dbSet;
コンストラクターは、データベース コンテキスト インスタンスを受け入れ、エンティティ セット変数を初期化します:
public GenericRepository(SchoolContext context)
{
this.context = context;
this.dbSet = context.Set<TEntity>();
}
Get
メソッドでは、ラムダ式を使用して、結果を並べる呼び出し元のコードでフィルター条件と列を指定できます。文字列パラメーターを使用すると、呼び出し元は一括読み込み用のナビゲーション プロパティのコンマ区切りリストを提供できます:
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
このコード Expression<Func<TEntity, bool>> filter
は、呼び出し元が TEntity
型に基づいてラムダ式を提供することを意味し、この式はブール値を返します。 たとえば、Student
エンティティ型に対してリポジトリがインスタンス化されている場合、呼び出し元のメソッドのコードで filter
パラメーターに student => student.LastName == "Smith
" を指定できます。
また、このコード Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy
は、呼び出し元がラムダ式を提供することを意味します。 ただし、この場合、式への入力は TEntity
型の IQueryable
オブジェクトです。 式は、IQueryable
オブジェクトの順序付けられたバージョンを返します。 たとえば、Student
エンティティ型に対してリポジトリがインスタンス化されている場合、呼び出し元のメソッドのコードで orderBy
パラメーターに q => q.OrderBy(s => s.LastName)
を指定できます。
Get
メソッド内のコードは IQueryable
オブジェクトを作成し、フィルター式が存在する場合はそれを適用します:
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
次に、コンマ区切りリストを解析した後に、一括読み込み式を適用します:
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
最後に、orderBy
式がある場合はそれを適用し、結果を返します。それ以外の場合は、順序付けられていないクエリから結果を返します:
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
Get
メソッドを呼び出すときは、これらの関数のパラメーターを指定する代わりに、メソッドによって返される IEnumerable
コレクションに対してフィルター処理と並べ替えを行うことができます。 ただし、並べ替えとフィルター処理は、Web サーバー上のメモリ内で実行されます。 これらのパラメーターを使用すると、Web サーバーではなくデータベースによって作業が行われるようにすることができます。 別の方法は、特定のエンティティ型の派生クラスを作成し、特殊な Get
メソッド (例: GetStudentsInNameOrder
や GetStudentsByName
) を追加することです。 ただし、複雑なアプリケーションでは、このような派生クラスや特殊なメソッドが多数発生する可能性があり、これを維持するにはさらに多くの作業が必要になる可能性があります。
GetByID
、Insert
、Update
メソッド内のコードは、非ジェネリック リポジトリで見たものと似ています。 (Find
メソッドを使用して一括読み込みを行うことはできませんので、GetByID
シグネチャに一括読み込みパラメーターを指定していません。)
Delete
メソッドには、次の 2 つのオーバーロードが用意されています:
public virtual void Delete(object id)
{
TEntity entityToDelete = dbSet.Find(id);
dbSet.Remove(entityToDelete);
}
public virtual void Delete(TEntity entityToDelete)
{
if (context.Entry(entityToDelete).State == EntityState.Detached)
{
dbSet.Attach(entityToDelete);
}
dbSet.Remove(entityToDelete);
}
そのうちの 1 つは、削除するエンティティの ID のみを渡し、1 つはエンティティ インスタンスを受け取ります。 「コンカレンシーの処理」に関するチュートリアルで説明したように、コンカレンシー処理には、追跡プロパティの元の値を含むエンティティ インスタンスを受け取る Delete
メソッドが必要です。
この汎用リポジトリは、一般的な CRUD 要件を処理します。 特定のエンティティ型に、より複雑なフィルター処理や順序付けなどの特別な要件がある場合は、その型に追加のメソッドを持つ派生クラスを作成できます。
Unit of Work クラスの作成
Unit of Work クラスは、複数のリポジトリを使用するときに、1 つのデータベース コンテキストを共有するということが唯一の目的です。 そうすれば、Unit of Work が完了したら、コンテキストのそのインスタンスで SaveChanges
メソッドを呼び出し、関連するすべての変更が調整されることを保証できます。 クラスに必要なのは、各リポジトリの Save
メソッドとプロパティだけです。 各リポジトリ プロパティは、他のリポジトリ インスタンスと同じデータベース コンテキスト インスタンスを使用してインスタンス化されたリポジトリ インスタンスを返します。
[DAL] フォルダーで、UnitOfWork.cs という名前のクラス ファイルを作成し、テンプレート コードを次のコードに変更します:
using System;
using ContosoUniversity.Models;
namespace ContosoUniversity.DAL
{
public class UnitOfWork : IDisposable
{
private SchoolContext context = new SchoolContext();
private GenericRepository<Department> departmentRepository;
private GenericRepository<Course> courseRepository;
public GenericRepository<Department> DepartmentRepository
{
get
{
if (this.departmentRepository == null)
{
this.departmentRepository = new GenericRepository<Department>(context);
}
return departmentRepository;
}
}
public GenericRepository<Course> CourseRepository
{
get
{
if (this.courseRepository == null)
{
this.courseRepository = new GenericRepository<Course>(context);
}
return courseRepository;
}
}
public void Save()
{
context.SaveChanges();
}
private bool disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
context.Dispose();
}
}
this.disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}
このコードは、データベース コンテキストと各リポジトリのクラス変数を作成します。 context
変数の場合、新しいコンテキストがインスタンス化されます:
private SchoolContext context = new SchoolContext();
private GenericRepository<Department> departmentRepository;
private GenericRepository<Course> courseRepository;
各リポジトリ プロパティは、リポジトリが既に存在するかどうかを確認します。 そうでない場合は、コンテキスト インスタンスを渡してリポジトリをインスタンス化します。 その結果、すべてのリポジトリが同じコンテキスト インスタンスを共有します。
public GenericRepository<Department> DepartmentRepository
{
get
{
if (this.departmentRepository == null)
{
this.departmentRepository = new GenericRepository<Department>(context);
}
return departmentRepository;
}
}
Save
メソッドは、データベース コンテキストで SaveChanges
を呼び出します。
クラス変数でデータベース コンテキストをインスタンス化するクラスと同様に、UnitOfWork
クラスは IDisposable
を実装してコンテキストを破棄します。
UnitOfWork クラスとリポジトリを使用するようにコース コントローラーを変更する
CourseController.cs で現在使用しているコードを次のコードに置き換えます:
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using ContosoUniversity.Models;
using ContosoUniversity.DAL;
namespace ContosoUniversity.Controllers
{
public class CourseController : Controller
{
private UnitOfWork unitOfWork = new UnitOfWork();
//
// GET: /Course/
public ViewResult Index()
{
var courses = unitOfWork.CourseRepository.Get(includeProperties: "Department");
return View(courses.ToList());
}
//
// GET: /Course/Details/5
public ViewResult Details(int id)
{
Course course = unitOfWork.CourseRepository.GetByID(id);
return View(course);
}
//
// GET: /Course/Create
public ActionResult Create()
{
PopulateDepartmentsDropDownList();
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(
[Bind(Include = "CourseID,Title,Credits,DepartmentID")]
Course course)
{
try
{
if (ModelState.IsValid)
{
unitOfWork.CourseRepository.Insert(course);
unitOfWork.Save();
return RedirectToAction("Index");
}
}
catch (DataException /* dex */)
{
//Log the error (uncomment dex variable name after DataException and add a line here to write a log.)
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
public ActionResult Edit(int id)
{
Course course = unitOfWork.CourseRepository.GetByID(id);
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
[Bind(Include = "CourseID,Title,Credits,DepartmentID")]
Course course)
{
try
{
if (ModelState.IsValid)
{
unitOfWork.CourseRepository.Update(course);
unitOfWork.Save();
return RedirectToAction("Index");
}
}
catch (DataException /* dex */)
{
//Log the error (uncomment dex variable name after DataException and add a line here to write a log.)
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
private void PopulateDepartmentsDropDownList(object selectedDepartment = null)
{
var departmentsQuery = unitOfWork.DepartmentRepository.Get(
orderBy: q => q.OrderBy(d => d.Name));
ViewBag.DepartmentID = new SelectList(departmentsQuery, "DepartmentID", "Name", selectedDepartment);
}
//
// GET: /Course/Delete/5
public ActionResult Delete(int id)
{
Course course = unitOfWork.CourseRepository.GetByID(id);
return View(course);
}
//
// POST: /Course/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
Course course = unitOfWork.CourseRepository.GetByID(id);
unitOfWork.CourseRepository.Delete(id);
unitOfWork.Save();
return RedirectToAction("Index");
}
protected override void Dispose(bool disposing)
{
unitOfWork.Dispose();
base.Dispose(disposing);
}
}
}
このコードは、UnitOfWork
クラスのクラス変数を追加します。 (ここでインターフェイスを使用していた場合は、ここで変数を初期化しません。代わりに、Student
リポジトリの場合と同様に、2 つのコンストラクターのパターンを実装します。)
private UnitOfWork unitOfWork = new UnitOfWork();
クラスの残りの部分では、データベース コンテキストへのすべての参照が適切なリポジトリへの参照に置き換えられ、UnitOfWork
プロパティを使用してリポジトリにアクセスします。 Dispose
メソッドは、UnitOfWork
インスタンスを破棄します。
var courses = unitOfWork.CourseRepository.Get(includeProperties: "Department");
// ...
Course course = unitOfWork.CourseRepository.GetByID(id);
// ...
unitOfWork.CourseRepository.Insert(course);
unitOfWork.Save();
// ...
Course course = unitOfWork.CourseRepository.GetByID(id);
// ...
unitOfWork.CourseRepository.Update(course);
unitOfWork.Save();
// ...
var departmentsQuery = unitOfWork.DepartmentRepository.Get(
orderBy: q => q.OrderBy(d => d.Name));
// ...
Course course = unitOfWork.CourseRepository.GetByID(id);
// ...
unitOfWork.CourseRepository.Delete(id);
unitOfWork.Save();
// ...
unitOfWork.Dispose();
サイトを実行し、[コース] タブをクリックします。
ページの外観と動作は変更前と同じです。また、他のコース ページも同じように動作します。
まとめ
これで、リポジトリ パターンと Unit of Work パターンの両方が実装されました。 ジェネリック リポジトリのメソッド パラメーターとしてラムダ式を使用しました。 IQueryable
オブジェクトでこれらの式を使用する方法の詳細については、MSDN ライブラリの IQueryable(T) インターフェイス (System.Linq) を参照してください。 次のチュートリアルでは、いくつかの高度なシナリオを処理する方法について説明します。
他の Entity Framework リソースへのリンクは、ASP.NET データ アクセス コンテンツ マップに関するページにあります。