EF:db.SaveChanges()与dbTransaction.Commit

Arj*_*non 2 c# entity-framework transactions distributed-transactions

我对实体框架还很陌生,对EF的db.SaveChange也有疑问。从一些帖子和MSDN中,我了解到db.SaveChange默认情况下会进行事务中的所有更改。还有一种方法,我们可以使用db.Database.BeginTransaction()“ db”作为上下文类对象来创建自己的事务。所以我有两个问题:

  1. 什么时候使用
  2. 如果我将数据插入到@@ identity是我的下一个插入表的外键的一个表中,而不是使用db.SaveChange()获取@@ identity的其他方法(db.SaveChanges()在用户定义的事务范围内),并且会将db.SaveChanges()我的更改提交到D B

Ste*_* Py 5

是的,如果您将上下文显式包装在诸如.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.
Run Code Online (Sandbox Code Playgroud)

但是,除非绝对必要,否则应谨慎避免此类操作。首先,以上示例是TransactionScope的常用用法,TransactionScope的默认隔离级别为“可序列化”,这在锁定方面是最悲观的。在具有多个并发操作/用户的系统上,即使适度使用此模式,也会由于锁定等待而导致死锁和性能下降。因此,如果使用TransactionScope,请确保指定隔离级别。

DTC在需要协调数据库或其他受Tx绑定的操作之间的提交的方案中很有用。例如,系统A正在保存更改,并且需要通过API与系统B协调更新/插入。A和B需要配置为使用DTC,但是一旦完成,A就可以开始交易,向DTC注册,将DTC令牌附加到B的API的标头中,B可以找到该令牌,并创建一个与之链接的ScopedTransaction令牌和基于A信号的提交/回滚。这产生了间接费用,这意味着两个系统上的事务打开时间都比平时长。如果有必要,那是业务成本。如果没有必要,那就是浪费和潜在的头痛根源。

有人可能会考虑使用显式Tx的另一个原因是,当他们想要更新相关实体中的FK时。创建订单可以选择创建新客户,订单具有客户ID,因此我们需要创建客户,获取要在Order上设置的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();  
} 
Run Code Online (Sandbox Code Playgroud)

对于EF,应该通过使用导航属性来缓解这种情况,该属性具有订单和客户之间的关系。这样,您可以创建客户,创建订单,将订单的Customer引用设置为新客户,将订单添加到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();
}
Run Code Online (Sandbox Code Playgroud)

更新:概述在您的实体中避免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.
Run Code Online (Sandbox Code Playgroud)

实体中没有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.
Run Code Online (Sandbox Code Playgroud)

使用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.
Run Code Online (Sandbox Code Playgroud)

两种方法(具有或不具有映射的FK)的工作原理相同。第二种方法的好处是,代码中没有混淆如何更新或评估订单的客户参考。例如,如果您在订单上既有客户又有客户ID,则更改客户ID并调用SaveChanges不会将订单移动到新客户,仅设置客户参考即可。设置客户引用不会自动更新CustomerId,因此,在刷新实体之前,通过订单上的CustomerId属性“获取” customerId的任何代码仍将检索旧的客户引用。

使用导航属性的重要之处在于延迟执行或有效地加载它们。例如,如果要加载订单列表并包括其客户名称:

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

**不好:如果这是MVC / Web API,则序列化程序将接管订单集合,并尝试序列化订单,请访问每个导航属性并尝试加载它。这将触发延迟加载调用。因此,如果Order有一个Customer,那将是对数据库的命中“ w.SELECT * FROM Customers WHERE CustomerId = 42”如果Order有Order行,则“ 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;
}
Run Code Online (Sandbox Code Playgroud)

**更好,但仍然很差。您可能只包括您认为需要的物品,但是序列化程序仍将获取订单上的所有物品。当实体被修改为包括新的数据链接时,这会再次咬住您。即使您包括所有内容,如果您只想要客户名称,这也是浪费的。

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;
}
Run Code Online (Sandbox Code Playgroud)

**这是具有导航属性和延迟执行的最佳选择。在数据库上运行的SQL仅从相关数据中返回这4列。没有延迟负载,您只需通过网络发送所需的数据量即可。

有人可能会争辩说,如果您通常需要订单中的CustomerId引用,例如在Order实体上具有CustomerId可以节省对Customer的引用。但是如上所述,该ID可能并不可靠,通过使用延迟执行让EF使用实体来填充您想要的数据,获取订单的客户ID只是包含/选择x.Customer.CustomerId仅包含所需列的问题,没有加载整个实体来获取它。