英孚:db.SaveChanges() 对比 dbTransaction.Commit

EF: db.SaveChanges() vs dbTransaction.Commit

我是 entity framework 的新手,我对 EF 的 db.SaveChange 有疑问。从一些帖子和 MSDN 中,我了解到 db.SaveChange 默认情况下会执行事务中的所有更改。还有一种方法我们可以使用 db.Database.BeginTransaction() 创建我们自己的交易,"db" 作为我的上下文 class 对象。所以我有两个问题:

  1. 使用什么以及何时使用
  2. 如果我将数据插入一个 table,其@@identity 是我下一次插入 table 的外键,而不是使用 db.SaveChange() 来获取 @@identity任何其他方式(db.SaveChanges() 在用户定义的事务范围内)并且 db.SaveChanges() 将我的更改提交到 DB

是的,如果您明确地将上下文包装在诸如 .Net 的 TransactionScope 之类的事务中,则可以在 .SaveChanges() 调用后从实体中检索 auto-generated 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",在锁方面是最悲观的。即使在具有多个并发 operations/users 的系统上适度使用此模式,也会由于锁等待而导致死锁和性能下降。因此,如果使用 TransactionScope,请务必指定隔离级别。

DTC 在您想要协调数据库或其他 Tx-bound 操作之间的提交的情况下很有用。例如,系统 A 正在保存更改,需要通过 API 与系统 B 协调 update/insert。 A&B需要配置为使用DTC,但是一旦完成,A就可以开始交易,向DTC注册,将DTC令牌附加到B的API的header,B可以发现令牌,创建链接到该令牌的 ScopedTransaction,并根据 A 发出的信号 commit/rollback。这会产生间接费用,这意味着两个系统上的交易开放时间比平时更长。如果有必要,那就是业务成本。如果没有必要,那就是浪费和潜在的头痛源。

有人可能考虑使用显式 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 引用...(many-to-one)

EntityTypeConfiguration for Order With FK in entity:

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 引用。设置 Customer 引用不会自动更新 CustomerId,因此在订单上通过 CustomerId 属性 的任何代码 "getting" customerId 仍然会检索旧的客户引用,直到实体被刷新。

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

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

** 错误:如果这是 MVC/Web API,序列化程序将接受命令 collection,并尝试序列化它们点击每个导航 属性 并尝试加载它。这会触发 lazy-load 调用 one-by-one。因此,如果订单有一个客户,那就是对数据库的命中 /w "SELECT * FROM Customers WHERE CustomerId = 42" 如果订单有订单行,则 "SELECT * FROM OrderLines WHERE OrderLineId = 121"、"SELECT * FROM OrderLines WHERE OrderLineId = 122" ...(您可能认为它知道通过 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;
}

** 这是导航属性和延迟执行的最佳结合点。在数据库 returns 上获取 运行 的 SQL 只是来自相关数据的那 4 列。没有延迟加载命中,您只需通过网络发送所需的数据量。

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