ASP.NET MVC アプリケーションでのリポジトリと作業単位のパターンの実装 (9/10)
Contoso University サンプル Web アプリケーションでは、Entity Framework 5 Code First と Visual Studio 2012 を使用して、ASP.NET MVC 4 アプリケーションを作成する方法を示します。 チュートリアル シリーズについては、シリーズの最初のチュートリアルをご覧ください。
Note
解決できない問題が発生した場合は、 完了した章をダウンロード して、問題を再現してみてください。 一般に、コードを完成したコードと比較することで、問題の解決策を見つけることができます。 一般的なエラーとその解決方法については、「エラーと回避策」を参照してください。
前のチュートリアルでは、継承を使用して、 および Instructor
エンティティ クラスの冗長なコードをStudent
減らしました。 このチュートリアルでは、CRUD 操作にリポジトリと作業単位パターンを使用するいくつかの方法について説明します。 前のチュートリアルと同様に、このチュートリアルでは、新しいページを作成するのではなく、既に作成したページでのコードの動作方法を変更します。
リポジトリと作業単位のパターン
リポジトリと作業単位パターンは、データ アクセス層とアプリケーションのビジネス ロジック層の間に抽象化レイヤーを作成することを目的としています。 これらのパターンを実装すると、データ ストアの変更からアプリケーションを隔離でき、自動化された単体テストやテスト駆動開発 (TDD) を円滑化できます。
このチュートリアルでは、エンティティの種類ごとにリポジトリ クラスを実装します。 エンティティの Student
種類については、リポジトリ インターフェイスとリポジトリ クラスを作成します。 コントローラーでリポジトリをインスタンス化するときに、 インターフェイスを使用して、コントローラーがリポジトリ インターフェイスを実装するオブジェクトへの参照を受け入れるようにします。 コントローラーが Web サーバーで実行されると、Entity Framework で動作するリポジトリを受け取ります。 コントローラーが単体テスト クラスで実行されると、メモリ内コレクションなどのテスト用に簡単に操作できる方法で格納されたデータと連携するリポジトリを受け取ります。
チュートリアルの後半では、コントローラーの と エンティティ型Course
に対Course
して、複数のリポジトリと Department
1 つの作業単位を使用します。 作業単位クラスは、それらすべてによって共有される単一のデータベース コンテキスト クラスを作成することによって、複数のリポジトリの作業を調整します。 自動単体テストを実行できるようにする場合は、リポジトリの場合と同じ方法で、これらのクラスのインターフェイスを Student
作成して使用します。 ただし、チュートリアルをシンプルにするために、インターフェイスなしでこれらのクラスを作成して使用します。
次の図は、リポジトリまたは作業単位パターンをまったく使用しない場合と比較して、コントローラーとコンテキスト クラスの間のリレーションシップを概念化する 1 つの方法を示しています。
このチュートリアル シリーズでは、単体テストを作成しません。 リポジトリ パターンを使用する MVC アプリケーションでの TDD の概要については、「 チュートリアル: ASP.NET MVC での TDD の使用」を参照してください。 リポジトリ パターンの詳細については、次のリソースを参照してください。
- MSDN のリポジトリ パターン。
- Julie Lerman のブログに投稿されたアジャイル Entity Framework 4 リポジトリ シリーズ。
- Dan Wahlin のブログで、アカウントを一目で確認する HTML5/jQuery アプリケーションを作成します。
Note
リポジトリと作業単位パターンを実装するには、多くの方法があります。 リポジトリ クラスは、作業単位クラスの有無にかかわらず使用できます。 すべてのエンティティ型に対して 1 つのリポジトリを実装することも、型ごとに 1 つ実装することもできます。 型ごとに 1 つを実装する場合は、個別のクラス、ジェネリック基底クラスと派生クラス、または抽象基底クラスと派生クラスを使用できます。 リポジトリにビジネス ロジックを含めたり、データ アクセス ロジックに制限したりできます。 エンティティ セットの DbSet 型ではなく、そこで IDbSet インターフェイスを使用して、抽象化レイヤーをデータベース コンテキスト クラスに構築することもできます。 このチュートリアルで示す抽象化レイヤーを実装する方法は、すべてのシナリオと環境の推奨事項ではなく、考慮すべき 1 つのオプションです。
Student Repository クラスの作成
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
使用し、作業クラスのユニットですべてのリポジトリが同じコンテキストを確実に使用できるようにする方法を確認します。
リポジトリは IDisposable を実装し、コントローラーで前に確認したようにデータベース コンテキストを破棄します。CRUD メソッドは、前に見たのと同じ方法でデータベース コンテキストを呼び出します。
リポジトリを使用するように学生コントローラーを変更する
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 ページも同じように動作します。 ただし、コントローラーの メソッドがフィルター処理と順序付けを行う方法 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
メソッドを使用してコレクションに変換されるまでデータベースに送信されません。これは、インデックス ビューが学生モデルにアクセスするまで発生しません。 上記の元のコードの メソッドは Where
、 WHERE
データベースに送信される SQL クエリの 句になります。 つまり、選択したエンティティのみがデータベースから返されます。 ただし、 を にstudentRepository.GetStudents()
変更context.Students
した結果、このステートメントの後のstudents
変数はIEnumerable
、データベース内のすべての学生を含むコレクションです。 メソッドを適用した Where
最終的な結果は同じですが、データベースではなく、Web サーバー上のメモリで作業が行われます。 大量のデータを返すクエリの場合、これは非効率的な場合があります。
ヒント
IQueryable/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'
このクエリは、リポジトリが検索条件を知らずにクエリを実行したため、すべての学生データを返します。 並べ替え、検索条件の適用、ページング用のデータのサブセットの選択 (この場合は 3 行のみ表示) のプロセスは、後で コレクションで メソッドが呼び出IEnumerable
されたときにToPagedList
メモリ内で実行されます。
以前のバージョンのコード (リポジトリを実装する前) では、検索条件を適用するまで、オブジェクトで が呼び出されるまで ToPagedList
、クエリはデータベースに IQueryable
送信されません。
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
する単体テスト プロジェクトに別のリポジトリ クラスを作成できます。このモック リポジトリ クラスは、コンテキストを呼び出してデータの読み取りと書き込みを行う代わりに、コントローラー関数をテストするためにメモリ内コレクションを操作できます。
汎用リポジトリと作業単位クラスを実装する
エンティティの種類ごとにリポジトリ クラスを作成すると、多くの冗長なコードが生成され、部分的な更新が発生する可能性があります。 たとえば、同じトランザクションの一部として 2 つの異なるエンティティ型を更新する必要があるとします。 それぞれが個別のデータベース コンテキスト インスタンスを使用している場合、1 つは成功し、もう 1 つは失敗する可能性があります。 冗長なコードを最小限に抑える 1 つの方法は、汎用リポジトリを使用することです。また、すべてのリポジトリが同じデータベース コンテキストを使用するようにする (したがって、すべての更新を調整する) 方法の 1 つは、作業単位クラスを使用することです。
チュートリアルのこのセクションでは、クラスとクラスをGenericRepository
作成し、コントローラーでそれらをCourse
使用して、 と エンティティ セットの両方Department
にCourse
アクセスUnitOfWork
します。 前に説明したように、チュートリアルのこの部分をシンプルにするために、これらのクラスのインターフェイスは作成しません。 ただし、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
は、呼び出し元がラムダ式を提供することを意味します。 ただし、この場合、式への入力は 型のIQueryable
TEntity
オブジェクトです。 式は、そのオブジェクトの順序付きバージョンを 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
メソッドを追加しますGetStudentsByName
。GetStudentsInNameOrder
ただし、複雑なアプリケーションでは、このような派生クラスや特殊化されたメソッドが多数発生する可能性があり、管理が必要な作業が増える可能性があります。
、Insert
、および メソッドのGetByID
コードは、非ジェネリック リポジトリで見たものとUpdate
似ています。 (メソッドを使用して一括読み込みを実行できないため、シグネチャに GetByID
一括読み込みパラメーターを Find
指定していません)。
メソッドには、次の 2 つのオーバーロードが Delete
用意されています。
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 要件を処理します。 特定のエンティティ型に、より複雑なフィルター処理や順序付けなどの特別な要件がある場合は、その型に追加のメソッドを持つ派生クラスを作成できます。
作業単位クラスの作成
作業単位クラスは、複数のリポジトリを使用するときに、1 つのデータベース コンテキストを共有することを確認するという 1 つの目的を果たします。 このようにして、作業単位が完了したら、コンテキストのそのインスタンスで メソッドを 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
追加します。 (ここでインターフェイスを使用していた場合は、ここで変数を初期化しません。代わりに、リポジトリの場合と同じように 2 つのコンストラクターのパターンを Student
実装します)。
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();
サイトを実行し、[コース] タブ を クリックします。
ページの外観と動作は変更前と同じで、他のコース ページも同じように機能します。
まとめ
これで、リポジトリと作業単位の両方のパターンが実装されました。 ジェネリック リポジトリでメソッド パラメーターとしてラムダ式を使用しました。 オブジェクトで IQueryable
これらの式を使用する方法の詳細については、MSDN ライブラリの IQueryable(T) インターフェイス (System.Linq) を参照してください。 次のチュートリアルでは、いくつかの高度なシナリオを処理する方法について説明します。
他の Entity Framework リソースへのリンクは、 ASP.NET データ アクセス コンテンツ マップにあります。