不使用存储库模式,按原样使用ORM(EF)

Dej*_*n.S 89 architecture aop design-patterns entity-framework repository-pattern

我总是使用Repository模式但是对于我的最新项目,我想看看我是否可以完善它的使用和我的"工作单元"的实现.我开始挖的越多,我开始问自己一个问题:"我真的需要它吗?"

现在这一切都从Stackoverflow上的一些评论开始,跟踪Ayende Rahien在他的博客上的帖子,其中有2个具体,

这可能是永远和永远讨论的,它取决于不同的应用程序.我想知道什么

  1. 这种方法是否适合实体框架项目?
  2. 使用这种方法是业务逻辑仍然在服务层或扩展方法(如下所述,我知道,扩展方法是使用NHib会话)?

使用扩展方法很容易做到这一点.干净,简单,可重复使用.

public static IEnumerable GetAll(
    this ISession instance, Expression<Func<T, bool>> where) where T : class
{
    return instance.QueryOver().Where(where).List();
}
Run Code Online (Sandbox Code Playgroud)

使用这种方法和NinjectDI,我是否需要创建Context一个接口并将其注入我的控制器?

Rya*_*yan 98

我已经走了很多路,并在不同的项目中创建了许多存储库的实现,并且......我已经抛弃并放弃了它,这就是原因.

编码异常

您是否将您的数据库从一种技术更改为另一种技术的可能性为1%?如果您正在考虑您的业务的未来状态,并说是可能的话,那么a)他们必须有很多钱才能转移到另一个数据库技术或b)您选择数据库技术来获得乐趣或者c你决定使用的第一项技术出现了严重错误.

为什么抛弃丰富的LINQ语法?

开发了LINQ和EF,因此您可以使用它来读取和遍历对象图.创建和维护一个可以为您提供相同灵活性的存储库是一项非常糟糕的任务.根据我的经验,每当我创建一个存储库时,我总是将业务逻辑泄漏到存储库层,以使查询更具执行性和/或减少数据库的命中数.

我不想为我必须编写的查询的每个单独排列创建一个方法.我不妨编写存储过程.我不想要GetOrder,GetOrderWithOrderItem,GetOrderWithOrderItemWithOrderActivity,GetOrderByUserId等等...我只是想获得主实体并遍历并包含对象图,因为我喜欢.

大多数存储库的例子都是废话

除非你正在开发像博客这样真正的东西,否则你的查询永远不会像你在互联网上找到的关于存储库模式的90%的例子一样简单.我不能强调这一点!这是人们必须爬过泥泞才能弄明白的东西.总会有一个查询打破了您创建的完美思考的存储库/解决方案,直到您第二次猜测自己和技术债务/侵蚀开始.

不要对我进行单元测试

但是,如果我没有存储库,那么单元测试呢?我怎么会嘲笑?很简单,你没有.让我们从两个角度来看待它:

没有存储库 - 你可以使用IDbContext或其他一些技巧来模拟DbContext,但是你真的是单元测试LINQ to Objects而不是LINQ to Entities,因为查询是在运行时确定的......好吧这样做不好!所以现在可以通过集成测试来解决这个问题.

使用存储库 - 您现在可以模拟存储库并对它们之间的层进行单元测试.好吧?实际上并非如此......在上述情况下,您必须将逻辑泄漏到存储库层以使查询更多地执行和/或更少地访问数据库,您的单元测试如何覆盖?它现在在repo层,你不想测试IQueryable吗?另外说实话,您的单元测试不会涵盖具有20行GetOrder子句和GetOrderWithOrderItem"一堆关系" 的查询,并再次点击数据库来执行所有其他的操作,等等,等等,因为查询是在运行时生成.此外,由于您创建了一个存储库来保持上层持久性无知,如果您现在想要更改数据库技术,抱歉您的单元测试肯定不会在运行时保证相同的结果,回到集成测试.所以存储库的重点似乎很奇怪..

2美分

在普通存储过程(批量插入,批量删除,CTE等)上使用EF时,我们已经失去了很多功能和语法,但我也在C#中编码,所以我不必输入二进制文件.我们使用EF,因此我们可以使用不同的提供程序,并以很好的相关方式处理对象图.某些抽象是有用的,有些则不是.

