By Tom Dykstra, Tom Dykstra is a Senior Programming Writer on Microsoft's Web Platform & Tools Content Team.
原文地址:http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application
全文目錄:Contoso 大學 - 使用 EF Code First 創建 MVC 應用
在上一次的教程中,你已經使用繼承來消除在 Student 和 Instructor 實體之間的重復代碼。在這個教程中,你將要看到使用倉儲和工作單元模式進行增、刪、改、查的一些方法。像前面的教程一樣,你將要修改已經創建的頁面中代碼的工作方式,而不是新創建的頁面。
9-1 倉儲和工作單元模式
倉儲和工作單元模式用來在數據訪問層和業務邏輯層之間創建抽象層。實現這些模式有助於隔離數據存儲的變化,便於自動化的單元測試或者測試驅動的開發 ( TDD )。
在這個教程中,你將要為每個實體類型實現一個倉儲類。對於 Student 實體來說,你需要創建一個倉儲接口和一個倉儲類。當在控制器中實例化倉儲對象的時候。你將會通過接口來使用它,當控制器在 Web 服務器上運行的時候,控制器將會接受任何實現倉儲接口的對象引用。通過接收倉儲對象進行數據的存儲管理,使得你可以容易地控制測試,就像使用內存中的集合一樣。
在教程的最后,你將要在 Course 控制器中對 Course 和 Department 實體使用多個倉儲和一個工作單元類。工作單元類則通過創建一個所有倉儲共享的數據庫上下文對象,來組織多個倉儲對象。如果你希望執行自動化的單元測試,你也應該對 Student類通過相同的方式創建和使用接口。不管怎樣,為了保持教程的簡單,你將不會通過接口創建和使用這些類。
下面的截圖展示了在控制器和上下文之間的概念圖,用來比較與不使用倉儲或工作單元模式的區別。
在這個教程中不會創建單元測試,在 MVC 應用中使用倉儲模式進行 TDD 的相關信息,可以查看 MSDN 網站中的 Walkthrough: Using TDD with ASP.NET MVC ,EF 團隊博客中的 Using Repository and Unit of Work patterns with Entity Framework 4.0 ,以及 Julie Lerman 的博客 Agile Entity Framework 4 Repository 系列。
注意:有多種方式可以實現倉儲和工作單元模式。配合工作單元類可以使用也可以不使用倉儲類。可以對所有的實體類型實現一個簡單的倉儲,或者每種類型一個。如果為每種類型實現一個倉儲,還可以通過分離的類,或者泛型的基類然后派生,或者抽象基類然后派生。可以將業務邏輯包含在倉儲中,或者限制只有數據訪問邏輯。也可以通過在實體中使用 IDbSet 接口代替 DbSet 類為數據庫上下文類創建一個抽象層。在這個教程中展示的目標實現了抽象層,只是其中一種考慮,並不是針對所有的場景和環境都適用。
9-2 創建 Student 倉儲類
在 DAL 文件夾中,創建一個文件名為 IStudentRepository.cs 的文件,將當前的代碼使用如下代碼替換。
using System; using System.Collections.Generic; using System.Linq; using System.Web; 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(); } }
代碼定義了一套典型的增、刪、改、查方法。包括兩個讀取方法 – 一個返回所有的學生實體,一個通過 ID 查詢單個實體。
在 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; }
你需要創建一個新的數據庫上下文實例,但是如果在控制器中需要使用多個倉儲類,每一個會得到一個不同的數據庫上下文對象。后面在 Course 控制器中,你將要使用多個倉儲,會看到如何使用工作單元類來保證所有的倉儲使用相同的數據庫上下文對象。
倉儲類還實現了 IDisposable 接口,如同在前面控制器中所見,釋放數據庫上下文,倉儲的增刪改查方法也如前所見調用數據庫上下文的方法。
9-3 修改 Student 控制器使用倉儲
在 StudentController.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; 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 (Request.HttpMethod == "GET") { searchString = currentFilter; } else { page = 1; } 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: 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] public ActionResult Create(Student student) { try { if (ModelState.IsValid) { studentRepository.InsertStudent(student); studentRepository.Save(); return RedirectToAction("Index"); } } catch (DataException) { //Log the error (add a variable name after DataException) ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see 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] public ActionResult Edit(Student student) { try { if (ModelState.IsValid) { studentRepository.UpdateStudent(student); studentRepository.Save(); return RedirectToAction("Index"); } } catch (DataException) { //Log the error (add a variable name after DataException) ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator."); } return View(student); } // // GET: /Student/Delete/5 public ActionResult Delete(int id, bool? saveChangesError) { if (saveChangesError.GetValueOrDefault()) { ViewBag.ErrorMessage = "Unable to save changes. Try again, and if the problem persists see your system administrator."; } Student student = studentRepository.GetStudentByID(id); return View(student); } // // POST: /Student/Delete/5 [HttpPost, ActionName("Delete")] public ActionResult DeleteConfirmed(int id) { try { Student student = studentRepository.GetStudentByID(id); studentRepository.DeleteStudent(id); studentRepository.Save(); } catch (DataException) { //Log the error (add a variable name after DataException) return RedirectToAction("Delete", new System.Web.Routing.RouteValueDictionary { { "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();
運行程序,點擊 Students 窗格。
現在的頁面顯示與使用倉儲之前完全相同。其他的學生頁面也一樣。實際上,在 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())); }
在原來版本的代碼中,students 變量的類型是 IQueryable ,查詢在使用諸如 ToList 方法轉換為集合之前並不會發送到數據庫中。這意味着這里的 Where 方法在處理到數據庫中的時候變成 SQL 中的 where 子句。同時意味着僅僅選中的實體從數據庫中返回。從 context.Students 修改為 studentRepository.GetStudents() 之后,代碼中的 students 變量類型成為 IEnumerable 集合,包括數據庫中所有的學生。通過 Where 方法得到的結果是一樣的,但是處理在 Web 服務器的內存中進行,而不是在數據庫中。對於大量的數據來說,這樣做是低效的。后繼的段落展示如何通過倉儲方法實現在數據庫中完成。
現在你已經在控制器和 EF 數據庫上下文之間創建了抽象層。如果你將在這個程序中執行自動化的單元測試,可以在單元測試項目中創建一個替代的實現接口 IStudentRepository 倉儲類,來代替實際的上下文完成讀寫數據。這個模擬 ( Mock ) 的倉儲類可以通過操作內存中的集合來測試控制器功能。
9-4 實現泛型的倉儲和工作單元
對每一個實體類型創建一個倉儲將會導致大量重復代碼。還會帶來部分更新的問題。例如,假設在一個事務中更新兩個不同的實體。 如果每一個倉儲使用不同的數據庫上下文實例,一個可能成功了,另外一個失敗了。一種減少冗余代碼的方式是使用泛型倉儲,另一種方式是使用工作單元類來確保所有的倉儲都使用同樣的數據庫上下文 ( 來協調所有的更新 )。
在這一節中,你將要創建 GenericRepository 類和 UnitOfWork 類。在 Course 控制器中使用它們來訪問 Department 和 Course 實體集。如前所述,為了保持教程的簡單,不為這些類創建接口,但是為了以后使用它們進行 TDD 的便利,你應該像在 Student 倉儲中一樣通過接口實現。
9-4-1 創建泛型倉儲
在 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(); }
Get 方法接受 Lambda 表達式,允許調用代碼通過 Lambda 表達式來傳遞過濾條件和排序列,字符串參數允許調用者傳遞一個逗號分隔的導航屬性進行預先加載。
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 類型的 Lambda 表達式,表達式將會返回 bool 類型的值。例如,如果倉儲實例化為 Student 類型,調用的方法可能為 filter 傳遞的參數為 student => student.LastName == "Smith"
代碼 Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy 也表示調用方需要提供一個 Lambda 表達式,在這里,表達式是 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 中的方法如同在非泛型方法中一樣簡單 ( 在 GetByID 方法扎沒有提供預先加載參數,因為不能對 Find 方法進行預先加載 )。
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); }
一個允許僅僅傳遞實體的 ID 進行刪除,另外一個使用實體實例。像在處理並發中所見,對於並發處理你需要 Delete 方法獲取包含追蹤屬性原始值的實體實例。
泛型倉儲可以處理典型的 CRUD 需求。當特定的實體有特定的需求時,例如更加復雜的過濾或者排序,可以通過創建派生類來增加額外的方法。
9-4-2 創建工作單元類
工作單元類服務於一個目的:當你使用多個倉儲的時候,共享單個的數據庫上下文實例。因此,當工作單元完成的時候,你可以通過在這個數據庫上下文實例上調用 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; } }
像在類中實例化數據庫上下文的其他類一樣, UnitOfWork 類也實現了 IDisposable 接口來釋放數據庫上下文。
9-4-3 修改 CourseController 使用工作單元類和倉儲
使用如下代碼替換當前的 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] public ActionResult Create(Course course) { try { if (ModelState.IsValid) { unitOfWork.CourseRepository.Insert(course); unitOfWork.Save(); return RedirectToAction("Index"); } } catch (DataException) { //Log the error (add a variable name after DataException) 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] public ActionResult Edit(Course course) { try { if (ModelState.IsValid) { unitOfWork.CourseRepository.Update(course); unitOfWork.Save(); return RedirectToAction("Index"); } } catch (DataException) { //Log the error (add a variable name after DataException) 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")] 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 倉儲的兩個構造函數 )
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();
運行程序,點擊 Courses 窗格。
頁面工作如同既往修改之前一樣,Course 頁面也同樣工作。
你現在已經實現了倉儲和工作單元模式。在泛型倉儲中使用 Lambda 表達式作為參數。更多對 IQueryable 對象使用表達式的信息,可以參閱 MSDN 庫中的 IQueryable(T) Interface (System.Linq) 。下一次,將學習如何處理一些高級場景。