在這篇文章中,咱們試着來理解Repository(下文簡稱倉儲)和Unit of Work(下文簡稱工做單元)模式。同時咱們使用ASP.NET MVC和Entity Framework 搭建一個簡單的web應用來實現通用倉儲和工做單元模式。web
我記得在.NET 1.1的時代,咱們不得不花費大量的時間爲每一個應用程序編寫數據訪問代碼。即便代碼的性質幾乎相同,數據庫模式的差別使咱們爲每一個應用程序編寫單獨的數據訪問層。在新版本的.NET框架中,在咱們的應用程序中使用orm(對象-關係映射工具)使咱們避免像之前同樣編寫大量的數據訪問層的代碼成爲可能sql
因爲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基礎項目搭建!)通常是依次執行者以下三個命令便可,我說通常:ide
PM> Enable-migrations PM>add-migration createBook PM> update-database
能夠用Vs自帶的服務器資源管理器打開生成的數據庫查看錶信息。函數
如今咱們的準備工做已經完成,可使用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操做了
如今從代碼和功能的角度來看這樣作並無什麼錯。但這種方法有兩個問題。
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類裏面。
下面想象下以下場景,咱們數據庫中有多個表,那樣咱們須要爲每一個表建立一個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實現了簡單的倉儲模式和工做單元模式。而後咱們建立了一個通用的倉儲類和工做單元類來避免在一大堆倉儲類中編寫重複的代碼。我但願你在這篇文章中能有所收穫
07 May 2014: First version
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
轉載自:http://www.cnblogs.com/wit13142/p/5432147.html