一、为什么我们需要通用仓储接口

在软件开发中,数据访问层(DAL)是连接业务逻辑和数据库的桥梁。传统的做法是,每个项目都针对不同的数据库表写一套CRUD(增删改查)操作,导致大量重复代码。比如,用户表的查询、订单表的插入、商品表的更新……这些操作虽然具体业务逻辑不同,但底层的数据访问模式却高度相似。

这时候,如果能设计一套通用的仓储接口,就能大幅减少重复代码。举个例子,假设我们使用C#和Entity Framework Core(技术栈示例),传统的仓储层可能是这样的:

// 传统仓储实现 - 用户仓储
public class UserRepository
{
    private readonly AppDbContext _dbContext;

    public UserRepository(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<User> GetByIdAsync(int id)
    {
        return await _dbContext.Users.FindAsync(id);
    }

    public async Task AddAsync(User user)
    {
        await _dbContext.Users.AddAsync(user);
        await _dbContext.SaveChangesAsync();
    }

    // 其他方法:Update, Delete, GetAll...
}

每张表都要写一个类似的仓储类,显然不够优雅。

二、通用仓储接口的设计思路

通用仓储的核心思想是抽象出共性的数据访问操作,让具体实现由底层框架完成。我们可以定义一个泛型接口:

// 通用仓储接口定义
public interface IRepository<T> where T : class
{
    Task<T> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
    Task AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(T entity);
    // 可以扩展更多通用方法,如条件查询
}

然后,基于Entity Framework Core实现这个接口:

// 通用仓储实现
public class Repository<T> : IRepository<T> where T : class
{
    private readonly DbContext _dbContext;
    private readonly DbSet<T> _dbSet;

    public Repository(DbContext dbContext)
    {
        _dbContext = dbContext;
        _dbSet = dbContext.Set<T>();
    }

    public async Task<T> GetByIdAsync(int id)
    {
        return await _dbSet.FindAsync(id);
    }

    public async Task<IEnumerable<T>> GetAllAsync()
    {
        return await _dbSet.ToListAsync();
    }

    // 其他方法的实现...
}

这样一来,所有实体类的仓储都可以复用同一套代码。

三、进阶技巧:支持复杂查询和Unit of Work

单纯的CRUD还不够,实际业务中我们经常需要复杂查询。这时候可以结合IQueryable和规约模式(Specification Pattern)来增强通用仓储:

// 扩展的通用仓储接口
public interface IRepository<T> where T : class
{
    // ...原有基础方法

    // 新增:支持动态查询
    IQueryable<T> Query(Expression<Func<T, bool>> predicate);
    
    // 新增:支持规约模式
    Task<IEnumerable<T>> FindAsync(ISpecification<T> spec);
}

对应的实现:

public class Repository<T> : IRepository<T> where T : class
{
    // ...原有实现

    public IQueryable<T> Query(Expression<Func<T, bool>> predicate)
    {
        return _dbSet.Where(predicate);
    }

    public async Task<IEnumerable<T>> FindAsync(ISpecification<T> spec)
    {
        return await ApplySpecification(spec).ToListAsync();
    }

    private IQueryable<T> ApplySpecification(ISpecification<T> spec)
    {
        return SpecificationEvaluator<T>.GetQuery(_dbSet, spec);
    }
}

Unit of Work模式则可以确保多个仓储共享同一个DbContext,保证事务一致性:

// Unit of Work接口
public interface IUnitOfWork : IDisposable
{
    IRepository<T> GetRepository<T>() where T : class;
    Task<int> CommitAsync();
}

// 实现
public class UnitOfWork : IUnitOfWork
{
    private readonly DbContext _dbContext;
    private Dictionary<Type, object> _repositories;

    public UnitOfWork(DbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public IRepository<T> GetRepository<T>() where T : class
    {
        _repositories ??= new Dictionary<Type, object>();
        
        if (!_repositories.ContainsKey(typeof(T)))
        {
            _repositories[typeof(T)] = new Repository<T>(_dbContext);
        }
        
        return (IRepository<T>)_repositories[typeof(T)];
    }

    public async Task<int> CommitAsync()
    {
        return await _dbContext.SaveChangesAsync();
    }
}

四、实际应用场景与注意事项

适用场景:

  1. 中小型项目,实体类CRUD操作标准化程度高
  2. 需要快速开发原型,减少重复编码
  3. 多数据库支持(通过更换DbContext实现)

优缺点分析:

优点

  • 代码复用率高,新实体只需定义模型,无需编写仓储类
  • 统一数据访问模式,团队协作更规范
  • 易于扩展,如添加缓存、日志等横切关注点

缺点

  • 复杂业务查询可能需要特殊处理,不适合所有场景
  • 过度抽象可能影响性能(如不恰当的IQueryable使用)

注意事项:

  1. 谨慎暴露IQueryable,避免服务层出现复杂查询逻辑
  2. 考虑引入缓存层(如Redis)提升高频查询性能
  3. 对于超大规模数据,可能需要特殊分页/批量处理优化

五、总结

通用仓储接口通过合理的抽象,能显著提升仓储层代码的复用率。结合Entity Framework Core等技术栈,我们可以用不到200行核心代码实现一个生产可用的通用数据访问层。关键在于平衡通用性与灵活性——既要避免重复造轮子,又要为特殊业务需求留出扩展空间。