DbSet.Attach(entity)vs DbContext.Entry(entity).State = EntityState.Modified

Eli*_*eth 104 c# entity-framework entity-framework-6

当我处于一个独立的场景并从客户端获取dto时,我将其映射到一个实体以保存它,我这样做:

context.Entry(entity).State = EntityState.Modified;
context.SaveChanges();
Run Code Online (Sandbox Code Playgroud)

那是什么呢 DbSet.Attach(entity)

或者当EntityState.Modified已经附加实体时,我为什么要使用.Attach方法?

sst*_*tan 256

当你这样做时context.Entry(entity).State = EntityState.Modified;,你不仅要将实体附加到DbContext,还要将整个实体标记为脏.这意味着,当您这样做时context.SaveChanges(),EF将生成一个更新语句,该语句将更新实体的所有字段.

这并不总是需要的.

另一方面,DbSet.Attach(entity)将实体附加到上下文而不将其标记为脏.这相当于做context.Entry(entity).State = EntityState.Unchanged;

以这种方式附加时,除非您继续更新实体上的属性,否则下次调用时context.SaveChanges(),EF将不会为此实体生成数据库更新.

即使您计划对实体进行更新,如果实体具有大量属性(db列)但您只想更新一些,您可能会发现执行a更有利DbSet.Attach(entity),然后只更新几个属性需要更新.这样做将从EF生成更有效的更新语句.EF将仅更新您修改的属性(与此相反context.Entry(entity).State = EntityState.Modified;,将导致更新所有属性/列)

相关文档:添加/附加和实体状态.

代码示例

假设您有以下实体:

