MVC+EF 理解和实现仓储模式和工作单元模式
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