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

时间:2013-01-01 13:31:04

标签: entity-framework design-patterns architecture aop repository-pattern

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

现在这一切都从Stackoverflow上的几条评论开始,跟踪Ayende Rahien在他博客上的帖子,具体为2,

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

  1. 这种方法是否适合实体框架项目?
  2. 使用这种方法是业务逻辑仍然在服务层或扩展方法(如下所述,我知道,扩展方法是使用NHib会话)?
  3. 使用扩展方法很容易完成。干净,简单且可重复使用。

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

    使用这种方法和Ninject作为DI,我是否需要创建Context接口并将其注入我的控制器中?

9 个答案:

答案 0 :(得分:94)

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

编码异常

您是否编码了数据库从一种技术更改为另一种技术的可能性为1%?如果您正在考虑您的业务的未来状态,并说“是”那么可能性a)他们必须有很多钱才能负担得起迁移到其他数据库技术或b)您&# 39;重新选择数据库技术以获得乐趣,或者c)您决定使用的第一项技术出现了严重错误。

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

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

我不想为我必须编写的查询的每个单独排列创建一个方法。我不妨编写存储过程。我不想要GetOrderGetOrderWithOrderItemGetOrderWithOrderItemWithOrderActivityGetOrderByUserId等等...我只想获取主要实体并遍历并包含对象图如我所愿。

大多数存储库示例都是废话

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

不要对我进行单元测试

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

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

使用存储库 - 您现在可以模拟存储库并对两者之间的层进行单元测试。好吧?好吧不是真的...在上面的情况下,你必须将逻辑泄漏到存储库层以使查询更高性能和/或更少的数据库命中,你的单元测试如何覆盖?现在它位于repo层,你不想对IQueryable<T>进行测试吗?另外,老实说,您的单元测试不会涵盖具有20行.Where()条款和.Include()一串关系的查询,并且会触及数据库再次做所有这些其他的东西,等等,等等,因为查询是在运行时生成的。此外,由于您创建了一个存储库来保持上层持久性无知,如果您现在想要更改数据库技术,抱歉您的单元测试肯定不会保证在运行时获得相同的结果,回到集成测试。所以存储库的重点似乎很奇怪..

2美分

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

答案 1 :(得分:47)

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

问题在于许多开发人员无法理解模式的目的并创建存储库,这些存储库会将持久性特定信息泄露给调用者(通常通过公开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 pattern结合起来。这为你提供了一个完整的抽象,也是可以测试的。)

答案 2 :(得分:24)

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();
}

及其实施:

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();
    }
}

到目前为止没有任何异常,但现在我们想添加一些日志记录 - 使用日志记录装饰器很容易。

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();
    }
}

全部完成,,不更改现有代码。我们可以添加许多其他交叉问题,例如异常处理,数据缓存,数据验证或其他任何问题以及整个设计和构建过程我们拥有的最有价值的东西,使我们能够添加简单的功能而无需更改任何我们现有的代码是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);
    }
}

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

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

P.S。装饰器使用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>)));
        }
    }
}

答案 3 :(得分:11)

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

单元测试的可模拟存储库,我们真的需要它吗?

我们让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创建过滤视图的存储库,该文件保存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);
  }
}

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

答案 4 :(得分:6)

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

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

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

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

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

答案 5 :(得分:1)

Linq是一个时下的“知识库”。

ISession + Linq已经是存储库,您既不需要GetXByY方法也不需要QueryData(Query q)概括。对于DAL使用有点偏执,我仍然更喜欢存储库接口。 (从可维护性的角度来看,我们还必须在特定的数据访问接口上有一些外观)。

这是我们使用的存储库 - 它将我们与nhibernate的直接使用联系起来,但提供了linq接口(在特殊情况下作为ISession访问,最终会受到重构)。

class Repo
{
    ISession _session; //via ioc
    IQueryable<T> Query()
    {
        return _session.Query<T>();
    }
}

答案 6 :(得分:1)

此时存储库(或者选择调用它)对我来说主要是抽象掉持久层。