我希望这可以帮助互联网上的某个人......

  • 您不创建存储库以便能够对它们进行单元测试.您可以创建存储库以便能够对***业务逻辑***进行单元测试.至于确保查询工作:为存储库编写集成测试要容易得多,因为它们只包含逻辑而不包含任何业务. (13认同)
  • `编码异常`:使用存储库不能切换数据库引擎.这是关于将业务与持久性分离. (13认同)
  • 这个答案+1.我发现我们确实不需要具有Entity Framework Core的存储库.`DbSet`是*repository*,`DbContext`是**工作单元**.为什么在ORM已经为我们这样做时实现存储库模式!要进行测试,只需将提供程序更改为"InMemory"即可.并做你的测试!它在MSDN中有详细记载. (3认同)
  • 这些都是非常有效的观点,背后有很多真相。然而,缺少的是 LINQ 散布在应用程序中而不是限制在一致的位置,这会在代码隐藏页面中创建等效于 SQL 调用的 EF。每个 LINQ 查询都是应用程序中一个潜在的维护点,数量越多(而且分布越广),维护成本和风险就越高。想象一下,向实体添加“已删除”标志,并且必须在大型应用程序中找到查询实体的每个位置,并且必须修改每个... (2认同)
  • 我认为这是短视和厌倦。为什么要将逻辑泄漏到 repo 中?如果你这样做了,那又有什么关系呢?这是一个数据实现。我们所做的只是通过将 LINQ 隐藏在 repo 后面来将 LINQ 与其余代码隔离开来。你说不要测试它,但是你用无法测试它作为反对这样做的论据。所以制作repo,不要暴露IQueryable,也不要测试它。至少您可以独立于数据实现测试其他所有内容。并且 1% 的 db 更改机会仍然是一个巨大的 $-wise。 (2认同)

jga*_*fin 46

存储库模式是一种抽象.它的目的是降低复杂性并使其余代码持久无知.作为奖励,它允许您编写单元测试而不是集成测试.

问题是许多开发人员无法理解模式的目的并创建存储库,这些存储库会将持久性特定信息泄露给调用者(通常通过公开IQueryable<T>).通过这样做,他们没有直接使用OR/M的好处.

更新以解决另一个答案

编码异常

使用存储库不是要能够切换持久性技术(即改变数据库或使用Web服务等).它是将业务逻辑与持久性分离,以降低复杂性和耦合.

单元测试与集成测试

您不为存储库编写单元测试.期.

但是通过引入存储库(或持久性和业务之间的任何其他抽象层),您可以为业务逻辑编写单元测试.即,您不必担心由于数据库配置不正确而导致测试失败.

至于查询.如果您使用LINQ,您还必须确保查询正常工作,就像您必须使用存储库一样.这是使用集成测试完成的.

不同之处在于,如果您没有将业务与LINQ语句混合,那么您可以100%确定它是您的持久性代码失败而不是其他东西.

如果你分析你的测试,如果你没有混合的问题(例如LINQ +业务逻辑),你也会发现它们更清洁.

存储库示例

大多数例子都是废话.这是非常真实的.但是,如果你谷歌任何设计模式,你会发现很多糟糕的例子.这是没有理由避免使用模式.

构建正确的存储库实现非常容易.实际上,您只需遵循一条规则:

在您需要它之前,不要向存储库类添加任何内容

很多程序员都很懒惰,并试图建立一个通用的存储库,并使用一个基类,它们可能需要很多方法.YAGNI.只要应用程序存在(可以是几年),您只需编写一次存储库类并保留它.为什么懒惰他妈的.保持干净,没有任何基类继承.它将使阅读和维护更容易.

(上面的陈述是一个指导原则,而不是一个法律.一个基类可以很好地激发.只需在添加它之前思考,以便你出于正确的原因添加它)

老东西

结论:

如果您不介意在业务代码中使用LINQ语句而不关心单元测试,我认为没有理由不直接使用Entity Framework.

更新

我在博客上写了关于存储库模式和"抽象"真正含义的内容:http://blog.gauffin.org/2013/01/repository-pattern-done-right/

更新2

对于具有20多个字段的单实体类型,您将如何设计查询方法以支持任何排列组合?您不希望仅按名称限制搜索,使用导航属性搜索,使用具有特定价格代码的项目列出所有订单,3级导航属性搜索.IQueryable发明的全部原因是能够组合搜索与数据库的任何组合.理论上一切看起来都很棒,但用户的需要胜过理论.

