不同存储库的事务范围 类
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 */)
// ...
}
}
这会给您带来一些好处:
- 工作范围单元的控制在服务中保持清晰。您可以调用任意数量的存储库,并且将根据服务的确定提交或回滚更改。 (检查结果、捕获异常等)
- 这个模型在有界上下文中工作得非常好。在较大的系统中,您可以将不同的关注点拆分到多个 DbContext 中。上下文定位器作为存储库的一个依赖项,可以访问 any/all DbContexts。 (想想日志记录、审计等)
- 对于使用工厂中的
CreateReadOnly()
范围创建的基于读取的操作,还有一个轻微的 performance/safety 选项。这创建了一个无法保存的上下文范围,因此它保证没有写入操作提交到数据库。
- IDbContextScopeFactory 和 IDbContextScope 很容易模拟,因此您的服务单元测试可以验证事务是否已提交。 (模拟一个 IDbContextScope 以断言
SaveChanges
,并模拟一个 IDbContextScopeFactory 以期望一个 Create
和 return DbContextScope 模拟。)在那个和存储库模式之间,没有混乱的模拟 DbContexts。
我在您的示例中看到的一个警告是,您的视图模型似乎正在充当实体的包装器。 (PizzaViewModel.Pizza) 我建议不要将实体传递给客户端,而是让视图模型只表示视图所需的数据。我概述了这样做的原因 here.
我正在尝试围绕发生在不同存储库 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 */)
// ...
}
}
这会给您带来一些好处:
- 工作范围单元的控制在服务中保持清晰。您可以调用任意数量的存储库,并且将根据服务的确定提交或回滚更改。 (检查结果、捕获异常等)
- 这个模型在有界上下文中工作得非常好。在较大的系统中,您可以将不同的关注点拆分到多个 DbContext 中。上下文定位器作为存储库的一个依赖项,可以访问 any/all DbContexts。 (想想日志记录、审计等)
- 对于使用工厂中的
CreateReadOnly()
范围创建的基于读取的操作,还有一个轻微的 performance/safety 选项。这创建了一个无法保存的上下文范围,因此它保证没有写入操作提交到数据库。 - IDbContextScopeFactory 和 IDbContextScope 很容易模拟,因此您的服务单元测试可以验证事务是否已提交。 (模拟一个 IDbContextScope 以断言
SaveChanges
,并模拟一个 IDbContextScopeFactory 以期望一个Create
和 return DbContextScope 模拟。)在那个和存储库模式之间,没有混乱的模拟 DbContexts。
我在您的示例中看到的一个警告是,您的视图模型似乎正在充当实体的包装器。 (PizzaViewModel.Pizza) 我建议不要将实体传递给客户端,而是让视图模型只表示视图所需的数据。我概述了这样做的原因 here.