附加(实体)与 DbContext.Entry (实体)

当我在一个分离的场景中,并从客户端获得一个 dto,我将它映射到一个实体来保存它,我这样做:

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

那么 DbSet.Attach(entity)是用来做什么的呢?

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

120050 次浏览

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

这并不总是人们所希望的。

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

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

即使您计划对一个实体进行更新,如果该实体有很多属性(db 列) ,但您只想更新少数属性,那么您可能会发现执行 DbSet.Attach(entity)是有利的,然后只更新需要更新的少数属性。这样做将从 EF 生成一个更有效的更新语句。EF 将只更新您修改的属性(与 context.Entry(entity).State = EntityState.Modified;不同,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; }
}

如果您的代码如下所示:

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.

注意上面的 update 语句将如何更新所有列,不管您是否实际更改了值。

相反,如果您的代码使用“普通”附件,则如下所示:

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.

可以看到,update 语句 只有更新了在将实体附加到上下文后实际更改的值。根据表的结构,这可能会对性能产生积极的影响。

现在,哪个选项对你来说更好完全取决于你想要做什么。

使用 DbSet.Update方法时,实体框架将实体的所有属性标记为 EntityState.Modified,因此可以跟踪它们。如果只想更改部分属性,而不是全部属性,请使用 DbSet.Attach。此方法创建所有属性 EntityState.Unchanged,因此必须创建要更新 EntityState.Modified的属性。因此,当应用程序命中到 DbContext.SaveChanges,它将只操作修改的属性。

除此之外(对于标记的答案)在 context.Entry(entity).State = EntityState.Unchangedcontext.Attach(entity)之间还有一个 重要的区别(在 EF 核心中) :

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

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

这是数据库中的(原始)测试数据: enter image description here

为了得到订单:

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

现在的测试:

简单更新 实体国家:

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

使用 实体国家更新儿童身份:

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

使用 接上更新儿童身份:

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

注意: 这将抛出 Exception,无论 Id 是否被更改或设置为原始值,似乎 Id 的状态被设置为“ change”,而这是不允许的(因为它是主键)

将子 ID 更改为新的更新(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。

使用 实体国家更改参考编号更新:

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

使用 接上更改参考编号更新:

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,您可以更新所有内容(我想可以更新所有级别的属性) ,但是您必须密切关注引用。 例如: 如果 User (OrderedByUser)是一个 drop Down,那么通过 drop Down 更改值可能会覆盖整个 User-object。 在这种情况下,原始的 drop Down-Value 将被覆盖,而不是引用。

对我来说,最好的情况是将 OrderedByUser 这样的对象设置为 null 并只设置顺序。OrderedByUserId 到新值,如果我只想更改引用(不管是 EntityState 还是 Attach)。

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

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

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