我将它与查询对象结合使用,因此我的应用程序中没有与任何特定技术的耦合。而且它也很容易进行测试。

所以,我倾向于

public interface IRepository : IDisposable
{
    void Save<TEntity>(TEntity entity);
    void SaveList<TEntity>(IEnumerable<TEntity> entities);

    void Delete<TEntity>(TEntity entity);
    void DeleteList<TEntity>(IEnumerable<TEntity> entities);

    IList<TEntity> GetAll<TEntity>() where TEntity : class;
    int GetCount<TEntity>() where TEntity : class;

    void StartConversation();
    void EndConversation();

    //if query objects can be self sustaining (i.e. not need additional configuration - think session), there is no need to include this method in the repository.
    TResult ExecuteQuery<TResult>(IQueryObject<TResult> query);
}

可能使用回调作为委托添加异步方法。 回购很容易实现一般,所以我无法触及从app到app的实现。好吧,至少在使用NH时这是真的,我也用EF做过,但让我讨厌EF。对话是交易的开始。如果几个类共享存储库实例,那就太酷了。此外,对于NH,我的实现中的一个repo等于在第一次请求时打开的一个会话。

然后查询对象

public interface IQueryObject<TResult>
{
    /// <summary>Provides configuration options.</summary>
    /// <remarks>
    /// If the query object is used through a repository this method might or might not be called depending on the particular implementation of a repository.
    /// If not used through a repository, it can be useful as a configuration option.
    /// </remarks>
    void Configure(object parameter);

    /// <summary>Implementation of the query.</summary>
    TResult GetResult();
}

对于我在NH中使用的配置,只传入ISession。在EF中,或多或少没有任何意义。

示例查询将是..(NH)

public class GetAll<TEntity> : AbstractQueryObject<IList<TEntity>>
    where TEntity : class
{
    public override IList<TEntity> GetResult()
    {
        return this.Session.CreateCriteria<TEntity>().List<TEntity>();
    }
}

要执行EF查询,您必须在Abstract基础中使用上下文,而不是会话。但当然ifc会是一样的。

通过这种方式,查询本身就被封装,并且可以轻松测试。最重要的是,我的代码仅依赖于接口。一切都很干净。域(业务)对象就是这样,例如没有混合的责任,比如使用几乎不可测试的活动记录模式,并混合域对象中的数据访问(查询)代码,这样做是混合问题(取自己的对象??)。每个人都可以自由地创建用于数据传输的POCO。

总而言之,这种方法提供了大量代码重用和简单性,而不是我能想象到的任何东西。有什么想法吗?

非常感谢Ayende的伟大职位和持续的奉献精神。它的想法在这里(查询对象),而不是我的。

答案 7 :(得分:1)

最适用于EF的不是存储库模式。它是一个Facade模式(将对EF方法的调用抽象为更简单,更易于使用的版本)。

EF是应用存储库模式(以及工作单元模式)的模式。也就是说,EF是抽象数据访问层的那个,因此用户不知道他们正在处理SQLServer。

就此而言,大多数&#34;存储库&#34;过EF甚至不是很好的外观,因为他们只是非常直接地映射到EF中的单个方法,甚至到了具有相同签名的点。

因此,应用这个所谓的&#34;存储库&#34; EF上的模式是为了更容易测试并建立一个&#34; canned&#34;打电话给它。本身并不坏,但显然不是存储库。

答案 8 :(得分:1)

对我来说,这是一个简单的决定,因素相对较少。因素是:

  1. 存储库适用于域类。
  2. 在我的一些应用中,域类与我的持久性(DAL)类相同,而在其他应用中则不是。
  3. 当它们相同时,EF已经为我提供了存储库。
  4. EF提供延迟加载和IQueryable。我喜欢这些。
  5. 通过EF提取/'实施'/重新实施存储库通常意味着丢失懒惰和可以使用的
  6. 所以,如果我的应用程序无法证明#2,单独的域和数据模型,那么我通常不会打扰#5。