101

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

现在这一切都始于对 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();
}

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

4

9 回答 9

111

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

异常编码

您是否为您的数据库从一种技术更改为另一种技术的 1% 机会编写代码?如果您正在考虑您的业务的未来状态并说是的,那么a)他们必须有很多钱来负担迁移到另一种数据库技术或b)您选择数据库技术是为了好玩或c ) 您决定使用的第一种技术出现了严重错误。

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

开发了 LINQ 和 EF,因此您可以用它做一些简洁的事情来读取和遍历对象图。创建和维护一个可以为您提供同样灵活性的存储库是一项艰巨的任务。根据我的经验,每当我创建存储库时,我总是将业务逻辑泄漏到存储库层中,以提高查询的性能和/或减少对数据库的访问次数。

我不想为我必须编写的查询的每一个排列创建一个方法。我还不如写存储过程。我不想要GetOrder, GetOrderWithOrderItem, GetOrderWithOrderItemWithOrderActivity, GetOrderByUserId, 等等...我只想获取主要实体并随意遍历并包含对象图。

大多数存储库的例子都是胡说八道

除非您正在开发像博客这样的非常简单的东西,否则您的查询永远不会像您在 Internet 上找到的围绕存储库模式的示例中的 90% 那样简单。我不能强调这一点!这是一个人必须在泥泞中爬行才能弄清楚的事情。总会有一个查询会破坏您创建的经过深思熟虑的存储库/解决方案,直到您再次猜测自己并开始技术债务/侵蚀的那一刻。

不要对我进行单元测试兄弟

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

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

使用存储库 - 您现在可以模拟您的存储库并对中间的层进行单元测试。很棒吧?不是真的...在上述情况下,您必须将逻辑泄漏到存储库层以提高查询的性能和/或减少对数据库的访问,您的单元测试如何涵盖这一点?它现在在 repo 层,你不想测试IQueryable<T>吗?老实说,您的单元测试不会涵盖具有 20 行.Where()子句的查询和.Include()'一堆关系并再次访问数据库以执行所有其他操作,等等,等等,因为查询是在运行时生成的。此外,由于您创建了一个存储库以保持对上层持久性的无知,如果您现在想要更改数据库技术,很抱歉您的单元测试绝对不能保证在运行时获得相同的结果,回到集成测试。所以存储库的整个点似乎很奇怪..

2 美分

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

于 2013-11-23T07:31:36.887 回答
50

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

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

更新以解决另一个答案

异常编码

使用存储库并不是为了能够切换持久性技术(即更改数据库或使用 Web 服务等)。它是关于将业务逻辑与持久性分离以降低复杂性和耦合性。

单元测试与集成测试

您不会为存储库编写单元测试。时期。

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

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

不同之处在于,如果您没有将业务与 LINQ 语句混合在一起,您可以 100% 确定失败的是您的持久性代码而不是其他原因。

如果您分析您的测试,您还会发现如果您没有混合关注点(即 LINQ + 业务逻辑),它们会更干净

存储库示例

大多数例子都是胡说八道。这是非常正确的。然而,如果你用谷歌搜索任何设计模式,你会发现很多蹩脚的例子。这不是避免使用模式的理由。

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

在您需要之前不要将任何东西添加到存储库类中

很多程序员都很懒惰,他们试图创建一个通用存储库并使用一个带有许多他们可能需要的方法的基类。雅尼。您编写存储库类一次,并在应用程序存在期间(可以是数年)保留它。何必因为懒惰而搞砸。在没有任何基类继承的情况下保持干净。它将使其更易于阅读和维护。

(上面的陈述是一个指导方针而不是法律。一个基类可以很好地被激励。在你添加它之前请三思而后行,这样你就可以有正确的理由添加它)

老东西

结论:

如果您不介意在业务代码中使用 LINQ 语句,也不关心单元测试,我认为没有理由不直接使用实体框架。

更新

我写过关于存储库模式和“抽象”真正含义的博客:http: //blog.gauffin.org/2013/01/repository-pattern-done-right/

更新 2

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

同样:具有 20 多个字段的实体被错误地建模。这是一个上帝实体。把它分解。

我并不是说那IQueryable不是为了查询而制作的。我是说它不适合像 Repository 模式这样的抽象层,因为它是泄漏的。没有 100% 完整的 LINQ To Sql 提供程序(如 EF)。

它们都有实现特定的东西,比如如何使用急切/延迟加载或如何执行 SQL“IN”语句。在存储库中公开IQueryable会迫使用户知道所有这些事情。因此,将数据源抽象出来的整个尝试是完全失败的。您只是增加了复杂性,而没有直接使用 OR/M 的任何好处。

要么正确实现存储库模式,要么根本不使用它。

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

于 2013-07-03T13:23:37.843 回答
29

IMORepository抽象和UnitOfWork抽象在任何有意义的开发中都具有非常有价值的位置。人们会争论实现细节,但正如给猫剥皮的方法有很多一样,实现抽象的方法也有很多。