再次:具有20多个字段的实体被错误地建模.这是一个上帝的实体.分解.

我并不认为这IQueryable不是为了查询.我说这对于像Repository模式这样的抽象层来说是不对的,因为它是漏洞的.没有100%完整的LINQ To Sql提供程序(如EF).

它们都具有特定于实现的功能,例如如何使用eager/lazy加载或如何执行SQL"IN"语句.暴露IQueryable在存储库中会强制用户知道所有这些事情.因此,抽象数据源的整个尝试是完全失败的.您只需添加复杂性而不会直接使用OR/M获得任何好处.

要么正确实现Repository模式,要么根本不使用它.

(如果你真的想要处理大型实体,你可以将Repository模式与Specification模式结合起来.这为你提供了一个完整的抽象,也是可以测试的.)

  • 不暴露IQueryable导致有限的搜索,人们最终为不同类型的查询创建更多的Get方法,并最终使存储库更复杂. (6认同)
  • @yat:每个聚合根一个回购.但是imho它不是***聚合根和表的集合***但只是***聚合根和聚合***.实际存储可能只使用一个表或多个表,即它可能不是每个聚合和表之间的一对一映射.我使用存储库来降低复杂性并删除底层存储的任何依赖关系. (3认同)
  • 你根本没有解决核心问题:通过存储库公开IQueryable并不是一个完整的抽象. (2认同)
  • 就个人而言,我认为在Repository类上实现IQueryable <T>接口也是有意义的,而不是在其中一个成员中公开底层集. (2认同)

quj*_*jck 23

IMO的Repository抽象和UnitOfWork抽象在任何有意义的开发中都占有非常重要的地位.人们会争论实现细节,但正如有许多方法可以对猫进行换肤,有许多方法可以实现抽象.

您的问题是专门使用或不使用以及为什么使用.

正如你无疑意识到你已经拥有了这两种模式内置到实体框架,DbContextUnitOfWorkDbSetRepository.您通常不需要单元测试UnitOfWorkRepository自己,因为它们只是在您的类和底层数据访问实现之间进行简化.在单元测试服务逻辑时,你会发现自己需要一次又一次地模仿这两个抽象.

您可以使用外部库模拟,伪造或其他任何内容,在执行测试的逻辑和正在测试的逻辑之间添加代码依赖性层(您无法控制).

因此,一个小问题是拥有自己的抽象,UnitOfWorkRepository在模拟单元测试时为您提供最大的控制和灵活性.

一切都很好,但对我来说,这些抽象的真正力量在于它们提供了一种应用面向方面编程技术并遵循SOLID原则的简单方法.

所以你有你的IRepository:

public interface IRepository<T>
    where T : class
{
    T Add(T entity);
    void Delete(T entity);
    IQueryable<T> AsQueryable();
}
Run Code Online (Sandbox Code Playgroud)

它的实施:

public class Repository<T> : IRepository<T>
    where T : class
{
    private readonly IDbSet<T> _dbSet;
    public Repository(PPContext context) 
    {
        _dbSet = context.Set<T>();
    }

    public T Add(T entity)
    { 
        return _dbSet.Add(entity); 
    }

    public void Delete(T entity)
    {
        _dbSet.Remove(entity); 
    }

    public IQueryable<T> AsQueryable() 
    {
        return _dbSet.AsQueryable();
    }
}
Run Code Online (Sandbox Code Playgroud)

到目前为止没有什么不寻常的,但现在我们想添加一些日志 - 使用日志装饰器很容易.

public class RepositoryLoggerDecorator<T> : IRepository<T>
    where T : class
{
    Logger logger = LogManager.GetCurrentClassLogger();
    private readonly IRepository<T> _decorated;
    public RepositoryLoggerDecorator(IRepository<T> decorated)
    {
        _decorated = decorated;
    }

    public T Add(T entity)
    {
        logger.Log(LogLevel.Debug, () => DateTime.Now.ToLongTimeString() );
        T added = _decorated.Add(entity);
        logger.Log(LogLevel.Debug, () => DateTime.Now.ToLongTimeString());
        return added;
    }

    public void Delete(T entity)
    {
        logger.Log(LogLevel.Debug, () => DateTime.Now.ToLongTimeString());
        _decorated.Delete(entity);
        logger.Log(LogLevel.Debug, () => DateTime.Now.ToLongTimeString());
    }

    public IQueryable<T> AsQueryable()
    {
        return _decorated.AsQueryable();
    }
}
Run Code Online (Sandbox Code Playgroud)

