DbSet.Attach (thực thể) so với DbContext.Entry (thực thể) .State = EntityState.Modified


115

Khi tôi ở trong một tình huống tách biệt và nhận được một dto từ ứng dụng khách mà tôi ánh xạ vào một thực thể để lưu nó, tôi thực hiện điều này:

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

Đối với những gì sau đó là DbSet.Attach(entity)

hoặc tại sao tôi nên sử dụng phương thức .Attach khi EntityState.Modified đã đính kèm thực thể?


Tốt hơn hãy thêm một số thông tin phiên bản, điều này đã được hỏi trước đây. Tôi không rõ liệu điều này có xứng đáng với một câu hỏi mới hay không.
Henk Holterman

Câu trả lời:


278

Khi bạn làm vậy context.Entry(entity).State = EntityState.Modified;, bạn không chỉ gắn thực thể vào DbContext, mà bạn còn đánh dấu toàn bộ thực thể là bẩn. Điều này có nghĩa là khi bạn thực hiện context.SaveChanges(), EF sẽ tạo một câu lệnh cập nhật sẽ cập nhật tất cả các trường của thực thể.

Điều này không phải lúc nào cũng mong muốn.

Mặt khác, DbSet.Attach(entity)gắn thực thể vào ngữ cảnh mà không đánh dấu nó là bẩn. Nó tương đương với làmcontext.Entry(entity).State = EntityState.Unchanged;

Khi đính kèm theo cách này, trừ khi sau đó bạn tiến hành cập nhật thuộc tính trên thực thể, trong lần gọi tiếp theo context.SaveChanges(), EF sẽ không tạo bản cập nhật cơ sở dữ liệu cho thực thể này.

Ngay cả khi bạn đang lên kế hoạch cập nhật một thực thể, nếu thực thể đó có nhiều thuộc tính (cột db) nhưng bạn chỉ muốn cập nhật một vài thuộc tính, bạn có thể thấy thuận lợi khi thực hiện một DbSet.Attach(entity)và sau đó chỉ cập nhật một vài thuộc tính cần cập nhật. Làm theo cách này sẽ tạo ra một câu lệnh cập nhật hiệu quả hơn từ EF. EF sẽ chỉ cập nhật các thuộc tính mà bạn đã sửa đổi (ngược lại context.Entry(entity).State = EntityState.Modified;sẽ làm cho tất cả các thuộc tính / cột được cập nhật)

Tài liệu liên quan: Thêm / Đính kèm và Thực thể Quốc gia .

Mã ví dụ

Giả sử bạn có thực thể sau:

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

Nếu mã của bạn trông như thế này:

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

SQL được tạo sẽ trông giống như sau:

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

Lưu ý cách câu lệnh cập nhật ở trên sẽ cập nhật tất cả các cột, bất kể bạn đã thực sự thay đổi các giá trị hay chưa.

Ngược lại, nếu mã của bạn sử dụng Đính kèm "bình thường" như thế này:

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

Sau đó, câu lệnh cập nhật được tạo sẽ khác:

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

Như bạn có thể thấy, câu lệnh cập nhật chỉ cập nhật các giá trị đã thực sự thay đổi sau khi bạn gắn thực thể vào ngữ cảnh. Tùy thuộc vào cấu trúc bảng của bạn, điều này có thể có tác động tích cực đến hiệu suất.

Bây giờ, lựa chọn nào tốt hơn cho bạn hoàn toàn phụ thuộc vào những gì bạn đang cố gắng làm.


1
EF không tạo mệnh đề WHERE theo cách này. Nếu bạn đính kèm một thực thể được tạo với mới (tức là Thực thể mới ()) và đặt nó thành đã sửa đổi, bạn phải đặt tất cả các trường ban đầu vì khóa lạc quan. Mệnh đề WHERE được tạo trong truy vấn UPDATE thường chứa tất cả các trường ban đầu (không chỉ Id) vì vậy nếu bạn không làm như vậy EF sẽ đưa ra một ngoại lệ đồng thời.
bubi

3
@budi: Cảm ơn bạn đã phản hồi. Tôi đã kiểm tra lại để chắc chắn và đối với một thực thể cơ bản, nó hoạt động như tôi đã mô tả, với WHEREmệnh đề chỉ chứa khóa chính và không có bất kỳ kiểm tra đồng thời nào. Để kiểm tra đồng thời, tôi cần định cấu hình rõ ràng một cột dưới dạng mã thông báo đồng thời hoặc rowVersion. Trong trường hợp đó, WHEREmệnh đề sẽ chỉ có khóa chính và cột mã thông báo đồng thời, không phải tất cả các trường. Nếu các bài kiểm tra của bạn cho thấy khác, tôi rất muốn nghe về nó.
sstan