您的问题是具体使用或不使用以及为什么。

毫无疑问,您已经意识到您已经将这两种模式都内置到了实体框架中,DbContext即是UnitOfWorkDbSetRepository. 您通常不需要对它们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抽象,那么答案是“添加装饰器很容易”</p>

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 您应该始终对将在多个地方引用的任何第 3 方组件进行简单的抽象。从这个角度来看,ORM 是完美的候选者,因为它在我们的很多代码中都被引用。

当有人说“我为什么要对这个或那个 3rd 方库进行抽象(例如)”时,通常会想到的答案Repository是“你为什么不呢?”</p>

使用 IoC 容器(例如SimpleInjector )应用 PS 装饰器非常简单。

[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>)));
        }
    }
}
于 2013-07-04T15:04:02.107 回答
12

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

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

我们让 EF 在单元测试中与测试数据库通信,以直接针对 SQL 测试数据库测试我们的业务逻辑。我根本看不到模拟任何存储库模式的任何好处。对测试数据库进行单元测试到底有什么问题?由于无法进行批量操作,因此我们最终编写了原始 SQL。内存中的 SQLite 非常适合对真实数据库进行单元测试。

不必要的抽象

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

Linq杀死单元测试?

我希望看到任何关于它如何杀死的例子。

依赖注入,IOC

哇,这些都是很棒的词,当然它们在理论上看起来很棒,但有时你必须在伟大的设计和伟大的解决方案之间做出权衡。我们确实使用了所有这些,最终我们把所有东西都扔进了垃圾箱并选择了不同的方法。大小与速度(代码大小和开发速度)在现实生活中非常重要。用户需要灵活性,他们不在乎您的代码在 DI 或 IoC 方面的设计是否出色。

除非您正在构建 Visual Studio

如果您正在构建像 Visual Studio 或 Eclipse 这样将由许多人开发并且需要高度可定制的复杂程序,则需要所有这些出色的设计。在这些 IDE 经历了多年的发展之后,所有伟大的开发模式都出现了,并且它们已经在所有这些伟大的设计模式如此重要的地方发展起来。但是,如果您正在做简单的基于 Web 的工资单或简单的商业应用程序,那么您最好随着时间的推移在开发中不断发展,而不是花时间为百万用户构建它,而它只会为 100 多个用户部署。

存储库作为过滤视图 - ISecureRepository

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

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

以下答案是过滤存储库的示例实现,无需创建整套类和方法。它可能无法直接回答问题,但在推导问题时可能很有用。

免责声明:我是 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 方法中针对每个操作的数据库进行评估,这些规则充当数据库前面的防火墙。

于 2013-07-08T08:46:24.713 回答
7

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

在 EF 中,UoW 是通过 DbContext 实现的,而 DbSet 是存储库。

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

我相信 Ayende 也有一些关于抽象 CUD 操作是多么糟糕的帖子。

我总是创建一个接口并让我的上下文从它继承,这样我就可以为 DI 使用 IoC 容器。

于 2013-01-01T13:50:22.517 回答
3

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

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

至此,EF 上的大多数“存储库”甚至都不是好的 Facade,因为它们只是非常直接地映射到 EF 中的单个方法,甚至具有相同的签名。

那么,在 EF 上应用这种所谓的“存储库”模式的两个原因是允许更轻松的测试并建立一个“罐头”调用的子集。本身还不错,但显然不是存储库。

于 2016-05-05T07:09:06.353 回答
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);
}

可能添加带有回调的异步方法作为委托。回购很容易实现一般,所以我不能触及从应用程序到应用程序的实现线。好吧,至少在使用 NH 时是这样,我也用 EF 做到了,但让我讨厌 EF。4. 对话是交易的开始。如果几个类共享存储库实例,那就太酷了。此外,对于 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 查询,您必须在抽象库中拥有上下文,而不是会话。但当然 ifc 也是一样的。

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

总而言之,这种方法提供了很多代码重用和简单性,但我无法想象。有任何想法吗?

非常感谢 Ayende 的出色职位和持续的奉献精神。这是他的想法(查询对象),而不是我的。

于 2013-07-03T13:16:09.503 回答
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>();
    }
}
于 2013-01-02T10:15:24.240 回答
1

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

  1. 存储库用于域类。
  2. 在我的一些应用程序中,域类与我的持久性 (DAL) 类相同,而在其他应用程序中则不然。
  3. 当它们相同时,EF 已经为我提供了存储库。
  4. EF 提供延迟加载和 IQueryable。我喜欢这些。
  5. 通过 EF 抽象/“立面”/重新实现存储库通常意味着丢失惰性和 IQueryable

所以,如果我的应用程序不能证明#2 的合理性,分离域和数据模型,那么我通常不会为#5 烦恼。

于 2016-09-04T09:59:56.700 回答