1. 为什么我们需要Repository Pattern
在软件开发中,数据访问层一直是个让人头疼的问题。我见过太多项目因为数据访问代码散落在业务逻辑中而变得难以维护。Repository Pattern(仓储模式)正是为了解决这个问题而生的。
想象一下这样的场景:你的业务代码中到处都是DbContext的直接调用,SQL查询和业务逻辑混杂在一起。当需要从SQL Server切换到MongoDB时,你得修改几十处代码。这就是典型的"紧耦合"问题,而Repository Pattern就是解耦的利器。
提示:Repository Pattern的核心价值在于它作为业务逻辑和数据源之间的中介层,让两者可以独立变化而不互相影响。
我在一个电商项目中就吃过这个亏。初期为了赶进度,直接在Controller里写数据访问代码。三个月后当需要添加缓存机制时,发现要修改的地方遍布整个项目。后来花了整整两周时间重构为Repository Pattern,才让项目重回正轨。
2. Repository Pattern的核心概念
2.1 基本架构解析
Repository Pattern的核心思想很简单:为每个聚合根(Aggregate Root)创建一个对应的Repository接口,然后在具体实现中封装所有数据访问细节。这样业务代码只需要和接口交互,完全不知道底层是用的EF Core、Dapper还是其他ORM。
典型的Repository接口长这样:
public interface IProductRepository { Product GetById(int id); IEnumerable<Product> GetAll(); void Add(Product product); void Update(Product product); void Delete(int id); // 特定业务查询 IEnumerable<Product> GetFeaturedProducts(); }2.2 与DAO模式的区别
很多刚接触Repository的开发者会问:这和DAO(Data Access Object)模式有什么区别?我在实际项目中总结出几个关键差异:
- 抽象层级不同:DAO更接近数据库表结构,而Repository更接近业务概念
- 操作粒度不同:DAO通常是CRUD操作,Repository可以包含复杂的业务查询
- 聚合边界不同:Repository以聚合根为单位组织数据访问,而DAO通常以单表为单位
2.3 工作单元(Unit of Work)模式
单独使用Repository还不够完美。当需要跨多个Repository保证事务一致性时,就需要引入Unit of Work模式。它就像一个"操作集合",确保所有修改要么全部提交,要么全部回滚。
public interface IUnitOfWork : IDisposable { IProductRepository Products { get; } IOrderRepository Orders { get; } int Complete(); } // 使用示例 using(var unit = new UnitOfWork()) { var product = unit.Products.GetById(1); product.Price += 10; unit.Complete(); // 提交所有修改 }3. 实战实现方案
3.1 基础实现(EF Core版)
让我们用EF Core实现一个具体的Repository。首先安装必要的NuGet包:
dotnet add package Microsoft.EntityFrameworkCore dotnet add package Microsoft.EntityFrameworkCore.SqlServer然后定义泛型Repository基类:
public class Repository<T> : IRepository<T> where T : class { protected readonly DbContext _context; protected readonly DbSet<T> _dbSet; public Repository(DbContext context) { _context = context; _dbSet = context.Set<T>(); } public virtual T GetById(int id) => _dbSet.Find(id); public virtual IEnumerable<T> GetAll() => _dbSet.ToList(); public virtual void Add(T entity) => _dbSet.Add(entity); public virtual void Update(T entity) { _dbSet.Attach(entity); _context.Entry(entity).State = EntityState.Modified; } public virtual void Delete(T entity) => _dbSet.Remove(entity); }3.2 特定业务Repository
对于有特殊业务逻辑的实体,可以继承基础Repository:
public class ProductRepository : Repository<Product>, IProductRepository { public ProductRepository(AppDbContext context) : base(context) { } public IEnumerable<Product> GetFeaturedProducts() { return _dbSet.Where(p => p.IsFeatured && p.Stock > 0) .OrderByDescending(p => p.CreatedDate) .Take(10) .ToList(); } }3.3 依赖注入配置
在ASP.NET Core中配置依赖注入:
services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); services.AddScoped<IProductRepository, ProductRepository>(); services.AddScoped<IUnitOfWork, UnitOfWork>();4. 高级应用场景
4.1 缓存集成
Repository是集成缓存的理想位置。我们可以实现一个带缓存的装饰器:
public class CachedProductRepository : IProductRepository { private readonly IProductRepository _inner; private readonly IMemoryCache _cache; public CachedProductRepository(IProductRepository inner, IMemoryCache cache) { _inner = inner; _cache = cache; } public Product GetById(int id) { string key = $"product_{id}"; return _cache.GetOrCreate(key, entry => { entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(5)); return _inner.GetById(id); }); } // 其他方法实现... }4.2 多数据源切换
Repository模式让切换数据源变得容易。比如从SQL Server切换到MongoDB:
public class MongoProductRepository : IProductRepository { private readonly IMongoCollection<Product> _collection; public MongoProductRepository(IMongoDatabase database) { _collection = database.GetCollection<Product>("products"); } public Product GetById(int id) { return _collection.Find(p => p.Id == id).FirstOrDefault(); } // 其他方法实现... }业务代码完全不需要知道底层数据源的变化。
4.3 查询规范(Specification)模式
对于复杂查询条件,可以引入Specification模式:
public interface ISpecification<T> { Expression<Func<T, bool>> Criteria { get; } List<Expression<Func<T, object>>> Includes { get; } } public class ProductByCategorySpec : ISpecification<Product> { public Expression<Func<Product, bool>> Criteria => p => p.CategoryId == _categoryId; public List<Expression<Func<Product, object>>> Includes => new() { p => p.Category }; private readonly int _categoryId; public ProductByCategorySpec(int categoryId) => _categoryId = categoryId; } // Repository中的使用方法 public IEnumerable<Product> Find(ISpecification<Product> spec) { var query = _dbSet.Where(spec.Criteria); return spec.Includes.Aggregate(query, (current, include) => current.Include(include)) .ToList(); }5. 常见陷阱与最佳实践
5.1 过度抽象问题
我看到很多团队犯的一个错误是过度抽象。比如为每个实体都创建Repository,即使它们没有任何特殊行为。这反而增加了不必要的复杂度。
经验法则:只有当实体有特殊查询需求时,才创建具体Repository类,否则使用泛型Repository即可。
5.2 性能注意事项
Repository模式虽然优雅,但也要注意性能:
- N+1查询问题:在GetAll()中默认不加载导航属性,按需使用Include
- 批量操作:添加批量插入/更新方法,避免循环调用Add
- 查询效率:复杂查询考虑直接使用原始SQL或存储过程
5.3 测试策略
Repository模式让单元测试变得容易。我们可以使用内存数据库或Mock:
// 使用内存数据库测试 var options = new DbContextOptionsBuilder<AppDbContext>() .UseInMemoryDatabase(databaseName: "TestDb") .Options; using var context = new AppDbContext(options); var repo = new ProductRepository(context); // 测试代码...5.4 与CQRS模式的结合
对于复杂系统,可以考虑将Repository与CQRS模式结合:
- 命令端:使用Repository处理写操作
- 查询端:直接使用Dapper或其他高效查询工具
这种混合架构既能保持写操作的封装性,又能获得查询的高性能。
6. 个人实战经验分享
在我主导的一个微服务项目中,我们采用了分层架构:
- API层:只处理HTTP请求和响应
- 应用层:协调业务流,调用领域服务
- 领域层:核心业务逻辑
- 基础设施层:Repository实现
这种架构下,Repository成为了领域层和基础设施层之间的契约。我们甚至能够在不修改任何业务代码的情况下,将部分数据从关系型数据库迁移到了Redis。
一个特别有用的技巧是"延迟加载代理"模式。我们为Repository接口创建了延迟加载的实现,只有在真正访问数据时才执行查询。这在处理复杂对象图时特别有用。
最后给一个实用建议:在项目初期,可以先用泛型Repository快速开发。当发现某些实体需要特殊处理时,再提取具体Repository类。这种"渐进式抽象"的策略能避免过早优化带来的复杂度。