利用Repository and Unit of Work重构项目
项目最基础的东西已经结束了,但是现在我们的项目还不健全 不利于测试 重复性代码多 层与层之间耦合性高 不利于扩展等问题.今天的这章 主要就是解决这些问题的。再解决这些问题时,自己也产生了很多疑问,理解的也并不是很透彻 ,希望我的疑问能在这里得到解答~~
一.模式介绍
1.Repository
在《企业架构模式》中,通过用来访问领域对象的一个类似集合的接口,在领域与数据映射层之间进行协调。还有请大家参考这个 P OF EAA详细介绍
然后说下我对这个的感觉和疑问 怎么都觉得这个Repository就是以前的dao(dal)层~~ 不过就是通过接口 泛型 与ORM结合 实现了更好的复用 不知道对不~~
2.Unit of Work
先上介绍 Unit Of Work 定义和解释 。主要是解决当有多个操作时,数据的变更 存储 以及事务的处理等 。unit of work是一个记录所有对象模型修改过的信息,在提交的时候,一次性修改,并把结果同步到数据库。 其实我们可以发现 DbContext 已经具备了这样的功能~~ 很大程度实现了Unit of work~ 因为我们savechange()
时 才提交数据的
3.整体概述
运用Repository 和Unit of Work 在 数据层 和业务逻辑层之间 再创建一个抽象层 。它将帮我们隔离变化,并且更利于测试.
下面借用下原文的图 说明下使用Repository 和Unit of Work和不使用的区别
二.改造开始
让我们回到最早的学生控制器上 忘了讲啥的朋友可以看下这节-------学生控制器 我们把
private SchoolContext db =new SchoolContext();
写在了每个控制器里面 声明周期与控制器完全耦合在了一起
1.创建学生资源库接口
using System; using System.Collections.Generic; using System.Linq; using System.Web; using ContosoUniversity.Models;
namespace ContosoUniversity.DAL { publicinterface IStudentRepository : IDisposable { IEnumerable<Student> GetStudents(); Student GetStudentByID(int studentId); void InsertStudent(Student student); void DeleteStudent(int studentID); void UpdateStudent(Student student); void Save(); } }
2.实现接口
using System; using System.Collections.Generic; using System.Linq; using System.Data; using ContosoUniversity.Models;
namespace ContosoUniversity.DAL { publicclass 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); }
publicvoid InsertStudent(Student student) { context.Students.Add(student); }
publicvoid DeleteStudent(int studentID) { Student student = context.Students.Find(studentID); context.Students.Remove(student); }
publicvoid UpdateStudent(Student student) { context.Entry(student).State = EntityState.Modified; }
publicvoid Save() { context.SaveChanges(); }
privatebool disposed =false;
protectedvirtualvoid Dispose(bool disposing) { if (!this.disposed) { if (disposing) { context.Dispose(); } } this.disposed =true; }
publicvoid Dispose() { Dispose(true); GC.SuppressFinalize(this); } } }
3.改造学生控制器
View Codeusing 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 { publicclass 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 pageIndex = (page ??1) -1; return View(students.ToPagedList(pageIndex, pageSize)); }
//// GET: /Student/Details/5public ViewResult Details(int id) { Student student = studentRepository.GetStudentByID(id); return View(student); }
//// GET: /Student/Createpublic 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/5public 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/5public 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"); }
protectedoverridevoid Dispose(bool disposing) { studentRepository.Dispose(); base.Dispose(disposing); } } }
通过上面的操作 现在我们的控制器 针对的是一个接口 而不再是直接通过dbContext 当然 如果我们这里使用IOC容器 将能实现更好的解耦 大家可以看下
桀骜的灵魂的 这篇文章
4.改造后出现的问题
1.我们本来在控制器里实现的dbContext, 现在这个被放在了Repository里 因为项目会有多个Repository 所以会有多个dbContext! 这是个时候 当我们有
一个复杂的逻辑 要用到多个Repository时 比如 我随便说个例子~~ 比如 添加个订单 更新个详细订单 再删除个客户 假设这一系列的逻辑 是在一起的 这里就会用到三个 Repository 我们要调用不同的Repository 里的 SAVE() 这时 我们就要做多次提交 而且不再是统一的事务了 效率 完整性 都大大的下降了。反而失去了
dbContext 自带 Unit of Work的好处~~ 没关系 后面我们会再实现Unit of Work 来改造这个问题
2.第二个问题 也是个很严重的问题 我们以前的时候 再查找学生 搜索 排序 以及分页时 是这样的
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())); }
而现在变成了
var students = from s in studentRepository.GetStudents() select s;
这有个很严重的问题 以前是 IQueryable 而现在的是
IEnumerable 变成了数据直接全部查找出来 所以再这里 我觉得Repository的查找 应该是返回
IQueryable
而不是 IEnumerable
要不然
就出现了我最早在文中的疑问 这不就是普通的CRUD 一个普通的数据访问层而已
而Repository用法我觉得应该是
返回
IQueryable 参数的接受应该是一个表达式树 不知道大家是否认同?希望大家帮我解决下疑惑 谢谢~
再下面的 公共泛型 Repository 会体现这个
三.创建一个公共的Repository
看看我们上面的学生Repository 如果我们再写 课程 院系 等等 这些代码 会非常类似 所以我们利用泛型注入 来实现复用 这里应该实现一个泛型接口 和泛型类
但是原文没有实现接口~ 只有个类
让我来看下这个类
namespace ContosoUniversity.DAL { publicclass GenericRepository<TEntity>where TEntity : class { internal SchoolContext context; internal DbSet<TEntity> dbSet;
public GenericRepository(SchoolContext context) { this.context = context; this.dbSet = context.Set<TEntity>(); }
publicvirtual 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 (newchar[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { query = query.Include(includeProperty); }
if (orderBy !=null) { return orderBy(query).ToList(); } else { return query.ToList(); } }
publicvirtual TEntity GetByID(object id) { return dbSet.Find(id); }
publicvirtualvoid Insert(TEntity entity) { dbSet.Add(entity); }
publicvirtualvoid Delete(object id) { TEntity entityToDelete = dbSet.Find(id); Delete(entityToDelete); }
publicvirtualvoid Delete(TEntity entityToDelete) { if (context.Entry(entityToDelete).State == EntityState.Detached) { dbSet.Attach(entityToDelete); } dbSet.Remove(entityToDelete); }
publicvirtualvoid Update(TEntity entityToUpdate) { dbSet.Attach(entityToUpdate); context.Entry(entityToUpdate).State = EntityState.Modified; } } }
这里重点讲这个方法
publicvirtual 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 (newchar[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { query = query.Include(includeProperty); }
if (orderBy !=null) { return orderBy(query).ToList(); } else { return query.ToList(); } }
还是如上面所说 我觉得这里应该返回的是 IQueryable
所以我觉得应该 去掉最后的 .ToList 并且返回IQueryable
然后 来看这个方法 第一个接受一个表达式树 其实就是过滤条件 第二个 是个委托 主要是用来排序的 第三个接受要贪婪加载哪些导航属性 可以用逗号隔开
并且 这里利用了下4.0的功能 可以给参数个默认值 都是空 怎么用这个方法 后面会写的有~~
还有个说的地方 以前我没有提到过 这里稍带的说下
publicvirtualvoid Delete(TEntity entityToDelete) { if (context.Entry(entityToDelete).State == EntityState.Detached) { dbSet.Attach(entityToDelete); } dbSet.Remove(entityToDelete); }
这个 dbSet.Attach(entityToDelete); 表示将对象添加到数据库上下文中 受dbcontext管理 这里我有个小疑问 不加这个判断 是否也可以 加这个的好处是?
四.创建 Unit of Work Class
创建这个类的主要目的 就是为了确保 多个Repository可以共享一个 database context 让我们看下这个类
namespace ContosoUniversity.DAL { publicclass 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; } }
publicvoid Save() { context.SaveChanges(); }
privatebool disposed =false;
protectedvirtualvoid Dispose(bool disposing) { if (!this.disposed) { if (disposing) { context.Dispose(); } } this.disposed =true; }
publicvoid Dispose() { Dispose(true); GC.SuppressFinalize(this); } } }
把想让unit of work 帮你整体控制的类写到里面 这里 我们只写了两个
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; } }
最后是保存和资源的释放
好 现在看下怎么用这个~
五.改变课程控制器
原文控制器代码namespace ContosoUniversity.Controllers { publicclass 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/5public ViewResult Details(int id) { Course course = unitOfWork.CourseRepository.GetByID(id); return View(course); }
//// GET: /Course/Createpublic 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); }
privatevoid 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/5public 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"); }
protectedoverridevoid Dispose(bool disposing) { unitOfWork.Dispose(); base.Dispose(disposing); } } }
原文的这个demo 并没有很好的体现出 使用 unit of work 的好处 因为他的这个例子没有出现一个逻辑用到多个资源库的 希望大家明白这点~~ 等以后的文章
我会写个完整的demo 来说明这点 这里大家先看下 明白怎么回事就行~
六.EF+MVC框架的疑问
1.是否有必要实现 IUnitOfWork 接口?代码大概这样
publicinterface IUnitOfWork
{
void Save();
IStudentRepository StudentRepository { get; }
}
实现接口
publicclass UnitOfWork : IUnitOfWork
{
private SchoolEntities context =new SchoolEntities();
private IStudentRepository studentRepository;
public IStudentRepository StudentRepository
{
get
{
if (this.studentRepository ==null)
{
this.studentRepository =new StudentRepository(context);
}
return studentRepository;
}
}
publicvoid Save()
{
context.SaveChanges();
}
}
控制器
private IUnitOfWork unitOfWork;
public StudentController () : this (new UnitOfWork())
{
}
public StudentController (IUnitOfWork unitOfWork)
{
this.unitOfWork = unitOfWork;
}
2. 是否有必要再 加上一个 服务层 service 这个层 在 控制器和dal 之间 也就是 通过 unitofwork 调用的东西等 都写在servie上 这样控制器里的代码会变得非常少, 个人觉得应该加上service层的,但是是否需要加Iservice 接口 加这个接口 能获得哪些好处 ? 我一直觉得 只用 给数据访问层 实现接口 就行 ~~
3. 这里我们是用的unit of work 完成了事务的一致性 以前我是使用
using (TransactionScope transaction =new TransactionScope()){ .... transaction.Complete(); }
用这个来实现 事务一致性 不知道大家觉得 这个和 unit of work 比怎么样? 我暂时还没研究这个~ 但是 小城岁月 对这个做了很好的介绍 感谢小城~ 大家可以参考他的这篇文章
4. 我们的缓存 比如用的 mongodb 这个写到哪层比较好呢?
六.总结
经过重构 代码终于有些项目的样子了~~ 下节讲讲EF的其他一些功能 如 直接执行SQL语句,关闭跟踪状态这些~~