一、仓储层为什么容易成为"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;
    }
}

这种写法至少有三大问题:

  1. 调用方可以随意拼接查询条件,仓储层失去对查询的控制
  2. 领域逻辑泄露到应用层,破坏了分层架构
  3. 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;
    }
}

五、总结与建议

  1. 仓储层应该作为领域模型与数据模型之间的防腐层
  2. 避免直接暴露IQueryable,改为使用规约模式
  3. 工作单元模式可以更好地管理事务
  4. 缓存等横切关注点可以通过装饰器模式实现
  5. 对象映射虽然有一定开销,但带来的架构收益更大

记住,好的仓储层设计应该像一堵墙,既保护领域模型不被数据访问细节污染,又为应用层提供清晰的契约。当你的仓储层开始看起来像ORM的简单包装时,就该停下来重新思考设计了。