MVC 中的通用存储库和 UnitOfWork 模式






4.87/5 (16投票s)
在 MVC 应用程序中使用通用存储库和工作单元模式,我们可以创建更灵活的应用程序。
引言
在开发应用程序时,如果我们牢记松耦合系统的优势,那么我们最终会创建出更易于维护的系统。松耦合系统是指组件对应用程序中其他组件的了解很少或没有。松耦合架构的一些优点是:
- 不同的组件可以独立更改,而不影响其他组件,从而使应用程序更易于维护
- 它更适合敏捷方法论,其中更改频繁发生,因此紧密耦合不允许将来的更改
- 创建模拟测试更容易,因为实际对象可以很容易地替换为模拟对象。
相反,在强耦合系统中,应用程序组件(例如类或模块)包含对另一个组件的具体实例的直接引用。这使得系统难以维护。
在 MVC 应用程序中工作时,我们应该记住 MVC 框架基于松耦合架构。在 MVC 框架中,每个组件都基于接口或抽象类。这意味着我们可以轻松地将框架的任何组件替换为另一个组件。
如果我们以控制器和视图为例,它们实现了 IController 和 IVeiw 接口。我们可以用任何实现这些接口的组件来替换它们。
为什么要使用存储库模式?
MVC 应用程序支持创建解耦架构,而创建解耦架构的一种方法是使用存储库模式。
通常,MVC 应用程序中的模型组件负责包含业务逻辑、业务规则以及访问数据源以进行数据检索和更新。虽然我们可以将数据访问逻辑放入模型中,但它有一些缺点:
如果我们在模型中的多个位置访问相同的数据,则代码会重复。这与 DRY(不要重复自己)设计方法相悖。DRY 是一种设计原则,意味着每条信息都应该唯一地表示,没有冗余。
业务逻辑知道数据源类型,无论数据源是 SQL Server 还是 Oracle。因此,如果数据源更改,模型也需要更新以反映更改。正如我们上面讨论的松耦合设计,我们可以称之为紧耦合设计。
如果我们不分离数据访问功能,那么模型除了封装业务逻辑的主要职责之外,还具有访问数据源并将数据映射到业务实体的额外职责。这使得模型难以进行单元测试,因为要测试模型,我们还需要访问数据源。
如果我们想实现应用程序数据访问规则,则没有一个中心位置来做出更改,相反,必须在多个地方进行更改,这是一个耗时的过程,代码也难以维护。
存储库是一种设计模式,它介于域层和数据层之间。它有助于数据检索和持久化。
使用存储库,我们分离了数据访问和映射职责,因此这使得模型和数据访问逻辑松耦合。
如果我们使用存储库,那么模型层会向存储库请求它想要获取或更新的任何数据。在数据源中检索和更新数据的职责仅在于存储库。我们可以轻松地切换后端数据源,而不会影响应用程序的其余部分。
假设我们的设计决策使我们更改数据源,这在敏捷方法论中很常见,在生命周期的后期,如果我们使用存储库模式,我们可以轻松地做到这一点。
业务逻辑以以下方式与存储库通信:
- 我们的业务逻辑以业务实体形式从存储库请求数据。
- 存储库从业务逻辑接收请求。它向底层数据源发出请求。
- 存储库从数据源接收原始数据。
- 存储库将数据映射到业务实体并返回实体。
因此,现在模型可以只专注于业务逻辑,而不必担心数据存储的位置和方式。
工作单元
工作单元是一种设计模式,它维护事务列表,用更改更新数据源,并提供并发问题的解决方案。因此,如果应用程序正在执行多个并发操作,那么工作单元就是解决方案。
尽管像 Entity Framework 这样的框架内部实现了工作单元模式,但在应用程序中实现工作单元有几个原因:
- 我们正在应用程序中实现事务,并希望对数据库的插入、删除和更新进行排序。
- 我们希望防止应用程序中出现重复更新。在一个应用程序中,我们可以有多个更新操作,其中一些可能是重复的。使用工作单元,只会有一个数据库更新,而不是多个。
- 我们希望对应用程序中的数据库操作有更多的控制权。
例如,如果我们想同时更新学生详细信息和教师详细信息,那么我们可以通过使用工作单元设计模式来实现这种程度的控制。使用工作单元,我们可以从应用程序的中心位置完全控制数据访问操作。
使用代码
在示例应用程序中,我们将创建一个 MVC 应用程序,该应用程序维护学生列表并提供更新列表的选项。
我们将使用工作单元模式实现 CRUD 操作。我们将创建一个通用存储库类,以便可以在应用程序中针对不同类型的实体进行重用。
IRepository<T> 和 Repository<T>
我们使用接口IRepository为通用存储库类定义接口。它包含用于插入、搜索、更新和删除功能的 CRUD 操作。
public interface IRepository<T>
{
T GetById(int id);
IQueryable<T> GetAll();
void Edit(T entity);
void Insert(T entity);
void Delete(T entity);
}
现在我们已经声明了存储库的接口,我们可以声明一个实现通用接口IRepository的通用类。该类有两个字段 DbContext 和 DbSet,分别用于与数据库通信和表示数据。
下面是 Repository 类的完整定义。
public class Repository<T> : IRepository<T> where T:class
{
public DbContext context;
public DbSet<T> dbset;
public Repository(DbContext context)
{
this.context = context;
dbset = context.Set<T>();
}
public T GetById(int id)
{
return dbset.Find(id);
}
public IQueryable<T> GetAll()
{
return dbset;
}
public void Insert(T entity)
{
dbset.Add(entity);
}
public void Edit(T entity)
{
context.Entry(entity).State = EntityState.Modified;
}
public void Delete(T entity)
{
context.Entry(entity).State = EntityState.Deleted;
}
}
UnitOfWork 和 IUnitOfWork
UnitOfWork 类维护应用程序中存储库的列表。IUnitOfWork 接口定义了 UnitOfWork 类的操作。
请注意,在 Save 方法中,我们调用了 context.SaveChanges() 方法。在上面的存储库中,我们只是设置了实体状态,但实际上并未插入数据库。只有 Save 方法才会在数据库中更新实体,因此我们的 UnitofWork 类负责数据库更新操作。
如果我们使用多个存储库,并且任何更新失败,我们都不会调用 SaveChanges() 方法。由于记录未在存储库中的数据库中更新,因此无需回滚更改。
public interface IUnitOfWork : IDisposable
{
IRepository<Student> StudentRepository { get; }
void Save();
}
public partial class UnitOfWork : IUnitOfWork
{
private IRepository<Student> _studentRepository;
private Context _context;
public IRepository<Student> StudentRepository
{
get {
if (_studentRepository==null)
_studentRepository = new Repository<Student>(_context);
return _studentRepository; }
}
public UnitOfWork()
{
_context=new Context();
}
public void Save()
{
_context.SaveChanges();
}
public void Dispose()
{
throw new NotImplementedException();
}
}
我们有一个单独的控制器 StudentsController,它访问 UnitofWork 类以根据用户交互执行不同的操作。
当我们执行示例应用程序时,我们可以执行不同的 CRUD 操作。
如果我们想要设计一个系统,其中 ORM 或数据库可以轻松切换,并且还希望能够对具有外部依赖项的业务逻辑进行单元测试,那么存储库模式就是解决方案。
关注点
尽管 MVC 不强制我们遵循特定的设计原则,但由于 MVC 应用程序应该灵活地适应未来的更改并可测试,因此我们应该使用有助于我们创建此类应用程序的设计原则。通过使用某些设计指南,我们可以创建更健壮和灵活的应用程序。