EF:db.SaveChanges()vs dbTransaction.Commit

时间:2018-06-13 19:25:45

标签: c# entity-framework transactions distributed-transactions

我对实体框架相当新,我对EF的db.SaveChange有疑问。从一些帖子和MSDN我了解到db.SaveChange默认执行事务中的所有更改。还有一种方法可以使用db.Database.BeginTransaction()创建我们自己的事务,“db”是我的上下文类对象。所以我有两个问题:

  1. 使用什么&当
  2. 如果我将数据插入一个表,其中@@ identity是我下一个插入表的外键,而不是使用db.SaveChange()来获取@@身份是否还有其他任何方式(db.SaveChanges()是在用户定义的事务范围内)并db.SaveChanges()将我的更改提交到DB

1 个答案:

答案 0 :(得分:3)

是的,如果您在诸如.Net的TransactionScope之类的事务中明确地包装您的上下文,您可以在.SaveChanges()调用之后从实体检索自动生成的ID,而无需提交作用域事务。

using (var tx = new TransactionScope())
{
  using (var context = new MyDbContext())
  {
     var newEntity = populateNewEntity();
     context.MyEntities.Add(newEntity);
     context.SaveChanges();
     int entityId = newEntity.EntityId; // Fetches the identity value.
  }
} // Rolls back the transaction. Entity not committed.

但是,除非绝对必要且谨慎,否则应避免此类操作。首先,上面的例子是TransactionScope的常见用法,而TransactionScope的默认隔离级别是" Serializable"这在锁定方面是最悲观的。即使在具有多个并发操作/用户的系统上适度使用此模式,也会因锁定等待而导致死锁和性能命中。因此,如果使用TransactionScope,请务必指定隔离级别。

在您希望协调数据库之间的提交或其他Tx绑定操作的情况下,DTC非常有用。例如,系统A正在保存更改,需要通过API协调与系统B的更新/插入。 A& B需要配置为使用DTC,但一旦完成,A可以启动事务,将其注册到DTC,将DTC令牌附加到B的API头,B可以找到该令牌,创建一个ScopedTransaction链接到那个令牌,并根据A信号提交/回滚。这具有开销成本,这意味着两个系统上的交易比平时更长。如果有必要那么这就是业务成本。如果没有必要,那么这是一种浪费和潜在的头痛问题。

有人可能会考虑使用显式Tx的另一个原因是他们想要在相关实体中更新FK。创建订单有一个选项可以创建一个新客户,订单有一个客户ID,所以我们需要创建客户,获取它在订单上设置的ID,然后保存订单。如果订单保存失败,则应回滚客户。

using (var tx = new TransactionScope())
{
  using (var context = new MyDbContext())
  {
     var newCustomer = createNewCustomer(); // dummy method to indicate creating a customer entity.
     context.Customers.Add(newCustomer);
     context.SaveChanges();
     var newOrder = createNewOrder(); 
     newOrder.CustomerId = newCustomer.CustomerId;
     context.Orders.Add(newOrder);
     context.SaveChanges();
  }
  tx.Commit();  
} 

使用EF时,应使用导航属性以及订单与客户之间的关系来减轻此情况。通过这种方式,您可以创建客户,创建订单,设置订单对新客户的客户参考,将订单添加到DbContext和.SaveChanges()。这让EF负责完成订单,查看引用的客户,插入,按顺序关联FK,并在一个隐式Tx中提交更改。

using (var context = new MyDbContext())
{
    var newCustomer = createNewCustomer();
    var newOrder = createNewOrder();
    newOrder.Customer = newCustomer;
    context.Orders.Add(newOrder);
    context.SaveChanges();
}

更新:概述避免实体中的FK引用......(多对一)

实体中使用FK的订单的EntityTypeConfiguration:

HasRequired(x => x.Customer)
  .WithMany(x => x.Orders) // Links to an element in the Orders collection of the Customer. If Customer does not have/need an Orders collection then .WithMany()
  .HasForeignKey(x => x.CustomerId); // Maps Order.Customer to use CustomerId property on Order entity.

实体中没有FK的订单的EntityTypeConfiguration:

HasRequired(x => x.Customer)
  .WithMany(x => x.Orders)
  .Map(x => x.MapKey("CustomerId")); // Maps Order.Customer to use CustomerId column on underlying Order table. Order entity does not expose a CustomerId.

使用EF Core - 从内存中,可能需要更新。

HasRequired(x => x.Customer)
  .WithMany(x => x.Orders) // Links to an element in the Orders collection of the Customer. If Customer does not have/need an Orders collection then .WithMany()
  .HasForeignKey("CustomerId"); // Creates a shadow property where Entity does not have a CustomerId property.

两种方法(有或没有映射的FK)都是一样的。第二种方法的好处是代码中没有关于如何更新或评估订单的客户参考的混淆。例如,如果您在订单上同时拥有Customer和CustomerId,则更改CustomerId并调用SaveChanges不会将订单移动到新客户,只会设置Customer引用。设置客户参考不会自动更新CustomerId,因此任何代码"获取"通过订单上的CustomerId属性的customerId仍将检索旧的客户引用,直到刷新实体。

使用导航属性的重要一点是利用延迟执行来利用它们或者有效地加载它们。例如,如果要加载订单列表并包含其客户名称:

using (var myContext = new MyDbContext())
{
  var orders = myContext.Orders.Where(x => x.OrderDate >= startDate && x.OrderDate < endDate).ToList();
  return orders;
}

**错误:如果这是MVC / Web API,序列化程序将接受订单集合,并尝试序列化它们命中每个导航属性并尝试加载它。这会逐个触发延迟加载调用。因此,如果Order有一个客户,那就是DB / W&#34; SELECT * FROM Customers WHERE CustomerId = 42&#34;如果订单有订单行,那么&#34; SELECT * FROM OrderLines WHERE OrderLineId = 121&#34;,&#34; SELECT * FROM OrderLines WHERE OrderLineId = 122&#34; ...(您可能认为它已经知道通过OrderId获取订单行,但是没有!巨大的性能影响返回实体,只是不做。

using (var myContext = new MyDbContext())
{
  var orders = myContext.Orders
    .Include(x => x.Customer)
    .Include(x => x.OrderLines)
    .Where(x => x.OrderDate >= startDate && x.OrderDate < endDate).ToList();
  return orders;
}

**更好,但仍然很糟糕。您可能只包含您认为自己需要的项目,但序列化程序仍会获取订单中的所有内容。随着实体的修订以包含新的数据链接,这又回过头来咬你。即使您包含所有内容,如果您想要的只是客户名称,这也是浪费。

using (var myContext = new MyDbContext())
{
  var orders = myContext.Orders
    .Where(x => x.OrderDate >= startDate && x.OrderDate < endDate)
    .Select(x => new OrderLineViewModel 
    {
      OrderId = x.OrderId,
      OrderNumber = x.OrderNumber,
      OrderAmount = x.OrderAmount,
      CustomerName = x.Customer.Name
    }).ToList();
  return orders;
}

**这是导航属性和延迟执行的最佳位置。在DB上运行的SQL只返回相关数据中的那4列。没有延迟加载命中,您只需通过线路发送所需的数据量。

有些人可能会争辩说,如果您通常需要订单中的CustomerId引用,例如,在订单实体上拥有CustomerId会保存引用客户。但如上所述,该Id可能不可靠,并且通过使用延迟执行让EF使用实体来填充您想要的数据获取订单的客户ID只需要包括/选择x.Customer.CustomerId包括只是那个想要的列,没有加载整个实体来获得它。