存储库模式(Repository Pattern)

1,054 阅读6分钟

翻译于Repository Pattern | DevIQ

存储库模式自 2004 年首次作为域驱动设计的一部分引入以来,已经获得了相当多的普及。从本质上讲,它提供了数据的抽象,以便您的应用程序可以使用具有近似集合接口的简单抽象。从此集合中添加、删除、更新和选择项是通过一系列简单的方法完成的,无需处理连接、命令、游标或读取器等数据库问题。使用此模式可以帮助实现松散耦合,并且可以使域对象的持久性保持无知。虽然这种模式非常受欢迎(或者可能正因为如此),但它也经常被误解和误用。有许多不同的方法可以实现存储库模式。让我们考虑其中的一些,以及它们的优点和缺点。

Ardalis.Specification

如果你正在考虑在 .NET 应用程序中实现存储库模式,尤其是在使用 EF 的情况下,请查看我的Ardalis.Specification 存储库NuGet 包。它可能提供了入门所需的一切,包括存储库实现(这是可选的)和对规范模式的支持,您应该强烈建议您考虑将其与存储库结合使用。

每个实体或业务对象的存储库

最简单的方法(尤其是对于现有系统)是为需要存储到持久性层或从持久性层检索的每个业务对象创建一个新的存储库实现。此外,只应实现在应用程序中调用的特定方法。避免创建必须为所有存储库实现的“标准”存储库类、基类或默认接口的陷阱。是的,如果您需要更新或删除方法,则应努力使其接口保持一致(Delete 是采用 ID,还是采用对象本身?),但不要在查找表存储库上实现您只会调用 List() 的 Delete 方法。这种方法的最大好处是YAGNI - 你不会浪费任何时间来实现永远不会被调用的方法。

通用存储库接口

另一种方法是继续为存储库创建一个简单的通用接口。您可以限制它使用哪种类型来成为某种类型,或者实现某种接口(例如,确保它具有Id属性,就像下面使用基类所做的那样)。通用 C# 存储库接口的示例可能是:

public interface IRepository<T> where T : EntityBase
{
    T GetById(int id);
    IEnumerable<T> List();
    IEnumerable<T> List(Expression<Func<T, bool>> predicate);
    void Add(T entity);
    void Delete(T entity);
    void Edit(T entity);
}

public abstract class EntityBase
{
   public int Id { get; protected set; }
}

此方法的优点是,它确保您具有用于处理任何对象的通用接口。您还可以通过使用通用存储库实现(见下文)来简化实现。请注意,采用谓词消除了返回 IQueryable 的需要,因为任何筛选器详细信息都可以传递到存储库中。但是,这仍然可能导致数据访问详细信息泄漏到调用代码中。如果遇到此问题,请考虑使用规范模式(如下所述)来缓解此问题。

通用存储库实现

假设您创建了一个通用存储库接口,您也可以以一般方式实现该接口。完成此操作后,您可以轻松地为任何给定类型创建存储库,而无需编写任何新代码,并且声明依赖项的类只需将 IRepository 指定为类型,并且您的 IoC 容器可以轻松地将其与存储库实现相匹配。您可以在此处查看使用实体框架的通用存储库实现示例。

public class Repository<T> : IRepository<T> where T : EntityBase
{
    private readonly ApplicationDbContext _dbContext;

    public Repository(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public virtual T GetById(int id)
    {
        return _dbContext.Set<T>().Find(id);
    }

    public virtual IEnumerable<T> List()
    {
        return _dbContext.Set<T>().AsEnumerable();
    }

    public virtual IEnumerable<T> List(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
    {
        return _dbContext.Set<T>()
               .Where(predicate)
               .AsEnumerable();
    }

    public void Insert(T entity)
    {
        _dbContext.Set<T>().Add(entity);
        _dbContext.SaveChanges();
    }

    public void Update(T entity)
    {
        _dbContext.Entry(entity).State = EntityState.Modified;
        _dbContext.SaveChanges();
    }

    public void Delete(T entity)
    {
        _dbContext.Set<T>().Remove(entity);
        _dbContext.SaveChanges();
    }
}

请注意,在此实现中,所有操作都在执行后保存;没有应用工作单元。有多种方法可以将工作单元行为添加到此实现中,其中最简单的方法是将显式 Save() 方法添加到 IRepository 方法,并且仅从此方法调用基础 SaveChanges() 方法。

IQueryable?

存储库的另一个常见问题与它们返回的内容有关。它们应该返回数据,还是应该返回可以在执行之前进一步优化的查询(IQueryable)?前者更安全,但后者提供了很大的灵活性。实际上,如果您选择IQueryable路线,则可以简化界面,仅提供一种读取数据的方法,因为从那里可以返回任意数量的项目。

这种方法的一个问题是,它往往会导致业务逻辑渗入更高的应用程序层,并在那里变得重复。如果返回有效客户的规则是他们没有被禁用,他们在去年购买了一些东西,那么最好使用一种方法ListValidCustomers()来封装此逻辑,而不是在对存储库的多个不同UI层引用中的lambda表达式中指定这些条件。实际应用程序中的另一个常见示例是使用“soft deletes”,该删除由实体上的 IsActive 或 IsDeleted 属性表示。删除项目后,在任何 UI 方案中,99% 的时间都应将其从显示中排除,因此几乎每个请求都将包含类似

.Where(foo => foo.IsActive)

除了存在的任何其他过滤器。这在存储库中更好地实现,它可以是 List() 方法的默认行为,或者 List() 方法可能会被重命名为类似于 ListActive() 之类的东西。如果确实有必要查看已删除/非活动的项目,则可以使用指定的 List 方法仅用于此目的(可能很少见)。

规范

遵循不公开 IQueryable 的建议的存储库通常会因许多自定义查询方法而变得臃肿。解决此问题的解决方案是使用规范设计模式将查询分离到它们自己的类型中。规范可以包括用于筛选查询的表达式、与此表达式关联的任何参数以及查询应返回的数据量(i.e. ".Include()" in EF/EF Core)。结合使用存储库和规范模式是确保在数据访问代码中遵循单一职责原则的好方法。请参阅有关如何在 C# 中实现通用存储库以及通用规范的示例。

引用