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

时间:2015-06-22 19:03:21

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

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

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

那么DbSet.Attach(entity)

是什么

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

4 个答案:

答案 0 :(得分:237)

执行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.

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

相反,如果您的代码使用" normal"像这样附上:

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.

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

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

答案 1 :(得分: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; }
}

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

要获取订单:

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

通过更改附加更改子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是更改还是设置为原始值,这都将引发Exception,似乎Id的状态设置为“更改”,并且这是不允许的(因为它是主键)

将子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实例,名称将被更新。

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

使用附加来更改参考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)。 / p>

结论 使用EntityState,您可以控制更多,但是您必须自己更新子属性(第二级)。 使用“附加”,您可以更新所有内容(我想具有所有级别的属性),但是您必须注意引用。 仅作为示例:如果User(OrderedByUser)是dropDown,则通过dropDown更改值可能会覆盖整个User对象。 在这种情况下,原始的dropDown-Value将被覆盖,而不是引用。

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

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

答案 2 :(得分:0)

当您使用DbSet.Update方法时,实体框架会将您实体的所有属性标记为EntityState.Modified,因此请跟踪它们。如果您只想更改部分属性,请使用DbSet.Attach。此方法会生成您的所有属性EntityState.Unchanged,因此您必须创建要更新的属性EntityState.Modified。因此,当应用程序点击DbContext.SaveChanges时,它只会运行修改后的属性。

答案 3 :(得分:0)

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

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