不同存储库的事务范围 类

Transaction Scope for different repository classes

我正在尝试围绕发生在不同存储库 classes 中的 2 个或更多数据库操作包装事务。每个存储库 class 使用一个 DbContext 实例,使用依赖注入。我正在使用 Entity Framework Core 2.1.

public PizzaService(IPizzaRepo pizzaRepo, IPizzaIngredientsRepo ingredientRepo)
{
    _pizzaRepo = pizzaRepo;
    _ingredientRepo = ingredientRepo;
}

public async Task SavePizza(PizzaViewModel pizza)
{
    using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
    {
        int pizzaRows = await _pizzaRepo.AddEntityAsync(pizza.Pizza);
        int ingredientRows = await _ingredientRepo.PutIngredientsOnPizza(
            pizza.Pizza.PizzaId,
            pizza.Ingredients.Select(x => x.IngredientId).ToArray());

        scope.Complete();
    }
}

}

显然,如果其中一个操作失败,我想回滚整个操作。 这个事务范围是否足以回滚,或者存储库 classes 是否应该有自己的事务?

即使上述方法可行,还有更好的方法来实现交易吗?

存储库模式非常适合启用测试,但没有新的存储库 DbContext,跨存储库共享上下文。

作为一个简单的示例(假设您使用的是 DI/IoC)

DbContext 已在您的 IoC 容器中注册,其生命周期范围为 Per Request。所以在服务调用开始时:

public PizzaService(PizzaDbContext context, IPizzaRepo pizzaRepo, IPizzaIngredientsRepo ingredientRepo)
{
  _context = pizzaContext;
  _pizzaRepo = pizzaRepo;
  _ingredientRepo = ingredientRepo;
}

public async Task SavePizza(PizzaViewModel pizza)
{
  int pizzaRows = await _pizzaRepo.AddEntityAsync(pizza.Pizza);
  int ingredientRows = await _ingredientRepo.PutIngredientsOnPizza(
    pizza.Pizza.PizzaId,
    pizza.Ingredients.Select(x => x.IngredientId).ToArray());

  _context.SaveChanges();
} 

然后在存储库中:

public class PizzaRepository : IPizzaRepository
{
  private readonly PizzaDbContext _pizzaDbContext = null;

  public PizzaRepository(PizzaDbContext pizzaDbContext)
  {
    _pizzaDbContext = pizzaDbContext;
  }

  public async Task<int> AddEntityAsync( /* params */ )
  {
     PizzaContext.Pizzas.Add( /* pizza */)
     // ...
   }
}

这个模式的问题是它将工作单元限制为请求,而且仅限于请求。您必须知道上下文保存更改发生的时间和地点。例如,您不希望存储库调用 SaveChanges,因为这可能会产生副作用,具体取决于调用之前上下文发生的变化。

因此,我使用工作单元模式来管理 DbContext 的生命周期范围,其中存储库不再注入 DbContext,而是获得定位器,服务获得上下文范围工厂. (工作单元)我用于 EF(6) 的实现是 Mehdime 的 DbContextScope。 (https://github.com/mehdime/DbContextScope) There are forks available for EFCore. (https://www.nuget.org/packages/DbContextScope.EfCore/) 使用 DBContextScope,服务调用看起来更像:

public PizzaService(IDbContextScopeFactory contextScopeFactory, IPizzaRepo pizzaRepo, IPizzaIngredientsRepo ingredientRepo)
{
  _contextScopeFactory = contextScopeFactory;
  _pizzaRepo = pizzaRepo;
  _ingredientRepo = ingredientRepo;
}

public async Task SavePizza(PizzaViewModel pizza)
{
  using (var contextScope = _contextScopeFactory.Create())
  {
    int pizzaRows = await _pizzaRepo.AddEntityAsync(pizza.Pizza);
    int ingredientRows = await _ingredientRepo.PutIngredientsOnPizza(
      pizza.Pizza.PizzaId,
      pizza.Ingredients.Select(x => x.IngredientId).ToArray());

    contextScope.SaveChanges();
  }
}  

然后在存储库中:

public class PizzaRepository : IPizzaRepository
{
  private readonly IAmbientDbContextLocator _contextLocator = null;

  private PizzaContext PizzaContext
  {
    get { return _contextLocator.Get<PizzaContext>(); }
  }

  public PizzaRepository(IDbContextScopeLocator contextLocator)
  {
    _contextLocator = contextLocator;
  }

  public async Task<int> AddEntityAsync( /* params */ )
  {
     PizzaContext.Pizzas.Add( /* pizza */)
     // ...
   }
}

这会给您带来一些好处:

  1. 工作范围单元的控制在服务中保持清晰。您可以调用任意数量的存储库,并且将根据服务的确定提交或回滚更改。 (检查结果、捕获异常等)
  2. 这个模型在有界上下文中工作得非常好。在较大的系统中,您可以将不同的关注点拆分到多个 DbContext 中。上下文定位器作为存储库的一个依赖项,可以访问 any/all DbContexts。 (想想日志记录、审计等)
  3. 对于使用工厂中的 CreateReadOnly() 范围创建的基于读取的操作,还有一个轻微的 performance/safety 选项。这创建了一个无法保存的上下文范围,因此它保证没有写入操作提交到数据库。
  4. IDbContextScopeFactory 和 IDbContextScope 很容易模拟,因此您的服务单元测试可以验证事务是否已提交。 (模拟一个 IDbContextScope 以断言 SaveChanges,并模拟一个 IDbContextScopeFactory 以期望一个 Create 和 return DbContextScope 模拟。)在那个和存储库模式之间,没有混乱的模拟 DbContexts。

我在您的示例中看到的一个警告是,您的视图模型似乎正在充当实体的包装器。 (PizzaViewModel.Pizza) 我建议不要将实体传递给客户端,而是让视图模型只表示视图所需的数据。我概述了这样做的原因 here.