MVC+EF 理解和實現倉儲模式和工作單元模式
文章介紹
在這篇文章中,我們試着來理解Repository(下文簡稱倉儲)和Unit of Work(下文簡稱工作單元)模式。同時我們使用ASP.NET MVC和Entity Framework 搭建一個簡單的web應用來實現通用倉儲和工作單元模式。
背景
我記得在.NET 1.1的時代,我們不得不花費大量的時間為每個應用程序編寫數據訪問代碼。即使代碼的性質幾乎相同,數據庫模式的差異使我們為每個應用程序編寫單獨的數據訪問層。在新版本的.NET框架中,在我們的應用程序中使用orm(對象-關系映射工具)使我們避免像以前一樣編寫大量的數據訪問層的代碼成為可能
由於orm的數據訪問操作變得那么簡單直接,導致數據訪問邏輯和邏輯謂詞(predicates)有可能散落在整個應用程序中。例如,每個控制器都有ObjectContext對象的實例,都可以進行數據訪問。
存儲模式和工作單位模式使通過ORM進行數據訪問操作更加干凈整潔,把所有的數據訪問幾種在一個位置,並且使程序維持可測試的能力。讓我們通過在一個簡單的MVC應用程序中實現倉儲模式和工作單元來代替枯燥的談論他們(“Talk is cheap,show me the code!)
創建代碼
首先使用vs創建一個MVC web應用程序,然后在Models中添加一個簡單的 Books類,我們將對這個類進行數據庫的CRUD操作。(原文使用的DB First方式搭建實例,鑒於我從開始正式接觸EF就沒有認真的進行DB First方式的學習,所以此處使用Code First方式來進行演示)
[Table("Books")] public class Book { [Key] public int Id { get; set; } [Column(TypeName = "varchar")] [MaxLength(100)] [Display(Name = "封面")] public string Cover { get; set; } [Column(TypeName = "nvarchar")] [MaxLength(200)] [Display(Name = "書名")] public string BookName { get; set; } [Column(TypeName = "nvarchar")] [MaxLength(200)] [Display(Name = "作者")] public string Author { get; set; } [Column(TypeName = "nvarchar")] [MaxLength(200)] [Display(Name = "譯名")] public string TranslatedName { get; set; } [Column(TypeName = "nvarchar")] [MaxLength(200)] [Display(Name = "譯者")] public string Translator { get; set; } [Column(TypeName = "nvarchar")] [MaxLength(200)] [Display(Name = "出版社")] public string Publisher { get; set; } [Display(Name = "字數")] public int WordCount { get; set; } [Display(Name = "頁數")] public int Pages { get; set; } [Column(TypeName = "varchar")] [MaxLength(50)] [Display(Name = "ISBN號")] public string ISBN { get; set; } [Column(TypeName = "float")] [Display(Name = "定價")] public double Price { get; set; } [Column(TypeName = "float")] [Display(Name = "售價")] public double SalePrice { get; set; } [Column(TypeName="date")] [Display(Name="出版日期")] public DateTime PublicationDate { get; set; } [Column(TypeName = "nvarchar")] [MaxLength(1000)] [Display(Name = "內容簡介")] [DataType(DataType.MultilineText)] public string Introduction { get; set; } [Column(TypeName = "nvarchar")] [MaxLength(1000)] [Display(Name = "作者簡介")] [DataType(DataType.MultilineText)] public string AboutTheAuthors { get; set; } [Column(TypeName = "varchar")] [MaxLength(100)] [Display(Name = "購買鏈接")] public string Link { get; set; }
然后就是在程序包管理器控制台中輸入數據遷移指令來實現數據表的創建(之前的步驟如果還不會的話,建議先去看下MVC基礎項目搭建!)一般是依次執行者如下三個命令即可,我說一般:
PM> Enable-migrations PM>add-migration createBook PM> update-database
可以用Vs自帶的服務器資源管理器打開生成的數據庫查看表信息。
使用MVC Scaffolding
現在我們的准備工作已經完成,可以使用Entity Framework來進行開發了,我們使用VS自帶的MVC模板創建一個Controller來完成Books 表的CRUD操作。
在解決方案中Controllers文件夾右鍵,選擇添加Controller,在窗口中選擇“包含視圖的MVC x控制器(使用Entity Framework)”

public class BooksController : Controller { private MyDbContext db = new MyDbContext(); // GET: Books public ActionResult Index() { return View(db.Books.ToList()); } // GET: Books/Details/5 public ActionResult Details(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Book book = db.Books.Find(id); if (book == null) { return HttpNotFound(); } return View(book); } // GET: Books/Create public ActionResult Create() { return View(); } // POST: Books/Create // 為了防止“過多發布”攻擊,請啟用要綁定到的特定屬性,有關 // 詳細信息,請參閱 http://go.microsoft.com/fwlink/?LinkId=317598。 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create([Bind(Include = "Id,Cover,BookName,Author,TranslatedName,Translator,Publisher,WordCount,Pages,ISBN,Price,Introduction,AboutTheAuthors,Link")] Book book) { if (ModelState.IsValid) { db.Books.Add(book); db.SaveChanges(); return RedirectToAction("Index"); } return View(book); } // GET: Books/Edit/5 public ActionResult Edit(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Book book = db.Books.Find(id); if (book == null) { return HttpNotFound(); } return View(book); } // POST: Books/Edit/5 // 為了防止“過多發布”攻擊,請啟用要綁定到的特定屬性,有關 // 詳細信息,請參閱 http://go.microsoft.com/fwlink/?LinkId=317598。 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit([Bind(Include = "Id,Cover,BookName,Author,TranslatedName,Translator,Publisher,WordCount,Pages,ISBN,Price,Introduction,AboutTheAuthors,Link")] Book book) { if (ModelState.IsValid) { db.Entry(book).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); } return View(book); } // GET: Books/Delete/5 public ActionResult Delete(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Book book = db.Books.Find(id); if (book == null) { return HttpNotFound(); } return View(book); } // POST: Books/Delete/5 [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public ActionResult DeleteConfirmed(int id) { Book book = db.Books.Find(id); db.Books.Remove(book); db.SaveChanges(); return RedirectToAction("Index"); } protected override void Dispose(bool disposing) { if (disposing) { db.Dispose(); } base.Dispose(disposing); } }
F5啟動調試,我們應該是已經可以對Books進行CRUD操作了
現在從代碼和功能的角度來看這樣做並沒有什么錯。但這種方法有兩個問題。
- 數據方位的代碼零散分布在應用程序中(Controllers),這將是后期程序維護的噩夢
- 在控制器(Controller)和動作(Action)內部創建了數據上下文(Context),這使得功能無法通過偽數據進行測試,我們無法驗證其結果,除非我們使用測試數據。(應該就是說功能不可測試)
Note:如果第二點感覺不清晰,那推薦閱讀關於在MVC中進行測試驅動開發(Test Driven Development using MVC)方面的內容。為防止離題,不再本文中進行討論。
實現倉儲模式
現在,我們來解決上面的問題。我們可以通過把所有包含數據訪問邏輯的代碼放到一起來解決這個問題。所以讓我們定義一個包含所有對 Books 表的數據訪問邏輯的類
但是在創建這個類之前,我們也順便考慮下第二個問題。如果我們創建一個簡單的定義了訪問Books表的約定的接口然后用剛才提到的類實現接口,我們會得到一個好處,我們可以使用另一個類偽造數據來實現接口。這樣,就可以保持Controller是可測試的。(原文很麻煩,就是表達這個意思)
所以,我們先定義對 Books 進行數據訪問的約定。
public interface IRepository<T> where T:class { IEnumerable<T> GetAll(Func<T, bool> predicate = null); T Get(Func<T, bool> predicate); void Add(T entity); void Update(T entity); void Delete(T entity); }
下面的類包含了對 Books 表CRUD操作接口的實現
public class BooksRepository:IRepository<Book> { private MyDbContext dbContext = new MyDbContext(); public IEnumerable<Book> GetAll(Func<Book, bool> predicate = null) { if(predicate!=null) { return dbContext.Books.Where(predicate); } return dbContext.Books; } public Book Get(Func<Book, bool> predicate) { return dbContext.Books.First(predicate); } public void Add(Book entity) { dbContext.Books.Add(entity); } public void Update(Book entity) { dbContext.Entry(entity).State = EntityState.Modified; } public void Delete(Book entity) { dbContext.Books.Remove(entity); } internal void SaveChanges() { dbContext.SaveChanges(); } }
現在,我們創建另一個包含對 Books 表進行CRUD操作的Controller,命名為BooksRepoController
public class BooksRepoController : Controller { private BooksRepository repo = new BooksRepository(); // GET: Books1 public ActionResult Index() { return View(repo.GetAll().ToList()); } // GET: Books1/Details/5 public ActionResult Details(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Book book = repo.Get(t=>t.Id==id.Value); if (book == null) { return HttpNotFound(); } return View(book); } // GET: Books1/Create public ActionResult Create() { return View(); } // POST: Books1/Create // 為了防止“過多發布”攻擊,請啟用要綁定到的特定屬性,有關 // 詳細信息,請參閱 http://go.microsoft.com/fwlink/?LinkId=317598。 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create([Bind(Include = "Id,Cover,BookName,Author,TranslatedName,Translator,Publisher,WordCount,Pages,ISBN,Price,Introduction,AboutTheAuthors,Link")] Book book) { if (ModelState.IsValid) { repo.Add(book); repo.SaveChanges(); return RedirectToAction("Index"); } return View(book); } // GET: Books1/Edit/5 public ActionResult Edit(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Book book = repo.Get(t => t.Id == id); if (book == null) { return HttpNotFound(); } return View(book); } // POST: Books1/Edit/5 // 為了防止“過多發布”攻擊,請啟用要綁定到的特定屬性,有關 // 詳細信息,請參閱 http://go.microsoft.com/fwlink/?LinkId=317598。 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit([Bind(Include = "Id,Cover,BookName,Author,TranslatedName,Translator,Publisher,WordCount,Pages,ISBN,Price,Introduction,AboutTheAuthors,Link")] Book book) { if (ModelState.IsValid) { repo.Update(book); repo.SaveChanges(); return RedirectToAction("Index"); } return View(book); } // GET: Books1/Delete/5 public ActionResult Delete(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Book book = repo.Get(t => t.Id == id); if (book == null) { return HttpNotFound(); } return View(book); } // POST: Books1/Delete/5 [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public ActionResult DeleteConfirmed(int id) { Book book = repo.Get(t => t.Id == id); repo.Delete(book); repo.SaveChanges(); return RedirectToAction("Index"); } }
現在這種方法的好處是,我的ORM的數據訪問代碼不是分散在控制器。它被包裝在一個Repository類里面。
如何處理多個Repository庫?
下面想象下如下場景,我們數據庫中有多個表,那樣我們需要為每個表創建一個Reporsitory類。(好多重復工作的說,其實這不是問題)
問題是關於 數據上下文(DbContext) 對象的。如果我們創建多個Repository類,是不是每一個都單獨的包含一個 數據上下文對象?我們知道同時使用多個 數據上下文 會存在問題,那我們該怎么處理每個Repository都擁有自己的數據上下文 對象的問題?
來解決這個問題吧。為什么每個Repository要擁有一個數據上下文的實例呢?為什么不在一些地方創建一個它的實例,然后在repository被實例化的時候作為參數傳遞進去呢。現在這個新的類被命名為 UnitOfWork ,此類將負責創建數據上下文實例並移交到控制器的所有repository實例。
實現工作單元
所以,我們在單獨創建一個使用 UnitOfWork 的Repository類,數據上下文對象將從外面傳遞給它因此,讓我們創建一個單獨的存儲庫將使用通過UnitOfWork類和對象上下文將被傳遞到此類以外。
public class BooksRepositoryWithUow : IRepository<Book> { private MyDbContext dbContext = null; public BooksRepositoryWithUow(MyDbContext _dbContext) { dbContext = _dbContext; } public IEnumerable<Book> GetAll(Func<Book, bool> predicate = null) { if (predicate != null) { return dbContext.Books.Where(predicate); } return dbContext.Books; } public Book Get(Func<Book, bool> predicate) { return dbContext.Books.FirstOrDefault(predicate); } public void Add(Book entity) { dbContext.Books.Add(entity); } public void Update(Book entity) { dbContext.Entry(entity).State = EntityState.Modified; } public void Delete(Book entity) { dbContext.Books.Remove(entity); } }
現在這個Repository類將從類的外面得到DbContext對象(每當它被創建時).
現在,假如我們創建多個倉儲類,我們在倉儲類實例化的時候得到 ObjectContext 對象。讓我們來看下 UnitOfWork 如何創建倉儲類並且傳遞到Controller中的。
public class UnitOfWork : IDisposable { private MyDbContext dbContext = null; public UnitOfWork() { dbContext = new MyDbContext(); } IRepository<Book> bookReporsitory = null; public IRepository<Book> BookRepository { get { if (bookReporsitory == null) { bookReporsitory = new BooksRepositoryWithUow(dbContext); } return bookReporsitory; } } public void SaveChanges() { dbContext.SaveChanges(); } private bool disposed = false; protected virtual void Dispose(bool disposing) { if (!this.disposed) { if (disposing) { dbContext.Dispose(); } this.disposed = true; } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } }
現在我們在創建一個Controller,命名為 BooksUowController 將通過調用 工作單元類來實現 Book 表的CRUD操作
public class BooksUowController : Controller { private UnitOfWork uow = null; //private MyDbContext db = new MyDbContext(); public BooksUowController() { uow = new UnitOfWork(); } public BooksUowController(UnitOfWork _uow) { this.uow = _uow; } // GET: BookUow public ActionResult Index() { return View(uow.BookRepository.GetAll().ToList()); } // GET: BookUow/Details/5 public ActionResult Details(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Book book = uow.BookRepository.Get(b => b.Id == id.Value); if (book == null) { return HttpNotFound(); } return View(book); } // GET: BookUow/Create public ActionResult Create() { return View(); } // POST: BookUow/Create // 為了防止“過多發布”攻擊,請啟用要綁定到的特定屬性,有關 // 詳細信息,請參閱 http://go.microsoft.com/fwlink/?LinkId=317598。 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create([Bind(Include = "Id,Cover,BookName,Author,TranslatedName,Translator,Publisher,WordCount,Pages,ISBN,Price,Introduction,AboutTheAuthors,Link")] Book book) { if (ModelState.IsValid) { uow.BookRepository.Add(book); uow.SaveChanges(); return RedirectToAction("Index"); } return View(book); } // GET: BookUow/Edit/5 public ActionResult Edit(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Book book = uow.BookRepository.Get(b => b.Id == id.Value); if (book == null) { return HttpNotFound(); } return View(book); } // POST: BookUow/Edit/5 // 為了防止“過多發布”攻擊,請啟用要綁定到的特定屬性,有關 // 詳細信息,請參閱 http://go.microsoft.com/fwlink/?LinkId=317598。 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit([Bind(Include = "Id,Cover,BookName,Author,TranslatedName,Translator,Publisher,WordCount,Pages,ISBN,Price,Introduction,AboutTheAuthors,Link")] Book book) { if (ModelState.IsValid) { uow.BookRepository.Update(book); uow.SaveChanges(); return RedirectToAction("Index"); } return View(book); } // GET: BookUow/Delete/5 public ActionResult Delete(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Book book = uow.BookRepository.Get(b => b.Id == id.Value); if (book == null) { return HttpNotFound(); } return View(book); } // POST: BookUow/Delete/5 [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public ActionResult DeleteConfirmed(int id) { Book book = uow.BookRepository.Get(b => b.Id == id); uow.BookRepository.Delete(book); uow.SaveChanges(); return RedirectToAction("Index"); } }
現在,Controller通過默認的構造函數實現了可測試能力。例如,測試項目可以為 UnitOfWork 傳入虛擬的測試數據來代替真實數據。同樣數據訪問的代碼也被集中到一個地方。
通用倉儲和工作單元
現在我們已經創建了倉儲類和 工作單元類。現在的問題是如果數據庫包含很多表,那樣我們需要創建很多倉儲類,然后我們的工作單元類需要為每個倉儲類創建一個訪問屬性
如果為所有的Mode類創建一個通用的倉儲類和 工作單元類豈不是更好,所以我們繼續來實現一個通用的倉儲類。
public class GenericRepository<T> : IRepository<T> where T : class { private MyDbContext dbContext = null; IDbSet<T> _objectSet; public GenericRepository(MyDbContext _dbContext) { dbContext = _dbContext; _objectSet = dbContext.Set<T>(); } public IEnumerable<T> GetAll(Expression< Func<T, bool>> predicate = null) { if (predicate != null) { return _objectSet.Where(predicate); } return _objectSet.AsEnumerable(); } public T Get(Expression<Func<T, bool>> predicate) { return _objectSet.First(predicate); } public void Add(T entity) { _objectSet.Add(entity); } public void Update(T entity) { _objectSet.Attach(entity); } public void Delete(T entity) { _objectSet.Remove(entity); } public IEnumerable<T> GetAll(Func<T, bool> predicate = null) { if (predicate != null) { return _objectSet.Where(predicate); } return _objectSet.AsEnumerable(); } public T Get(Func<T, bool> predicate) { return _objectSet.First(predicate); } }
UPDATE: 發現一個很有用的評論,我認為應該放在文章中分享一下
在.NET中,對‘Where’至少有兩個重寫方法:
public static IQueryable Where(this IQueryable source, Expression> predicate); public static IEnumerable Where(this IEnumerable source, Func predicate);
現在我們正在使用的是
Func<T, bool>
現在的查詢將會使用'IEnumerable'版本,在示例中,首先從數據庫中取出整個表的記錄,然后再執行過濾條件取得最終的結果。想要證明這一點,只要去看看生成的sql語句,它是不包含Where字句的。
若要解決這個問題,我們需要修改'Func' to 'Expression Func'.
Expression<Func<T, bool>> predicate
現在 'Where'方法使用的就是 'IQueryable'版本了。
Note: 因此看來,使用 Expression Func 比起使用 Func是更好的主意.
現在使用通用的倉儲類,我們需要創建一個對應的工作單元類。這個工作單元類將檢查倉儲類是否已經創建,如果存在將返回一個實例,否則將創建一個新的實例。
public class GenericUnitOfWork:IDisposable { private MyDbContext dbContext=null; public GenericUnitOfWork() { dbContext = new MyDbContext(); } public Dictionary<Type, object> repositories = new Dictionary<Type, object>(); public IRepository<T> Repository<T>() where T : class { if (repositories.Keys.Contains(typeof(T)) == true) { return repositories[typeof(T)] as IRepository<T>; } IRepository<T> repo=new GenericRepository<T>(dbContext); repositories.Add(typeof(T), repo); return repo; } public void SaveChanges() { dbContext.SaveChanges(); } private bool disposed = false; protected virtual void Dispose(bool disposing) { if (!this.disposed) { if (disposing) { dbContext.Dispose(); } } this.disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } }
然后,我們在創建一個使用通用工作單元類 GenericUnitOfWork 的Controller,命名為GenericContactsController ,完成對 Book 表的CRUD操作。
public class GenericBooksController : Controller { private GenericUnitOfWork uow = null; //private MyDbContext db = new MyDbContext(); public GenericBooksController() { uow = new GenericUnitOfWork(); } public GenericBooksController(GenericUnitOfWork uow) { this.uow = uow; } // GET: GenericBooks public ActionResult Index() { return View(uow.Repository<Book>().GetAll().ToList()); } // GET: GenericBooks/Details/5 public ActionResult Details(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Book book = uow.Repository<Book>().Get(b=>b.Id==id.Value); if (book == null) { return HttpNotFound(); } return View(book); } // GET: GenericBooks/Create public ActionResult Create() { return View(); } // POST: GenericBooks/Create // 為了防止“過多發布”攻擊,請啟用要綁定到的特定屬性,有關 // 詳細信息,請參閱 http://go.microsoft.com/fwlink/?LinkId=317598。 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create([Bind(Include = "Id,Cover,BookName,Author,TranslatedName,Translator,Publisher,WordCount,Pages,ISBN,Price,Introduction,AboutTheAuthors,Link")] Book book) { if (ModelState.IsValid) { uow.Repository<Book>().Add(book); uow.SaveChanges(); return RedirectToAction("Index"); } return View(book); } // GET: GenericBooks/Edit/5 public ActionResult Edit(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Book book = uow.Repository<Book>().Get(b => b.Id == id.Value); if (book == null) { return HttpNotFound(); } return View(book); } // POST: GenericBooks/Edit/5 // 為了防止“過多發布”攻擊,請啟用要綁定到的特定屬性,有關 // 詳細信息,請參閱 http://go.microsoft.com/fwlink/?LinkId=317598。 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit([Bind(Include = "Id,Cover,BookName,Author,TranslatedName,Translator,Publisher,WordCount,Pages,ISBN,Price,Introduction,AboutTheAuthors,Link")] Book book) { if (ModelState.IsValid) { uow.Repository<Book>().Update(book); uow.SaveChanges(); return RedirectToAction("Index"); } return View(book); } // GET: GenericBooks/Delete/5 public ActionResult Delete(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Book book = uow.Repository<Book>().Get(b => b.Id == id.Value); if (book == null) { return HttpNotFound(); } return View(book); } // POST: GenericBooks/Delete/5 [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public ActionResult DeleteConfirmed(int id) { Book book = uow.Repository<Book>().Get(b => b.Id == id); uow.Repository<Book>().Delete(book); uow.SaveChanges(); return RedirectToAction("Index"); } }
現在,我們已經在解決方案中現實了一個通用的倉儲類和工作單元類
要點總結
在這篇文章中,我們理解了倉儲模式和工作單元模式。我們也在ASP.NET MVC應用中使用Entity Framework實現了簡單的倉儲模式和工作單元模式。然后我們創建了一個通用的倉儲類和工作單元類來避免在一大堆倉儲類中編寫重復的代碼。我希望你在這篇文章中能有所收獲
History
07 May 2014: First version
License
This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)
譯注
原文采用objectContext,使用EF圖形化建模編寫的示例代碼,譯者修改code first形式
參考
https://msdn.microsoft.com/en-us/data/jj592676.aspx
https://msdn.microsoft.com/en-us/library/system.data.entity.dbset(v=vs.113).aspx
