【问题标题】:Transaction Scope for different repository classes不同存储库类的事务范围
【发布时间】:2018-11-15 17:07:24
【问题描述】:

我正在尝试围绕发生在不同存储库类中的 2 个或更多数据库操作来包装事务。每个存储库类都使用一个 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();
    }
}

}

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

即使上述方法有效,还有更好的方法来实现事务吗?

【问题讨论】:

  • 假设您的数据库提供商支持这一点,那么是的。但是,如果您的两个存储库使用两个 不同 DbContext 实例,这将需要分布式事务,这是一种反模式,或者至少不建议用于高频事务。另一方面,如果您注入一个支持这两个存储库的单个 DbContext 实例,那么这绝对没问题。

标签: c# entity-framework transactions transactionscope


【解决方案1】:

存储库模式非常适合启用测试,但没有新的存储库 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(s) 的生命周期范围,其中存储库不再注入 DbContext,而是获得定位器,服务获得上下文范围工厂。 (工作单元)我用于 EF(6) 的实现是 Mehdime 的 DbContextScope。 (https://github.com/mehdime/DbContextScope) 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 中拆分不同的关注点。上下文定位器用作存储库的一个依赖项,并且可以访问任何/所有 DbContext。 (想想日志记录、审计等)
  3. 对于使用工厂中创建的CreateReadOnly() 范围的基于读取的操作,还有一个轻微的性能/安全选项。这会创建一个无法保存的上下文范围,因此它保证不会向数据库提交任何写入操作。
  4. IDbContextScopeFactory 和 IDbContextScope 很容易模拟,因此您的服务单元测试可以验证事务是否已提交。 (模拟一个 IDbContextScope 以断言 SaveChanges,模拟一个 IDbContextScopeFactory 以期望一个 Create 并返回 DbContextScope 模拟。)在这和存储库模式之间,没有混乱的模拟 DbContexts。

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

【讨论】:

  • 感谢您的广泛回答。即使 Viewmodel 属性和 Entity 属性是 1:1 相似的,也不应该将实体传递给客户端吗?
  • 即使那样我仍然会避免它,因为序列化实体会触发延迟加载调用或导致发送不完整的实体图。它设定了从客户端返回的实体是完整的期望,并且存在将其简单地附加到上下文并保存更改而不验证来自客户端的数据未被篡改的诱惑。绝对不相信来自客户的任何东西。 (浏览器或 API 使用者)Automapper 可以很容易地管理这些映射。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-09-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-18
相关资源
最近更新 更多