一、仓储层为什么容易成为"ORM直通车"
很多开发者在实现领域驱动设计(DDD)的仓储层时,常常犯一个致命错误——直接把ORM接口暴露给应用层。这就好比在高级餐厅里,厨师直接把生鲜食材端给顾客,还美其名曰"保持原汁原味"。
看看这个典型的错误示例(使用C#/.NET技术栈):
// 错误示范:直接暴露EF Core接口
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _dbContext;
public OrderRepository(AppDbContext dbContext)
{
_dbContext = dbContext;
}
// 直接把DbSet暴露给调用方
public IQueryable<Order> GetOrders()
{
return _dbContext.Orders;
}
// 调用方可以随意操作ORM实体
public void Update(Order order)
{
_dbContext.Entry(order).State = EntityState.Modified;
}
}
这种写法至少有三大问题:
- 调用方可以随意拼接查询条件,仓储层失去对查询的控制
- 领域逻辑泄露到应用层,破坏了分层架构
- ORM实体直接暴露,导致领域模型与数据模型耦合
二、如何正确封装仓储接口
正确的做法应该像高级餐厅的传菜流程:厨房(领域层)处理好食材后,由服务员(仓储层)以标准方式呈现给顾客(应用层)。
看这个改进后的示例:
// 正确示范:封装后的仓储接口
public interface IOrderRepository
{
// 返回领域对象而非ORM实体
Task<Order> GetByIdAsync(OrderId id);
// 使用规约模式封装查询条件
Task<IEnumerable<Order>> FindAsync(ISpecification<Order> spec);
// 只暴露必要的操作方法
Task AddAsync(Order order);
Task UpdateAsync(Order order);
}
实现这个接口时,我们需要做转换层:
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _dbContext;
private readonly IMapper _mapper; // 使用AutoMapper做对象转换
public async Task<Order> GetByIdAsync(OrderId id)
{
var orderEntity = await _dbContext.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == id.Value);
// 转换为领域模型
return orderEntity == null ? null : _mapper.Map<Order>(orderEntity);
}
public async Task AddAsync(Order order)
{
// 转换为持久化模型
var orderEntity = _mapper.Map<OrderEntity>(order);
await _dbContext.Orders.AddAsync(orderEntity);
}
}
三、常见问题及解决方案
1. 性能问题怎么解决?
很多开发者担心对象转换会影响性能。实际上可以通过以下方式优化:
// 使用Select投影避免不必要的数据加载
public async Task<IEnumerable<Order>> GetPendingOrders()
{
return await _dbContext.Orders
.Where(o => o.Status == OrderStatus.Pending)
.Select(o => new Order
{
Id = new OrderId(o.Id),
Total = o.TotalAmount,
// 只映射需要的字段
})
.ToListAsync();
}
2. 复杂查询怎么处理?
推荐使用规约模式:
// 定义规约接口
public interface ISpecification<T>
{
Expression<Func<T, bool>> Criteria { get; }
List<Expression<Func<T, object>>> Includes { get; }
}
// 实现一个订单规约
public class OrderByCustomerSpec : ISpecification<OrderEntity>
{
private readonly CustomerId _customerId;
public OrderByCustomerSpec(CustomerId customerId)
{
_customerId = customerId;
}
public Expression<Func<OrderEntity, bool>> Criteria =>
o => o.CustomerId == _customerId.Value;
public List<Expression<Func<OrderEntity, object>>> Includes =>
new() { o => o.Items, o => o.Payments };
}
// 仓储层使用规约
public async Task<IEnumerable<Order>> FindAsync(ISpecification<Order> spec)
{
var query = _dbContext.Orders.AsQueryable();
// 应用条件
if (spec.Criteria != null)
{
query = query.Where(spec.Criteria);
}
// 应用关联加载
foreach (var include in spec.Includes)
{
query = query.Include(include);
}
var entities = await query.ToListAsync();
return _mapper.Map<IEnumerable<Order>>(entities);
}
四、进阶技巧与最佳实践
1. 工作单元模式的应用
仓储层应该与工作单元(UoW)配合使用:
// 在应用层使用
public class OrderService
{
private readonly IOrderRepository _orderRepo;
private readonly IUnitOfWork _uow;
public async Task CancelOrder(OrderId orderId)
{
var order = await _orderRepo.GetByIdAsync(orderId);
order.Cancel();
// 只需调用一次提交
await _uow.CommitAsync();
}
}
2. 缓存策略的实现
可以在仓储层加入缓存逻辑:
public class CachedOrderRepository : IOrderRepository
{
private readonly IOrderRepository _inner;
private readonly IDistributedCache _cache;
public async Task<Order> GetByIdAsync(OrderId id)
{
var cacheKey = $"order_{id}";
var cached = await _cache.GetStringAsync(cacheKey);
if (cached != null)
{
return JsonSerializer.Deserialize<Order>(cached);
}
var order = await _inner.GetByIdAsync(id);
await _cache.SetStringAsync(cacheKey,
JsonSerializer.Serialize(order),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) });
return order;
}
}
五、总结与建议
- 仓储层应该作为领域模型与数据模型之间的防腐层
- 避免直接暴露IQueryable,改为使用规约模式
- 工作单元模式可以更好地管理事务
- 缓存等横切关注点可以通过装饰器模式实现
- 对象映射虽然有一定开销,但带来的架构收益更大
记住,好的仓储层设计应该像一堵墙,既保护领域模型不被数据访问细节污染,又为应用层提供清晰的契约。当你的仓储层开始看起来像ORM的简单包装时,就该停下来重新思考设计了。
评论