Làm thế nào tôi có thể tự động tìm thấy tài sản phù thủy được sửa đổi?
Navid_pdp11

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

có thể hơi lạc đề, nhưng nếu tôi sử dụng một mẫu kho lưu trữ, tôi phải tạo một kho lưu trữ cho mọi mô hình vì mọi mô hình đều có một số thực thể cần ở trạng thái không được theo dõi trong khi chèn một bản ghi mới trong db, vì vậy tôi không thể có một kho lưu trữ chung gắn các thực thể vào ngữ cảnh trong quá trình chèn. Làm thế nào để bạn xử lý điều này tốt nhất?
jayasurya_j

3

Khi bạn sử dụng DbSet.Updatephương pháp, Entity Framework đánh dấu tất cả các thuộc tính của thực thể của bạn là EntityState.Modified, vì vậy hãy theo dõi chúng. Nếu bạn chỉ muốn thay đổi một số thuộc tính của mình, không phải tất cả chúng, hãy sử dụng DbSet.Attach. Phương thức này tạo ra tất cả các thuộc tính của bạn EntityState.Unchanged, vì vậy bạn phải tạo các thuộc tính mà bạn muốn cập nhật EntityState.Modified. Do đó, khi ứng dụng truy cập đến DbContext.SaveChanges, nó sẽ chỉ hoạt động các thuộc tính đã sửa đổi.


0

Ngoài (đối với câu trả lời được đánh dấu), có một sự khác biệt quan trọng giữa context.Entry(entity).State = EntityState.Unchangedcontext.Attach(entity)(trong EF Core):

Tôi đã thực hiện một số thử nghiệm để tự mình hiểu nó nhiều hơn (do đó điều này cũng bao gồm một số thử nghiệm tham chiếu chung), vì vậy đây là kịch bản thử nghiệm của tôi:

  • Tôi đã sử dụng EF Core 3.1.3
  • Tôi đã sử dụng QueryTrackingBehavior.NoTracking
  • Tôi chỉ sử dụng các thuộc tính để lập bản đồ (xem bên dưới)
  • Tôi đã sử dụng các ngữ cảnh khác nhau để nhận đơn hàng và cập nhật đơn hàng
  • Tôi đã xóa toàn bộ db cho mọi bài kiểm tra

Đây là các mô hình:

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; }
}

Đây là dữ liệu thử nghiệm (gốc) trong cơ sở dữ liệu: nhập mô tả hình ảnh ở đây

Để nhận đơn đặt hàng:

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

Bây giờ các bài kiểm tra:

Cập nhật đơn giản với 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

Cập nhật đơn giản với Đính kèm :

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

Cập nhật với việc thay đổi Id con với EntityState :

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

Cập nhật với việc thay đổi Id trẻ em với Đính kèm :

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

Lưu ý: Điều này ném Ngoại lệ, bất kể Id đã được thay đổi hoặc được đặt thành giá trị ban đầu, có vẻ như trạng thái của Id được đặt thành "đã thay đổi" và điều này không được phép (vì đó là khóa chính)

Cập nhật với việc thay đổi Id trẻ em như mới (không có sự khác biệt giữa EntityState và 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

Lưu ý: Xem sự khác biệt đối với Bản cập nhật với EntityState mà không có bản mới (ở trên). Lần này Tên sẽ được cập nhật, vì phiên bản Người dùng mới.

Cập nhật bằng cách thay đổi các Id tham chiếu với EntityState :

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

Cập nhật với việc thay đổi các Id Tham chiếu với Đính kèm :

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

Lưu ý: Các tài liệu tham khảo sẽ được thay đổi cho người dùng 3, nhưng cũng cho người dùng 1 sẽ được cập nhật, tôi đoán đây là do order.OrderedByUser.Idkhông thay đổi (nó vẫn còn 1).

Kết luận Với EntityState, bạn có nhiều quyền kiểm soát hơn, nhưng bạn phải tự mình cập nhật các thuộc tính phụ (cấp hai). Với Attach, bạn có thể cập nhật mọi thứ (tôi đoán với tất cả các cấp thuộc tính), nhưng bạn phải theo dõi các tài liệu tham khảo. Ví dụ: Nếu Người dùng (Người dùng đặt hàng) sẽ là một dropDown, thì việc thay đổi giá trị thông qua dropDown có thể ghi đè toàn bộ đối tượng Người dùng. Trong trường hợp này, dropDown-Value ban đầu sẽ bị ghi đè thay vì tham chiếu.

Đối với tôi trường hợp tốt nhất là đặt các đối tượng như OrderedByUser thành null và chỉ đặt order.OrderedByUserId thành giá trị mới, nếu tôi chỉ muốn thay đổi tham chiếu (bất kể là EntityState hay Attach).

Hy vọng điều này sẽ giúp ích, tôi biết nó là rất nhiều văn bản: D

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.