在这篇文章中,咱们试着来理解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