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


115

当我处于分离场景中并从客户端获取dto时,我将其映射到实体中以保存它,请执行以下操作:

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

那是什么 DbSet.Attach(entity)

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


最好添加一些版本信息,这已经被问过。我不清楚这是否值得提出新的问题。
Henk Holterman

Answers:


277

执行此操作时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;这将导致所有属性/列都被更新)

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

代码示例

假设您具有以下实体:

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语句在将实体附加到上下文之后更新实际更改的值。根据表的结构,这可能会对性能产生积极影响。

现在,哪种方法更适合您完全取决于您要执行的操作。


1
EF不会以这种方式生成WHERE子句。如果您附加了使用new创建的实体(即new Entity())并将其设置为Modify,则由于乐观锁,您必须设置所有原始字段。在UPDATE查询中生成的WHERE子句通常包含所有原始字段(不仅是ID),因此,如果不这样做,EF将引发并发异常。
bubi

3
@budi:谢谢您的反馈。我进行了重新测试,以确保对于基本实体,它的行为确实与我所描述的相同,WHERE子句仅包含主键,而没有任何并发​​检查。要进行并发检查,我需要将列明确配置为并发令牌或rowVersion。在这种情况下,WHERE子句将仅具有主键和并发令牌列,而没有所有字段。如果您的测试另有说明,我很想听听。
斯坦(Sstan)2015年

我如何动态地发现女巫的属性被修改了?
navid_pdp11 '16

2
@ Navid_pdp11 DbContext.Entry(person).CurrentValuesDbContext.Entry(person).OriginalValues
Shimmy Weitzhandler'5

可能会稍微偏离主题,但是如果我使用存储库模式,则必须为每个模型创建一个存储库,因为每个模型都有一些实体,当在db中插入新记录时,该实体需要处于未跟踪状态,所以我不能在插入过程中将实体附加到上下文的通用存储库。您如何最好地处理?
jayasurya_j

3

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


0

除了(在已标记的答案之外)和之间(在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; }
}

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

获取订单:

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

更新时将Child-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

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

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.