完成所有操作并且不更改现有代码.我们可以添加许多其他交叉问题,例如异常处理,数据缓存,数据验证或其他任何内容,在整个设计和构建过程中,我们拥有的最有价值的东西使我们能够添加简单的功能而无需更改任何现有代码是我们的IRepository抽象.

现在,很多次我在StackOverflow上看到了这个问题 - "你如何让实体框架在多租户环境中工作?".

https://stackoverflow.com/search?q=%5Bentity-framework%5D+multi+tenant

如果你有Repository抽象,那么答案是"很容易添加一个装饰"

public class RepositoryTennantFilterDecorator<T> : IRepository<T>
    where T : class
{
    //public for Unit Test example
    public readonly IRepository<T> _decorated;
    public RepositoryTennantFilterDecorator(IRepository<T> decorated)
    {
        _decorated = decorated;
    }

    public T Add(T entity)
    {
        return _decorated.Add(entity);
    }

    public void Delete(T entity)
    {
        _decorated.Delete(entity);
    }

    public IQueryable<T> AsQueryable()
    {
        return _decorated.AsQueryable().Where(o => true);
    }
}
Run Code Online (Sandbox Code Playgroud)

IMO你应该总是在任何第三方组件上放置一个简单的抽象,这些组件将在少数几个地方被引用.从这个角度来看,ORM是完美的候选者,因为它在我们的代码中被引用.

当有人说"我为什么要Repository对这个或那个第三方图书馆进行抽象"时,通常会想到的答案是"你为什么不呢?"

PS装饰器使用IoC容器非常简单,例如SimpleInjector.

