在事务中使用DbContext SaveChanges

时间:2014-07-16 17:58:54

标签: c# asp.net-mvc transactions entity-framework-5 dbcontext

作为MSDN confirms,在EF 5及以上版本中,DbContext类是“工作单元和存储库模式的组合”。在我构建的Web应用程序中,我倾向于在现有的DbContext类之上实现Repository和Unit-Of-Work模式。最近,和其他许多人一样,我发现这在我的场景中有点过分。我并不担心从SQL Server中改变的底层存储机制,虽然我很欣赏单元测试带来的好处,但在实际应用程序中实现它之前,我仍然需要了解很多东西。

因此,我的解决方案是直接使用DbContext类作为Repository和Unit-Of-Work,然后使用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");
    }
}

地区服务

public class TerritoryService
{
    private readonly NorthwindDbContext _dbContext;

    public TerritoryService(NorthwindDbContext dbContext) 
    {
        _dbContext = dbContext;
    }

    public void Insert(Territory territory)
    {
        _dbContext.Territories.Add(territory);
    }
}

托运人服务

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()一次?我个人宁愿在交易提交之前就在最后调用它,但我想确定我没有错过任何关于交易或做错事的问题?如果您读到这里,感谢您抽出宝贵时间提供帮助。我知道这是一个很长的问题。

2 个答案:

答案 0 :(得分:5)

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

这当然会导致这些操作可能不是单独原子的问题。请考虑以下情况:

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

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

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

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

虽然不同的业务对象,服务等应该在内部维护自己的逻辑,但我发现通常拥有操作原子性的对象是在应用程序级别,由用户调用的业务流程控制。 / p>

答案 1 :(得分:0)

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

要回答你的问题,你可以问自己另一个问题。 &#34;我将如何使用此功能?&#34;

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

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

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

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