在事务中使用DbContext SaveChanges

本文关键字:DbContext SaveChanges 事务 | 更新日期: 2023-09-27 18:13:23

正如MSDN确认的那样,在EF 5及以后,DbContext类是"工作单元模式和存储库模式的组合"。在我构建的web应用程序中,我倾向于在现有DbContext类的基础上实现Repository和Unit-Of-Work模式。最近,像其他许多人一样,我发现在我的场景中这是过度的。我并不担心SQL Server的底层存储机制会发生变化,虽然我很欣赏单元测试带来的好处,但在实际在实际应用程序中实现它之前,我仍然有很多东西要学习。

因此,我的解决方案是直接使用DbContext类作为存储库和工作单元,然后使用StructureMap为每个请求注入一个实例到各个服务类,允许它们在上下文上工作。然后在我的控制器中,注入我需要的每个服务,并相应地调用每个操作所需的方法。此外,每个请求都包装在请求开始时从DbContext创建的事务中,如果发生任何类型的异常(无论是EF错误还是应用程序错误),则回滚,如果一切正常,则提交。下面是一个示例代码场景。

此示例使用Northwind示例数据库中的Territory和Shipper表。在这个示例管理控制器中,将同时添加一个区域和一个托运人。

控制器

public class AdminController : Controller 
{
    private readonly TerritoryService _territoryService;
    private readonly ShipperService _shipperService;
    public AdminController(TerritoryService territoryService, ShipperService shipperService)
    {
        _territoryService = territoryService;
        _shipperService = shipperService;
    }
    // all other actions omitted...
    [HttpPost]
    public ActionResult Insert(AdminInsertViewModel viewModel)
    {
        if (!ModelState.IsValid)
            return View(viewModel);
        var newTerritory = // omitted code to map from viewModel
        var newShipper = // omitted code to map from viewModel
        _territoryService.Insert(newTerritory);
        _shipperService.Insert(newShipper);
        return RedirectToAction("SomeAction");
    }
}
<<p> 领土服务/strong>
public class TerritoryService
{
    private readonly NorthwindDbContext _dbContext;
    public TerritoryService(NorthwindDbContext dbContext) 
    {
        _dbContext = dbContext;
    }
    public void Insert(Territory territory)
    {
        _dbContext.Territories.Add(territory);
    }
}
<<p> Shipper服务/strong>
public class ShipperService
{
    private readonly NorthwindDbContext _dbContext;
    public ShipperService(NorthwindDbContext dbContext) 
    {
        _dbContext = dbContext;
    }
    public void Insert(Shipper shipper)
    {
        _dbContext.Shippers.Add(shipper);
    }
}

创建Application_BeginRequest()上的事务

// _dbContext is an injected instance per request just like in services
HttpContext.Items["_Transaction"] = _dbContext.Database.BeginTransaction(System.Data.IsolationLevel.ReadCommitted);

Application_EndRequest上的事务回滚或提交

var transaction = (DbContextTransaction)HttpContext.Items["_Transaction"];
if (HttpContext.Items["_Error"] != null) // populated on Application_Error() in global
{
    transaction.Rollback();
}
else
{
    transaction.Commit();
}

现在这一切似乎工作得很好,但我现在唯一的问题是在哪里最好调用DbContext上的SaveChanges()函数?我应该在每个服务层方法中调用它吗?

public class TerritoryService
{
    // omitted code minus changes to Insert() method below
    public void Insert(Territory territory)
    {
        _dbContext.Territories.Add(territory);
        _dbContext.SaveChanges();  // <== Call it here?
    }
}
public class ShipperService
{
    // omitted code minus changes to Insert() method below
    public void Insert(Shipper shipper)
    {
        _dbContext.Shippers.Add(shipper);
        _dbContext.SaveChanges();  // <== Call it here?
    }
}
或者我应该保持服务类Insert()方法不变,在事务提交之前调用SaveChanges() ?
var transaction = (DbContextTransaction)HttpContext.Items["_Transaction"];
// HttpContext.Items["_Error"] populated on Application_Error() in global
if (HttpContext.Items["_Error"] != null) 
{
    transaction.Rollback();
}
else
{
    // _dbContext is an injected instance per request just like in services
    _dbContext.SaveChanges(); // <== Call it here?
    transaction.Commit();
}

两种方法都可以吗?它是安全的调用SaveChanges()不止一次,因为它是包装在一个事务?我这样做会遇到什么问题吗?还是最好在事务实际提交之前调用一次SaveChanges() ?我个人宁愿在事务提交之前调用它,但我想确保我没有错过任何与事务有关的问题或做错了什么?如果你读到这里,感谢你花时间帮助我。我知道这是个很长的问题。

在事务中使用DbContext SaveChanges

在提交单个原子持久化操作时调用SaveChanges()。由于您的服务并不真正了解彼此或相互依赖,因此在内部它们无法保证其中一个或另一个将提交更改。因此,在这种设置中,我认为他们将每个必须提交他们的更改。

这当然会导致这些操作可能不是单独原子的问题。考虑这个场景:

_territoryService.Insert(newTerritory);  // success
_shipperService.Insert(newShipper);  // error

在这种情况下,您已经部分提交了数据,使系统处于未知状态。

在此场景中,哪个对象控制操作的原子性?在web应用中,我认为那是通常是控制器。毕竟,操作是用户发出的请求。在大多数场景中(当然也有例外),我想人们会期望整个请求成功或失败。

如果是这种情况,并且您的原子性属于请求级别,那么我建议从控制器级别的IoC容器中获取DbContext并将其传递给服务。(它们已经在自己的构造函数中要求它,所以这里没有太大的变化。)这些服务可以在上下文上操作,但不能提交上下文。一旦所有的服务都完成了它们的操作,消费代码(控制器)就可以提交它(或者回滚它,或者放弃它,等等)。

虽然不同的业务对象、服务等应该各自在内部维护自己的逻辑,但我发现拥有操作原子性的对象通常处于应用程序级别,由用户调用的业务流程管理。

你基本上是在这里创建一个存储库,而不是一个服务。

要回答你的问题,你可以再问自己一个问题。"我将如何使用这个功能?"

你正在添加一些记录,删除一些记录,更新一些记录。我们可以说你调用了各种方法大约30次。如果您调用SaveChanges 30次,那么您将进行30次到数据库的往返,这会导致大量可以避免的流量和开销。

我通常建议尽可能少地进行数据库往返,并限制对SaveChanges()的调用数量。因此,我建议您在存储库/服务层中添加Save()方法,并在调用存储库/服务层的层中调用它。

除非绝对需要在做其他事情之前保存一些东西,否则你不应该调用30次。你应该只打一次。如果有必要在做其他事情之前保存一些东西,你仍然可以在调用存储库/服务层的层的绝对需求时刻调用SaveChanges。

Summary/TL;DR:在存储库/服务层创建Save()方法,而不是在每个存储库/服务方法中调用SaveChanges()。这将提高您的性能,并节省不必要的开销。