[TestFixture]
public class IRepositoryTesting
{
    [Test]
    public void IRepository_ContainerRegisteredWithTwoDecorators_ReturnsDecoratedRepository()
    {
        Container container = new Container();
        container.RegisterLifetimeScope<PPContext>();
        container.RegisterOpenGeneric(
            typeof(IRepository<>), 
            typeof(Repository<>));
        container.RegisterDecorator(
            typeof(IRepository<>), 
            typeof(RepositoryLoggerDecorator<>));
        container.RegisterDecorator(
            typeof(IRepository<>), 
            typeof(RepositoryTennantFilterDecorator<>));
        container.Verify();

        using (container.BeginLifetimeScope())
        {
            var result = container.GetInstance<IRepository<Image>>();

            Assert.That(
                result, 
                Is.InstanceOf(typeof(RepositoryTennantFilterDecorator<Image>)));
            Assert.That(
                (result as RepositoryTennantFilterDecorator<Image>)._decorated,
                Is.InstanceOf(typeof(RepositoryLoggerDecorator<Image>)));
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Aka*_*ava 11

首先,正如一些答案所建议的那样,EF本身就是一个存储库模式,没有必要创建进一步的抽象只是将其命名为存储库.

用于单元测试的Mockable存储库,我们真的需要它吗?

我们让EF在单元测试中测试数据库,以便直接针对SQL测试数据库测试我们的业务逻辑.我根本没有看到模拟任何存储库模式的任何好处.针对测试数据库进行单元测试真的有什么问题?因为它是不可能的批量操作,我们最终编写原始SQL.内存中的SQLite是对真实数据库进行单元测试的理想选择.

不必要的抽象

您是否想要创建存储库以便将来可以轻松地用NHbibernate等替换EF或其他任何东西?听起来很棒,但它真的有成本效益吗?

Linq杀死单元测试?

我想看看它如何杀死的任何例子.

依赖注入,IoC

哇这些都是好话,确保它们在理论上看起来很棒,但有时候你必须在优秀的设计和出色的解决方案之间进行权衡.我们确实使用了所有这些,最后我们把所有东西扔进垃圾桶并选择不同的方法.尺寸与速度(代码大小和开发速度)在现实生活中非常重要.用户需要灵活性,他们不关心您的代码在DI或IoC方面的设计是否优秀.

除非您正在构建Visual Studio

如果要构建一个像Visual Studio或Eclipse这样的复杂程序,需要所有这些优秀的设计,这些程序将由许多人开发,并且需要高度可定制.经过多年的开发,这些IDE经历了所有伟大的开发模式,并且它们已经发展到所有这些伟大的设计模式都非常重要的地方.但是,如果您正在使用简单的基于Web的工资单或简单的业务应用程序,那么您最好随着时间的推移在开发中发展,而不是花费时间为数百万用户构建它,而只为100个用户部署它.

存储库作为过滤视图 - ISecureRepository

另一方面,存储库应该是EF的过滤视图,其通过基于当前用户/角色应用必要的填充来保护对数据的访问.

但这样做会使存储库变得更加复杂,因为它最终会在庞大的代码库中进行维护.人们最终会为不同的用户类型或实体类型组合创建不同的存储库.不仅如此,我们最终还有很多DTO.

以下答案是Filtered Repository的示例实现,无需创建整套类和方法.它可能不会直接回答问题,但它可能有助于推导出一个问题.

免责声明:我是Entity REST SDK的作者.

http://entityrestsdk.codeplex.com

牢记这一点,我们开发了一个SDK,它基于SecurityContext创建过滤视图的存储库,SecurityContext包含CRUD操作的过滤器.只有两种规则可以简化任何复杂的操作.首先是访问实体,其他是属性的读/写规则.

优点是,您不会为不同的用户类型重写业务逻辑或存储库,您只需阻止或授予它们访问权限.

public class DefaultSecurityContext : BaseSecurityContext {

  public static DefaultSecurityContext Instance = new DefaultSecurityContext();

  // UserID for currently logged in User
  public static long UserID{
       get{
             return long.Parse( HttpContext.Current.User.Identity.Name );
       }
  }

  public DefaultSecurityContext(){
  }

  protected override void OnCreate(){

        // User can access his own Account only
        var acc = CreateRules<Account>();

        acc.SetRead( y => x=> x.AccountID == UserID ) ;
        acc.SetWrite( y => x=> x.AccountID == UserID );

        // User can only modify AccountName and EmailAddress fields
        acc.SetProperties( SecurityRules.ReadWrite, 
              x => x.AccountName,
              x => x.EmailAddress);

        // User can read AccountType field
        acc.SetProperties<Account>( SecurityRules.Read, 
              x => x.AccountType);

        // User can access his own Orders only
        var order = CreateRules<Order>();
        order.SetRead( y => x => x.CustomerID == UserID );

        // User can modify Order only if OrderStatus is not complete
        order.SetWrite( y => x => x.CustomerID == UserID 
            && x.OrderStatus != "Complete" );

        // User can only modify OrderNotes and OrderStatus
        order.SetProperties( SecurityRules.ReadWrite, 
              x => x.OrderNotes,
              x => x.OrderStatus );

        // User can not delete orders
        order.SetDelete(order.NotSupportedRule);
  }
}
Run Code Online (Sandbox Code Playgroud)

对于每个操作,这些LINQ规则在SaveChanges方法中针对数据库进行评估,并且这些规则在数据库前充当防火墙.

  • 针对数据库的单元测试意味着您对测试有额外的外部要求.如果该数据库已关闭,或者数据被清除,或者该数据库发生任何事情,则测试将失败.这是不希望的.暴露IQueryable的存储库大约需要2分钟来设置.没有浪费时间在这里.为什么DI会带你很长时间?所有这些都需要几分钟.我会说这对我的服务层中的复杂查询进行单元测试非常有用.没有数据库可以连接,真是太好了.从nuget获得一个模拟框架花了大约一分钟.这个东西不需要任何时间. (2认同)

Jos*_*osh 6

关于哪种方法是正确的存在很多争论,所以我认为两者都是可以接受的,所以我使用哪一个我最喜欢的(哪个没有存储库,UoW).

在EF中,UoW通过DbContext实现,DbSets是存储库.

至于如何使用数据层我只是直接处理DbContext对象,对于复杂的查询,我将为可以重用的查询创建扩展方法.

我相信Ayende也有一些关于如何抽象出CUD操作不好的帖子.

我总是创建一个接口并让我的上下文继承它,所以我可以使用IoC容器进行DI.

  • 这一切都很好,直到域对象的属性需要由未存储在数据库中的数据来补充; 或者你需要深入研究一种比膨胀的ORM更高效的技术.OOPS!ORM根本不是存储库的替代品,它是一个实现细节. (3认同)