public class Person
{
    public int Id { get; set; } // primary key
    public string FirstName { get; set; }
    public string LastName { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

如果您的代码如下所示:

context.Entry(personEntity).State = EntityState.Modified;
context.SaveChanges();
Run Code Online (Sandbox Code Playgroud)

生成的SQL看起来像这样:

UPDATE person
SET FirstName = 'whatever first name is',
    LastName = 'whatever last name is'
WHERE Id = 123; -- whatever Id is.
Run Code Online (Sandbox Code Playgroud)

请注意上述更新语句将如何更新所有列,无论您是否实际更改了值.

相反,如果您的代码使用"正常"附加,如下所示:

context.People.Attach(personEntity); // State = Unchanged
personEntity.FirstName = "John"; // State = Modified, and only the FirstName property is dirty.
context.SaveChanges();
Run Code Online (Sandbox Code Playgroud)

然后生成的更新语句是不同的:

UPDATE person
SET FirstName = 'John'
WHERE Id = 123; -- whatever Id is.
Run Code Online (Sandbox Code Playgroud)

如您所见,update语句更新将实体附加到上下文后实际更改的值.根据表的结构,这可能会对性能产生积极影响.

现在,哪个选项更适合您,完全取决于您要做的事情.

  • @budi:谢谢你的反馈.我重新测试以确定,对于一个基本实体,它的行为与我所描述的一样,`WHERE`子句只包含主键,没有任何并发​​检查.要进行并发检查,我需要将列显式配置为并发令牌或rowVersion.在这种情况下,`WHERE`子句将只有主键和并发令牌列,而不是所有字段.如果您的测试显示不同,我很乐意听到它. (3认同)
  • 如何动态发现女巫属性被修改? (3认同)
  • EF 不会以这种方式生成 WHERE 子句。如果您附加了一个用 new 创建的实体(即 new Entity())并将其设置为已修改,由于乐观锁,您必须设置所有原始字段。UPDATE 查询中生成的 WHERE 子句通常包含所有原始字段(不仅是 Id),因此如果您不这样做,EF 将引发并发异常。 (2认同)
  • @ Navid_pdp11`DbContext.Entry(person).CurrentValues`和`DbContext.Entry(person).OriginalValues`. (2认同)
  • 我发现 `context.Entry(entity).State = EntityState.Unchanged` 和 `context.Attach(entity)` 之间的一个区别是 `Attach()` 还将跟踪所有链接对象的属性(例如 `entity.OwnedEntity. Xyz`),而 `Entry(entity).State` 将仅跟踪实体的“第一级”属性。 (2认同)

Ste*_*ieG 6

除了(标记的答案)和(在 EF Core 中)之间还有一个重要区别context.Entry(entity).State = EntityState.Unchangedcontext.Attach(entity)

我自己做了一些测试以更深入地了解它(因此这也包括一些通用的参考测试),所以这是我的测试场景:

  • 我使用了 EF Core 3.1.3
  • 我用了 QueryTrackingBehavior.NoTracking
  • 我只使用属性进行映射(见下文)
  • 我使用不同的上下文来获取订单并更新订单
  • 我每次测试都擦掉了整个数据库

这些是模型:

public class Order
{
    public int Id { get; set; }
    public string Comment { get; set; }
    public string ShippingAddress { get; set; }
    public DateTime? OrderDate { get; set; }
    public List<OrderPos> OrderPositions { get; set; }
    [ForeignKey("OrderedByUserId")]
    public User OrderedByUser { get; set; }
    public int? OrderedByUserId { get; set; }
}

public class OrderPos
{
    public int Id { get; set; }
    public string ArticleNo { get; set; }
    public int Quantity { get; set; }
    [ForeignKey("OrderId")]
    public Order Order { get; set; }
    public int? OrderId { get; set; }
}

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

这是数据库中的(原始)测试数据: 在此处输入图片说明

获取订单:

order = db.Orders.Include(o => o.OrderPositions).Include(o => o.OrderedByUser).FirstOrDefault();
Run Code Online (Sandbox Code Playgroud)

现在测试:

使用EntityState 进行简单更新:

db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1
Run Code Online (Sandbox Code Playgroud)

附件的简单更新:

db.Attach(order);
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 1 Call:
// UPDATE [OrderPositions] SET [ArticleNo] = 'K-1234' WHERE [Id] = 1
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555 (NEW)', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 1
Run Code Online (Sandbox Code Playgroud)

使用EntityState更改 Child-Ids 进行更新

db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.Id = 3; // will be IGNORED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].Id = 3; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1
Run Code Online (Sandbox Code Playgroud)

使用Attach更改 Child-Ids 进行更新:

db.Attach(order);
order.ShippingAddress = "Germany"; // would be UPDATED
order.OrderedByUser.Id = 3; // will throw EXCEPTION
order.OrderedByUser.FirstName = "William (CHANGED)"; // would be UPDATED
order.OrderPositions[0].Id = 3; // will throw EXCEPTION
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // would be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // would be INSERTED
db.SaveChanges();
// Throws Exception: The property 'Id' on entity type 'User' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key first delete the dependent and invoke 'SaveChanges' then associate the dependent with the new principal.)
Run Code Online (Sandbox Code Playgroud)

注意:这会抛出异常,无论 Id 是更改还是设置为原始值,似乎 Id 的状态设置为“已更改”,这是不允许的(因为它是主键)

更新将 Child-Ids 更改为新的(EntityState 和 Attach 之间没有区别):

db.Attach(order); // or db.Entry(order).State = EntityState.Unchanged;
order.OrderedByUser = new User();
order.OrderedByUser.Id = 3; // // Reference will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED (on User 3)
db.SaveChanges();
// Will generate SQL in 2 Calls:
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 3
Run Code Online (Sandbox Code Playgroud)

注意:请查看与没有 new 的 EntityState 更新(上面)的区别。这次 Name 将被更新,因为新的 User 实例。

使用EntityState更改 Reference-Ids 进行更新

db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUserId = 3; // will be UPDATED
order.OrderedByUser.Id = 2; // will be IGNORED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].Id = 3; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1
Run Code Online (Sandbox Code Playgroud)

通过使用Attach更改 Reference-Ids 进行更新:

db.Attach(order);
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUserId = 3; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED (on FIRST User!)
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 1 Call:
// UPDATE [OrderPositions] SET [ArticleNo] = 'K-1234' WHERE [Id] = 1
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555 (NEW)', 1, 5)
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 1
Run Code Online (Sandbox Code Playgroud)

注:参考将变为用户3,但用户1将被更新,我想这是因为order.OrderedByUser.Id是不变的(它仍然是1)。

结论 使用 EntityState 您有更多控制权,但您必须自己更新子属性(二级)。使用 Attach 您可以更新所有内容(我猜是所有级别的属性),但您必须密切关注引用。举个例子:如果 User (OrderedByUser) 是一个 dropDown,则通过 dropDown 更改值可能会覆盖整个 User 对象。在这种情况下,原始 dropDown-Value 将被覆盖而不是引用。

对我来说,最好的情况是将 OrderedByUser 之类的对象设置为 null,并且只将 order.OrderedByUserId 设置为新值,如果我只想更改引用(无论是 EntityState 还是 Attach)。

希望这会有所帮助,我知道这是很多文字:D