C# 在事务中使用DbContext SaveChanges

C# 在事务中使用DbContext SaveChanges,c#,asp.net-mvc,transactions,entity-framework-5,dbcontext,C#,Asp.net Mvc,Transactions,Entity Framework 5,Dbcontext,作为MSDN,在EF 5及更高版本中,DbContext类是“工作单元和存储库模式的组合”。在我构建的web应用程序中,我倾向于在现有DbContext类的基础上实现存储库和工作单元模式。最近,像许多其他人一样,我发现在我的场景中,这是一种过度的杀伤力。我并不担心底层存储机制会随着SQL Server的不断变化而变化,虽然我很欣赏单元测试将带来的好处,但在实际应用程序中实现它之前,我还有很多需要了解的地方 因此,我的解决方案是直接使用DbContext类作为存储库和工作单元,然后使用Struct

作为MSDN,在EF 5及更高版本中,DbContext类是“工作单元和存储库模式的组合”。在我构建的web应用程序中,我倾向于在现有DbContext类的基础上实现存储库和工作单元模式。最近,像许多其他人一样,我发现在我的场景中,这是一种过度的杀伤力。我并不担心底层存储机制会随着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");
    }
}
地区服务

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);
    }
}
托运人服务

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);
    }
}
在应用程序上创建事务\u BeginRequest()

应用程序请求上事务的回滚或提交

// _dbContext is an injected instance per request just like in services
HttpContext.Items["_Transaction"] = _dbContext.Database.BeginTransaction(System.Data.IsolationLevel.ReadCommitted);
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?
    }
}
公共类TerritoryService
{
//省略的代码减去对下面Insert()方法的更改
公共空白插入(区域)
{
_dbContext.territions.Add(territory);

_dbContext.SaveChanges();//您基本上是在这里创建存储库,而不是服务

要回答您的问题,您可以问自己另一个问题:“我将如何使用此功能?”

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

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

除非绝对需要在执行其他操作之前保存某些内容,否则您不应该调用它30次。您应该调用它1次。如果必须在执行其他操作之前保存某些内容,您仍然可以在调用存储库/服务层的层中的绝对需要时刻调用SaveChanges


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

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

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

_territoryService.Insert(newTerritory);  // success
_shipperService.Insert(newShipper);  // error
在本例中,您已经部分提交了数据,使系统处于一种未知状态

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

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


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

我原本想这样做,但这样做不会违背服务层,如果您现在可以访问上下文中您想要的任何数据库集,而不必使用服务层。我希望尽可能将与上下文的交互隔离到服务层,以保持控制器干净。还是我让这变得复杂了?@ryanulit:我认为这种情况下的服务不是真正的服务,它们更像repositories。服务通常是自身公开原子业务操作的东西,可以用web服务实现等替代。这里的内容看起来更像是存储库,在逻辑上与工作单元无关。在这种情况下,
DbContext
就是该工作单元。也许您甚至可以吃西餐