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


132

我们正在开发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();
}

每个存储库都将实现此接口,例如:

  • 客户资料库:IRepository
  • 产品存储库: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一起使用似乎毫无用处。我已经在这里详细讨论
阿米特·乔希

Answers:


169

这是与存储库模式本身一样古老的问题。LINQ's 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是蠕虫的另一种可能-关于该主题的观点与肚脐一样多。


9
说真的,很好的答案。谢谢!
哔哔

5
那么您将如何与此一起使用IoC / DI?(我在国际奥委会的新手)我的问题在全问候你的模式:stackoverflow.com/questions/4312388/...

36
“信息库是要建模的域的一部分,并且该域不是通用的。不是每个实体都可以删除,不是每个实体都可以添加,不是每个实体都有一个信息库”完美!
adamwtiko

我知道这是一个老答案,但我很好奇是否故意从Repository类中删除了Update方法。我很难找到一种干净的方法来做到这一点。
rtf 2015年

1
老歌,但是好东西。这篇文章非常明智,应该阅读和重新阅读,但所有有经验的开发人员都可以。谢谢@BryanWatts。我的实现通常基于dapper,但前提是相同的。基本存储库,带有特定的存储库以表示选择使用功能的域。
pimbrouwers

27

我实际上不同意Bryan的帖子。我认为他是对的,最终所有东西都是非常独特的,依此类推。但是,与此同时,大部分都是在您设计时出现的,我发现在建立模型时建立通用存储库并使用它,我可以非常快速地建立一个应用程序,然后在发现需要这样做。

因此,在这种情况下,我经常创建具有完整CRUD堆栈的通用IRepository,这使我可以快速使用API​​,并让人们在使用UI的同时进行集成和用户接受度测试。然后,当我发现我需要对存储库进行特定查询等时,如果需要,我开始用特定的替换该依赖,然后从那里开始。一个潜在的暗示。易于创建和使用(并可能挂钩到内存中的db或静态对象或模拟对象等)。

就是说,我最近开始做的就是打破这种行为。因此,如果您为IDataFetcher,IDataUpdater,IDataInserter和IDataDeleter做接口(例如),则可以通过接口混合匹配以定义您的需求,然后使用实现来解决其中的部分或全部问题,我可以在我构建应用程序时,仍然注入要使用的全能实施。

保罗


4
感谢@Paul的回复。我实际上也尝试过这种方法。我不知道该如何概括地表达我尝试过的第一种方法GetById()。我应该使用IRepository<T, TId>GetById(object id)或做出假设和使用GetById(int id)?复合键如何工作?我想知道按ID进行的通用选择是否值得抽象。如果没有,通用存储库还会被迫笨拙地表达什么?那是抽象实现而不是接口背后的推理路线。
布莱恩·瓦茨

10
同样,通用查询机制是ORM的职责。您的存储库应该通过使用通用查询机制来实现针对项目实体的特定查询。除非这是问题域的一部分,例如报告,否则不应强迫存储库的使用者编写自己的查询。
Bryan Watts'4

2
@Bryan-关于您的GetById()。我使用FindById <T,TId>(TId id); 因此,结果类似于repository.FindById <Invoice,int>(435);。
约书亚·海斯

坦率地说,我通常不将字段级查询方法放在通用接口上。正如您所指出的,并非所有模型都应通过单个键查询,并且在某些情况下,您的应用程序根本不会通过ID检索某些内容(例如,如果您使用的是DB生成的主键,而您只能通过自然键,例如登录名)。查询方法在我作为重构的一部分构建的特定接口上发展。
保罗2010年

13

我更喜欢从具有可覆盖方法签名的通用存储库(或用于指定确切行为的通用存储库列表)派生的特定存储库。


您能否提供一个简短的示例示例?
Johann Gerell 2011年

@Johann Gerell不,因为我不再使用存储库。
2011年

远离存储库,您现在使用什么?
克里斯(Chris

@Chris我非常关注具有丰富的域模型。应用程序的输入部分很重要。如果仔细监视所有状态变化,那么只要数据足够有效,读取数据的方式就无关紧要。所以我只是直接使用NHibernate ISession。如果没有存储库层抽象,那么指定诸如紧急加载,多查询等内容的方法就容易得多,而且如果您确实需要,也可以轻松模拟出ISession。
2011年

3
@JesseWebb Naah ...凭借丰富的域模型,查询逻辑得到了显着简化。例如,如果我想搜索已购买商品的用户,我只是搜索users.Where(u => u.HasPurchasedAnything)而不是users.Join(x => Orders,有些东西,我不知道linq).Where( order => order.Status == 1).Join(x => x.products).Where(x .... etc etc etc ....等等等等等等
Arnis Lapsa 2011年

5

具有由特定存储库包装的通用存储库。这样,您可以控制公共接口,但仍具有通用存储库带来的代码重用的优势。


2

公共类UserRepository:存储库,IUserRepository

您不应该注入IUserRepository以避免暴露该接口。正如人们所说,您可能不需要完整的CRUD堆栈等。


2
这更像是评论而不是答案。
阿米特·乔希
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.