创建通用存储库与为每个对象创建特定存储库的优势是什么?

139
我们正在开发一个ASP.NET MVC应用程序,现在正在构建存储库/服务类。我想知道创建所有存储库实现的通用IRepository接口是否有任何主要优点,而不是每个存储库都有自己独特的接口和一组方法。
例如:通用的IRepository接口可能如下所示(取自这个答案):
public interface IRepository : IDisposable
{
    T[] GetAll<T>();
    T[] GetAll<T>(Expression<Func<T, bool>> filter);
    T GetSingle<T>(Expression<Func<T, bool>> filter);
    T GetSingle<T>(Expression<Func<T, bool>> filter, List<Expression<Func<T, object>>> subSelectors);
    void Delete<T>(T entity);
    void Add<T>(T entity);
    int SaveChanges();
    DbTransaction BeginTransaction();
}

每个存储库都会实现这个接口,例如:

  • CustomerRepository:IRepository
  • ProductRepository:IRepository
  • 等等。

我们在以前的项目中遵循的替代方案是:

public interface IInvoiceRepository : IDisposable
{
    EntityCollection<InvoiceEntity> GetAllInvoices(int accountId);
    EntityCollection<InvoiceEntity> GetAllInvoices(DateTime theDate);
    InvoiceEntity GetSingleInvoice(int id, bool doFetchRelated);
    InvoiceEntity GetSingleInvoice(DateTime invoiceDate, int accountId); //unique
    InvoiceEntity CreateInvoice();
    InvoiceLineEntity CreateInvoiceLine();
    void SaveChanges(InvoiceEntity); //handles inserts or updates
    void DeleteInvoice(InvoiceEntity);
    void DeleteInvoiceLine(InvoiceLineEntity);
}

在第二种情况下,表达式(LINQ或其他)将完全包含在存储库实现中,无论谁实现服务,只需要知道调用哪个存储库函数即可。
我想我没有看到在服务类中编写所有表达式语法并传递给存储库的优势。这是否意味着易于出错的LINQ代码在许多情况下都被复制了?
例如,在我们旧的发票系统中,我们调用
InvoiceRepository.GetSingleInvoice(DateTime invoiceDate, int accountId)

从几个不同的服务中获取数据(客户、发票、账户等)。这比在多个地方编写以下内容要清晰得多:

rep.GetSingle(x => x.AccountId = someId && x.InvoiceDate = someDate.Date);

我认为使用特定方法的唯一缺点是我们可能会得到许多Get *函数的排列组合,但这仍然比将表达式逻辑推入Service类更可取。
我错过了什么?

