实体框架6代码优先 - 存储库实现是一个好的吗?

时间:2014-01-25 11:30:13

标签: entity-framework repository-pattern unit-of-work

我即将使用存储库和工作单元实现Entity Framework 6设计。

有很多文章,我不确定最好的建议是什么:例如,我真的很喜欢这里实现的模式:出于文章here中提出的原因

但是,Tom Dykstra (Senior Programming Writer on Microsoft's Web Platform & Tools Content Team)建议应该在另一篇文章中完成:here

我订阅Pluralsight,并且每次在课程中使用它时都会以稍微不同的方式实现,因此选择设计很困难。

有些人似乎认为工作单元已由DbContext实施,如post所示,因此我们根本不需要实施。

我意识到之前已经提出过这类问题,这可能是主观的,但我的问题是直接的:

我喜欢第一篇(Code Fizzle)文章中的方法,并想知道它是否可以更容易维护,并且可以像其他方法一样容易测试并且可以安全地继续使用?

欢迎任何其他观点。

7 个答案:

答案 0 :(得分:46)

@Chris Hardie是正确的,EF实现UoW开箱即用。然而,许多人忽略了EF也开箱即用的通用存储库模式这一事实:

var repos1 = _dbContext.Set<Widget1>();
var repos2 = _dbContext.Set<Widget2>();
var reposN = _dbContext.Set<WidgetN>();

...这是一个非常好的通用存储库实现,内置于工具本身。

为什么在DbContext为您提供所需的一切时,会遇到创建大量其他界面和属性的麻烦?如果你想在应用程序级接口后面抽象DbContext,并且想要应用命令查询隔离,你可以做一些简单的事情:

public interface IReadEntities
{
    IQueryable<TEntity> Query<TEntity>();
}

public interface IWriteEntities : IReadEntities, IUnitOfWork
{
    IQueryable<TEntity> Load<TEntity>();
    void Create<TEntity>(TEntity entity);
    void Update<TEntity>(TEntity entity);
    void Delete<TEntity>(TEntity entity);
}

public interface IUnitOfWork
{
    int SaveChanges();
}

您可以将这3个接口用于所有实体访问,而不必担心将3个或更多不同的存储库注入到与3个或更多实体集一起使用的业务代码中。当然,您仍然会使用IoC来确保每个Web请求只有1个DbContext实例,但是所有3个接口都是由同一个类实现的,这使得它更容易。

public class MyDbContext : DbContext, IWriteEntities
{
    public IQueryable<TEntity> Query<TEntity>()
    {
        return Set<TEntity>().AsNoTracking(); // detach results from context
    }

    public IQueryable<TEntity> Load<TEntity>()
    {
        return Set<TEntity>();
    }

    public void Create<TEntity>(TEntity entity)
    {
        if (Entry(entity).State == EntityState.Detached)
            Set<TEntity>().Add(entity);
    }

    ...etc
}

现在,您只需要在依赖项中注入一个接口,无论需要使用多少个不同的实体:

// NOTE: In reality I would never inject IWriteEntities into an MVC Controller.
// Instead I would inject my CQRS business layer, which consumes IWriteEntities.
// See @MikeSW's answer for more info as to why you shouldn't consume a
// generic repository like this directly by your web application layer.
// See http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=91 and
// http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=92 for more info
// on what a CQRS business layer that consumes IWriteEntities / IReadEntities
// (and is consumed by an MVC Controller) might look like.
public class RecipeController : Controller
{
    private readonly IWriteEntities _entities;

    //Using Dependency Injection 
    public RecipeController(IWriteEntities entities)
    {
        _entities = entities;
    }

    [HttpPost]
    public ActionResult Create(CreateEditRecipeViewModel model)
    {
        Mapper.CreateMap<CreateEditRecipeViewModel, Recipe>()
            .ForMember(r => r.IngredientAmounts, opt => opt.Ignore());

        Recipe recipe = Mapper.Map<CreateEditRecipeViewModel, Recipe>(model);
        _entities.Create(recipe);
        foreach(Tag t in model.Tags) {
            _entities.Create(tag);
        }
        _entities.SaveChanges();
        return RedirectToAction("CreateRecipeSuccess");
    }
}

我最喜欢这个设计的一个方面是它最小化消费者 上的实体存储依赖性。在此示例中,RecipeController是使用者,但在实际应用程序中,使用者将是命令处理程序。 (对于查询处理程序,您通常只使用IReadEntities,因为您只想返回数据,而不是改变任何状态。)但是对于此示例,我们只需使用RecipeController作为使用者来检查依赖关系影响:

假设您为上述操作编写了一组单元测试。在每个单元测试中,您新建Controller,将模拟传递给构造函数。然后,假设您的客户决定在创建新配方时允许人们创建新的Cookbook或添加到现有的Cookbook。

使用每个实体的存储库或每个聚合的存储库接口模式,您必须将新的存储库实例IRepository<Cookbook>注入到控制器构造函数中(或者使用@Chris Hardie的答案,编写代码以进行附加UoW实例的另一个存储库)。这将立即使所有其他单元测试中断,您将不得不返回修改所有这些中的构造代码,传递另一个模拟实例,并扩展您的依赖项数组。但是,如上所述,所有其他单元测试仍然至少可以编译。您所要做的就是编写额外的测试以涵盖新的食谱功能。

答案 1 :(得分:42)

我(不)很遗憾地说代码失败,Dyksta的文章以及之前的回答是错误的。简单来说,他们使用EF实体作为域(业务)对象,这是一个很大的WTF。

更新:对于技术性较差的解释(简单来说),请阅读Repository Pattern for Dummies

简而言之,任何存储库接口都不应该与任何持久性(ORM)细节相关联。 repo界面仅处理对应用程序的其余部分有意义的对象(域,可能是UI,如演示文稿)。很多人(MS领导包装,我怀疑的意图)犯了错误,认为他们可以重用他们的EF实体或者可以作为业务对象。

虽然可以发生,但这种情况非常罕见。在实践中,您将拥有许多域对象&#39; design&#39;在数据库规则之后,即不良建模存储库的目的是将应用程序的其余部分(主要是业务层)与其持久性表单分离。

当你的repo处理EF实体(持久性细节)或者它的方法返回IQueryable时,你如何解耦它,这是一个带有错误语义的泄漏抽象(IQueryable允许你构建一个查询,因此暗示你需要知道持久性细节因此否定了存储库的目的和功能)?

一个统治对象永远不应该知道持久性,EF,连接等。它不应该知道你正在使用什么数据库引擎,或者你是否正在使用它。与应用程序的其余部分相同,如果您希望它与持久性详细信息解耦

存储库界面只知道更高层知道的内容。这意味着,通用域存储库接口看起来像这样

public interface IStore<TDomainObject> //where TDomainObject != Ef (ORM) entity
{
   void Save(TDomainObject entity);
   TDomainObject Get(Guid id);
   void Delete(Guid id);
 }

实现将驻留在DAL中,并将使用EF来处理数据库。但是实现看起来像这样

public class UsersRepository:IStore<User>
 {
   public UsersRepository(DbContext db) {}


    public void Save(User entity)
    {
       //map entity to one or more ORM entities
       //use EF to save it
    }
           //.. other methods implementation ...

 }

您确实没有具体通用存储库。具体通用存储库的唯一用法是当任何域对象以序列化形式存储在类似键的值中时。 ORM的情况并非如此。

查询怎么样?

 public interface IQueryUsers
 {
       PagedResult<UserData> GetAll(int skip, int take);
       //or
       PagedResult<UserData> Get(CriteriaObject criteria,int skip, int take); 
 }

UserData 是适合查询上下文使用的读/视型模型。

如果您不介意DAL了解视图模型,那么您可以直接使用EF在query handler中进行查询。在这种情况下,您不会需要任何查询回购。

<强>结论

  • 您的业务对象不应该了解EF实体。
  • 存储库将使用ORM ,但永远不会将ORM 公开给应用程序的其余部分,因此repo界面将仅使用域对象或视图模型(或任何其他不具有持久性详细信息的应用程序上下文对象
  • 您没有告诉回购如何来完成其工作,即永远不要将IQueryable与repo接口一起使用
  • 如果您只是想以更简单/更酷的方式使用数据库,并且您正在处理一个简单的CRUD应用程序,您不需要(确保它)以保持关注点的分离,那么< em>一起跳过存储库,直接使用EF获取所有数据。该应用程序将与EF紧密耦合,但至少你会削减中间人,并且它的目的不是错误的。

请注意,以错误的方式使用存储库会使其使用无效,您的应用仍然会与持久性(ORM)紧密耦合。

如果您认为ORM可以神奇地存储您的域对象,那么它就不是。 ORM的目的是在关系表之上模拟OOP存储。它与持久性有关,与域无关,因此不要使用ORM外部持久性。

答案 2 :(得分:4)

DbContext确实是以工作单元模式构建的。它允许其所有实体共享与我们一起使用的相同上下文。此实现是内部DbContext

但是,应该注意的是,如果您实例化两个DbContext个对象,它们都不会看到他们每个跟踪的另一个实体。它们相互隔离,这可能会有问题。

当我构建MVC应用程序时,我想确保在请求过程中,我的所有数据访问代码都只使用一个DbContext。为实现这一目标,我将工作单位应用为DbContext外部的模式。

这是我正在建立的烧烤食谱应用程序中的工作单元对象:

public class UnitOfWork : IUnitOfWork
{
    private BarbecurianContext _context = new BarbecurianContext();
    private IRepository<Recipe> _recipeRepository;
    private IRepository<Category> _categoryRepository;
    private IRepository<Tag> _tagRepository;

    public IRepository<Recipe> RecipeRepository
    {
        get
        {
            if (_recipeRepository == null)
            {
                _recipeRepository = new RecipeRepository(_context);
            }
            return _recipeRepository;
        }
    }

    public void Save()
    {
        _context.SaveChanges();
    }
    **SNIP**

我将我的所有存储库(都注入了相同的DbContext)附加到我的工作单元对象。只要从工作单元对象请求任何存储库,我们就可以确保我们所有的数据访问代码都将使用相同的DbContext进行管理 - 非常棒!

如果我在MVC应用程序中使用它,我会确保在整个请求中使用工作单元,在控制器中实例化它,并在整个行动中使用它:

public class RecipeController : Controller
{
    private IUnitOfWork _unitOfWork;
    private IRepository<Recipe> _recipeService;
    private IRepository<Category> _categoryService;
    private IRepository<Tag> _tagService;

    //Using Dependency Injection 
    public RecipeController(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
        _categoryRepository = _unitOfWork.CategoryRepository;
        _recipeRepository = _unitOfWork.RecipeRepository;
        _tagRepository = _unitOfWork.TagRepository;
    }

现在,我们可以确保所有数据访问代码都使用相同的DbContext

    [HttpPost]
    public ActionResult Create(CreateEditRecipeViewModel model)
    {
        Mapper.CreateMap<CreateEditRecipeViewModel, Recipe>().ForMember(r => r.IngredientAmounts, opt => opt.Ignore());

        Recipe recipe = Mapper.Map<CreateEditRecipeViewModel, Recipe>(model);
        _recipeRepository.Create(recipe);
        foreach(Tag t in model.Tags){
             _tagRepository.Create(tag); //I'm using the same DbContext as the recipe repo!
        }
        _unitOfWork.Save();

答案 3 :(得分:3)

在互联网上搜索我发现了http://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework/这是一篇关于Jon Smith的存储库模式有用性的2篇文章。 第二部分侧重于解决方案。希望它有所帮助!

答案 4 :(得分:2)

使用工作单元模式实现的存储库是一个很难回答你的问题。

实体框架的DbContext由Microsoft根据工作单元格式实现。这意味着context.SaveChanges一次性以事务方式保存您的更改。

DbSet也是Repository模式的一个实现。不要构建您可以执行的存储库:

void Add(Customer c)
{
   _context.Customers.Add(c);
}

为服务中的内容创建一个单行方法???

没有任何好处,现在没有人将EF ORM改为另一个ORM ......

你不需要那种自由......

克里斯·哈迪(Chris Hardie)认为可以实例化多个上下文对象但已经这样做了你做错了......

只需使用您喜欢的IOC工具,并根据Http请求设置MyContext即可。

以ninject为例:

kernel.Bind<ITeststepService>().To<TeststepService>().InRequestScope().WithConstructorArgument("context", c => new ITMSContext());

运行业务逻辑的服务会注入上下文。

保持简单愚蠢: - )

答案 5 :(得分:1)

你应该考虑“命令/查询对象”作为替代,你可以在这个区域找到一堆有趣的文章,但这里有一个很好的文章:

https://rob.conery.io/2014/03/03/repositories-and-unitofwork-are-not-a-good-idea/

当您需要跨多个数据库对象的事务时,请为每个命令使用一个命令对象,以避免UOW模式的复杂性。

大多数项目可能不需要每个查询的查询对象。相反,您可以选择以“FooQueries”对象开头 ......我的意思是你可以从READS的Repository模式开始,但将其命名为“Queries”,以明确它不会也不应该进行任何插入/更新。

稍后,如果您想添加授权和日志记录等内容,可能会找到拆分单个查询对象,您可以将查询对象提供给管道。

答案 6 :(得分:0)

我总是首先使用UoW和EF代码。我发现它更高效,更容易管理你的上下文,防止内存泄漏等。您可以在我的github上找到我的解决方法示例:RADAR项目中的http://www.github.com/stefchri

如果您有任何疑问,请随时提出。