【问题标题】:DbSet.Attach(entity) vs DbContext.Entry(entity).State = EntityState.ModifiedDbSet.Attach(entity) vs DbContext.Entry(entity).State = EntityState.Modified
【发布时间】:2015-09-08 08:50:18
【问题描述】:

当我处于分离场景并从客户端获取 dto 并将其映射到实体以保存它时,我会这样做:

context.Entry(entity).State = EntityState.Modified;
context.SaveChanges();

那么DbSet.Attach(entity)是什么

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

【问题讨论】:

  • 最好添加一些版本信息,这个之前已经问过了。我不清楚这是否值得提出一个新问题。

标签: c# entity-framework entity-framework-6


【解决方案1】:

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

这并不总是需要的。

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

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

即使您计划更新实体,如果实体有很多属性(db 列)但您只想更新一些,您可能会发现执行DbSet.Attach(entity) 是有利的,并且然后只更新需要更新的几个属性。这样做会从 EF 生成更有效的更新语句。 EF 只会更新您修改的属性(与 context.Entry(entity).State = EntityState.Modified; 不同,后者会导致所有属性/列都被更新)

相关文档:Add/Attach and Entity States.

代码示例

假设您有以下实体:

public class Person
{
    public int Id { get; set; } // primary key
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

如果您的代码如下所示:

context.Entry(personEntity).State = EntityState.Modified;
context.SaveChanges();

生成的 SQL 如下所示:

UPDATE person
SET FirstName = 'whatever first name is',
    LastName = 'whatever last name is'
WHERE Id = 123; -- whatever Id is.

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

相比之下,如果您的代码使用“普通”附加,如下所示:

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

那么生成的更新语句就不同了:

UPDATE person
SET FirstName = 'John'
WHERE Id = 123; -- whatever Id is.

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

现在,哪个选项更适合您完全取决于您要做什么。

【讨论】:

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

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

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

  • 我使用的是 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; }
}

这是数据库中的(原始)测试数据:

获取订单:

order = db.Orders.Include(o => o.OrderPositions).Include(o => o.OrderedByUser).FirstOrDefault();

现在开始测试:

使用 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

使用附加进行简单更新:

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

使用 EntityState 更改子 ID 进行更新:

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

使用 Attach 更改子 ID 进行更新:

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.)

注意:这会抛出异常,无论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

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

使用 EntityState 更改参考 ID 进行更新:

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

使用 Attach 更改参考 ID 进行更新:

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

注意:引用将更改为用户 3,但将更新用户 1,我猜这是因为 order.OrderedByUser.Id 未更改(它仍然是 1)。

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

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

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

【讨论】:

    【解决方案3】:

    当您使用DbSet.Update 方法时,Entity Framework 将您的实体的所有属性标记为EntityState.Modified,以便跟踪它们。如果您只想更改部分属性,而不是全部属性,请使用DbSet.Attach。此方法使您的所有属性EntityState.Unchanged,因此您必须使您想要更新的属性EntityState.Modified。因此当应用点击DbContext.SaveChanges时,它只会操作修改后的属性。

    【讨论】:

      【解决方案4】:

      也可以使用这种技术部分更新实体,而不是将其附加到上下文中。两者都一样。虽然将实体状态设置为 Modified 会更新所有属性,但您可以通过将其 IsModified 属性设置为 false 来排除那些不应更新的属性。

      EntityEntry entry = context.Entry(entityToUpdate);
      entry.State = EntityState.Modified;
      entry.Property("CreatedAt").IsModified = false;
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-05-23
        • 1970-01-01
        • 1970-01-01
        • 2018-05-31
        • 1970-01-01
        • 2011-03-19
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多