使用带有完整ORM的通用存储库看起来毫无用处。我在这里详细讨论了这个问题(链接:https://dev59.com/B1QK5IYBdhLWcg3wev6V#51781877)。 - Amit Joshi
5个回答

178

这是一个与仓储模式本身一样古老的问题。LINQ中引入了IQueryable——一个查询的统一表示形式,这引起了对这个主题的大量讨论。

个人而言,我更喜欢具体的仓储,尽管我曾经努力构建过通用仓储框架。无论我尝试了多么巧妙的机制,最终都遇到了同样的问题:仓储是被建模的领域的一部分,而该领域并非通用的。并非每个实体都可以被删除、添加,也不是每个实体都有一个仓储。查询方式各不相同;仓储API变得与实体本身一样独特。

我经常使用的一种模式是具有特定的仓储接口,但实现上却拥有一个基类。例如,使用LINQ to SQL,你可以这样操作:

public abstract class Repository<TEntity>
{
    private DataContext _dataContext;

    protected Repository(DataContext dataContext)
    {
        _dataContext = dataContext;
    }

    protected IQueryable<TEntity> Query
    {
        get { return _dataContext.GetTable<TEntity>(); }
    }

    protected void InsertOnCommit(TEntity entity)
    {
        _dataContext.GetTable<TEntity>().InsertOnCommit(entity);
    }

    protected void DeleteOnCommit(TEntity entity)
    {
        _dataContext.GetTable<TEntity>().DeleteOnCommit(entity);
    }
}

用你选择的工作单元替换 DataContext。一个示例实现可能是:

public interface IUserRepository
{
    User GetById(int id);

    IQueryable<User> GetLockedOutUsers();

    void Insert(User user);
}

public class UserRepository : Repository<User>, IUserRepository
{
    public UserRepository(DataContext dataContext) : base(dataContext)
    {}

    public User GetById(int id)
    {
        return Query.Where(user => user.Id == id).SingleOrDefault();
    }

    public IQueryable<User> GetLockedOutUsers()
    {
        return Query.Where(user => user.IsLockedOut);
    }

    public void Insert(User user)
    {
        InsertOnCommit(user);
    }
}

请注意,仓库的公共API不允许删除用户。此外,暴露IQueryable是一个完全不同的问题 - 在这个话题上有很多意见,就像肚脐眼一样。


5
那么你会如何使用IoC/DI?(我对IoC还是个新手)我的问题涉及到你提供的完整模式:http://stackoverflow.com/questions/4312388/how-should-i-use-ioc-di-with-this-repository-pattern - dan
40
"仓库(repository)是被建模领域的一部分,而这个领域并不是通用的。并非每个实体都可以被删除,也不是每个实体都可以被添加,也不是每个实体都有一个仓库。" - adamwtiko
我知道这是一个旧答案,但我很好奇是否有意从Repository类中省略Update方法。我很难找到一种干净的方法来做到这一点。 - rtf
@Tanner:更新是隐式的 - 当您修改跟踪对象时,然后提交“DataContext”,LINQ to SQL会发出相应的命令。 - Bryan Watts
1
老而弥坚。这篇文章非常明智,值得所有富有的开发者阅读和反复阅读。感谢@BryanWatts。我的实现通常是基于Dapper的,但前提是相同的。基础存储库,具体的存储库代表领域,选择加入功能。 - pim

28

我其实在某些方面不太同意Bryan的观点。我认为他是对的,最终每个东西都是独特的等等。但同时,大多数设计出来的内容都是通用的,我发现使用一个通用存储库,在开发模型时可以快速搭建应用程序,然后在需要时进行重构以获得更大的特定性。

因此,在这种情况下,我经常创建一个具有完整CRUD堆栈的通用IRepository,这使我能够快速尝试API并让人们玩弄UI和进行集成和用户验收测试。然后,当我发现需要特定查询时,我开始将该依赖项替换为特定的依赖项,并从那里开始。

总的来说,创建一个底层的实现很容易(可能钩住内存数据库或静态对象或模拟对象或其他任何东西)。

话虽如此,我最近开始分离行为。比如,如果你为IDataFetcher、IDataUpdater、IDataInserter和IDataDeleter创建接口,你可以通过接口混合使用来定义要求,然后有一些或所有实现可以处理它们,而我仍然可以注入“做一切”实现,以便在构建应用程序时使用。

保罗


4
谢谢您的回复,@Paul。实际上我也尝试了那种方法。我无法想出如何泛化表达我尝试过的第一种方法 GetById()。我应该使用 IRepository<T, TId>GetById(object id) 还是假设并使用 GetById(int id)?复合键会如何运作?我想知道是否泛型选择 ID 是一个值得的抽象。如果不是,泛型仓库将被迫表达哪些笨拙的内容呢?这就是在抽象“实现”而不是“接口”后面的推理线索。 - Bryan Watts
11
同时,通用的查询机制是ORM的职责。您的仓储应该通过“使用”通用查询机制来实现项目实体的特定查询。除非这是您问题域的一部分(例如报告),否则不应强制仓库的消费者编写自己的查询。 - Bryan Watts
2
@Bryan - 关于你的GetById()。我使用FindById<T, TId>(TId id); 因此得到类似repository.FindById<Invoice, int>(435);的结果。 - Joshua Hayes
我通常不会在通用接口上放置字段级别的查询方法,坦率地说。正如你所指出的,不是所有的模型都应该通过单个键进行查询,在某些情况下,您的应用程序根本不会通过ID检索任何内容(例如,如果您使用DB生成的主键,并且只通过自然键获取,例如登录名)。查询方法会随着重构的一部分而在特定接口上演变。 - Paul

13

我更喜欢具体的仓库,它们派生自通用的仓库(或者列出通用的仓库以指定确切的行为),并具有可覆盖的方法签名。


请提供一个简短的示例代码,好吗? - Johann Gerell
@Johann Gerell 不,因为我不再使用存储库。 - Arnis Lapsa
你现在使用什么来避免使用代码库? - Chris
@Chris 我非常注重拥有丰富的领域模型。应用程序的输入部分是最重要的。只要所有状态变化都被仔细监控,读取数据的方式并不那么重要,只要足够高效。因此,我直接使用 NHibernate ISession,没有仓储层抽象,这样就更容易指定像急切加载、多查询等内容。如果你真的需要,也不难模拟出ISession。 - Arnis Lapsa
@Arnis - 一开始这样做可能会更容易/更快/更简单,但是当您第一次需要在域模型的多个位置重复使用任何查询逻辑时,它很快就会成为一个问题。将存储库层与业务域分开但由其使用使得轻松地将其用于其他事情。尽管我承认,对于较小的系统,这种简化可能值得冒险,因为很有可能一开始不会有太多或任何重复使用。 - Jesse Webb
4
@JesseWebb 没有...使用丰富的领域模型,查询逻辑会显着简化。例如,如果我想搜索已经购买过物品的用户,我只需搜索 users.Where(u=>u.HasPurchasedAnything),而不是 users.Join(x=>Orders, something something, I dont know linq).Where(order=>order.Status==1).Join(x=>x.products).Where(x.... 等等等等...废话废话废话。 - Arnis Lapsa

5

可以使用一个通用的存储库,由特定的存储库进行包装。这样,您可以控制公共接口,同时仍然具有从通用存储库中获得代码重用的优势。


3

公共类UserRepository : Repository, IUserRepository

你是否应该注入IUserRepository以避免暴露接口。就像其他人说的那样,你可能不需要完整的CRUD堆栈等。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接