DbSet.Attach (เอนทิตี) กับ DbContext.Entry (เอนทิตี) .State = EntityState.Modified


115

เมื่อฉันอยู่ในสถานการณ์ที่แยกออกมาและได้รับ dto จากไคลเอนต์ที่ฉันแมปเป็นเอนทิตีเพื่อบันทึกฉันทำสิ่งนี้:

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

สำหรับอะไรแล้ว DbSet.Attach(entity)

หรือเหตุใดฉันจึงควรใช้เมธอด. Attach เมื่อ EntityState.Modified แนบเอนทิตีแล้ว?


เพิ่มข้อมูลรุ่นที่ดีกว่านี้ถูกถามก่อนหน้านี้ ฉันไม่ชัดเจนว่าสิ่งนี้สมควรได้รับคำถามใหม่หรือไม่
Henk Holterman

คำตอบ:


278

เมื่อคุณทำเช่น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.

สังเกตว่าคำสั่งอัพเดตข้างต้นจะอัปเดตคอลัมน์ทั้งหมดอย่างไรไม่ว่าคุณจะเปลี่ยนค่าจริงหรือไม่ก็ตาม

ในทางตรงกันข้ามหากรหัสของคุณใช้ไฟล์แนบ "ปกติ" เช่นนี้:

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 ด้วยวิธีนี้ หากคุณแนบเอนทิตีที่สร้างขึ้นด้วยใหม่ (เช่นเอนทิตีใหม่ ()) และตั้งค่าเป็นแก้ไขคุณต้องตั้งค่าฟิลด์เดิมทั้งหมดเนื่องจากการล็อกในแง่ดี คำสั่ง WHERE ที่สร้างขึ้นในแบบสอบถาม UPDATE มักจะมีฟิลด์ดั้งเดิมทั้งหมด (ไม่ใช่เฉพาะ Id) ดังนั้นหากคุณไม่ทำเช่นนั้น EF จะทำให้เกิดข้อยกเว้นการเกิดพร้อมกัน
bubi

3
@budi: ขอบคุณสำหรับคำติชม ฉันทดสอบอีกครั้งเพื่อให้แน่ใจและสำหรับเอนทิตีพื้นฐานมันทำงานตามที่ฉันอธิบายไว้โดยWHEREประโยคที่มีเฉพาะคีย์หลักและไม่มีการตรวจสอบการทำงานพร้อมกัน หากต้องการตรวจสอบการทำงานพร้อมกันฉันต้องกำหนดค่าคอลัมน์อย่างชัดเจนเป็นโทเค็นพร้อมกันหรือ rowVersion ในกรณีนั้นWHEREประโยคคำสั่งจะมีเฉพาะคีย์หลักและคอลัมน์โทเค็นพร้อมกันเท่านั้นไม่ใช่ทุกฟิลด์ หากการทดสอบของคุณแสดงเป็นอย่างอื่นฉันชอบที่จะได้ยินเกี่ยวกับเรื่องนี้
sstan

ฉันจะค้นหาคุณสมบัติแม่มดแบบไดนามิกได้อย่างไร
Navid_pdp11

2
@ Navid_pdp11 DbContext.Entry(person).CurrentValuesและDbContext.Entry(person).OriginalValues.
Shimmy Weitzhandler

อาจจะไม่ตรงกับหัวข้อเล็กน้อย แต่ถ้าฉันใช้รูปแบบที่เก็บฉันต้องสร้างที่เก็บสำหรับทุกโมเดลเนื่องจากทุกโมเดลมีเอนทิตีบางอย่างที่ต้องอยู่ในสถานะที่ไม่ถูกติดตามในขณะที่แทรกเร็กคอร์ดใหม่ใน db ดังนั้นฉันจึงไม่สามารถมีได้ ที่เก็บทั่วไปที่แนบเอนทิตีกับบริบทระหว่างการแทรก คุณจัดการเรื่องนี้อย่างไรให้ดีที่สุด?
jayasurya_j

3

เมื่อคุณใช้DbSet.Updateวิธีนี้ Entity Framework จะทำเครื่องหมายคุณสมบัติทั้งหมดของเอนทิตีของคุณเป็นEntityState.Modifiedดังนั้นติดตามคุณสมบัติเหล่านั้น DbSet.Attachหากคุณต้องการที่จะเปลี่ยนเพียงบางส่วนของคุณสมบัติของคุณไม่ใช่ทั้งหมดของพวกเขาใช้ วิธีการนี้จะทำให้คุณสมบัติทั้งหมดของคุณเพื่อให้คุณจะต้องทำให้คุณสมบัติของคุณว่าคุณต้องการที่จะปรับปรุงEntityState.Unchanged EntityState.Modifiedดังนั้นเมื่อแอปเข้าสู่DbContext.SaveChangesระบบจะใช้งานคุณสมบัติที่แก้ไขเท่านั้น


0

นอกจากนี้ (สำหรับคำตอบที่ทำเครื่องหมายไว้) ยังมีความแตกต่างที่สำคัญระหว่างcontext.Entry(entity).State = EntityState.Unchangedและcontext.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

อัปเดตด้วยการเปลี่ยน Child-Ids ด้วย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

อัปเดตด้วยการเปลี่ยนรหัสเด็กพร้อมไฟล์แนบ :

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

หมายเหตุ: ดูความแตกต่างของการอัปเดตด้วย EntityState ที่ไม่มีใหม่ (ด้านบน) คราวนี้ชื่อจะได้รับการอัปเดตเนื่องจากอินสแตนซ์ผู้ใช้ใหม่

อัปเดตด้วยการเปลี่ยนรหัสอ้างอิงด้วย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

อัปเดตด้วยการเปลี่ยนรหัสอ้างอิงพร้อมไฟล์แนบ :

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 (OrderByUser) เป็น dropDown การเปลี่ยนค่าผ่าน dropDown อาจถูกเขียนทับทั้ง User-object ในกรณีนี้ dropDown-Value ดั้งเดิมจะถูกเขียนทับแทนการอ้างอิง

สำหรับฉันกรณีที่ดีที่สุดคือการตั้งค่าวัตถุเช่น OrderByUser เป็น null และตั้งค่าคำสั่งเท่านั้น OrderByUserId เป็นค่าใหม่ถ้าฉันต้องการเปลี่ยนการอ้างอิงเท่านั้น (ไม่ว่า EntityState หรือ Attach)

หวังว่านี่จะช่วยได้ฉันรู้ว่ามันมีข้อความมากมาย: D

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.