ไม่สามารถเปลี่ยนแปลงความสัมพันธ์ได้เนื่องจากคุณสมบัติของ foreign-key หนึ่งรายการหรือมากกว่านั้นไม่สามารถยกเลิกได้


192

ฉันได้รับข้อผิดพลาดนี้เมื่อฉัน GetById () บนเอนทิตีแล้วตั้งค่าการรวบรวมเอนทิตีลูกเป็นรายการใหม่ของฉันซึ่งมาจากมุมมอง MVC

การดำเนินการล้มเหลว: ไม่สามารถเปลี่ยนความสัมพันธ์ได้เนื่องจากคุณสมบัติต่างประเทศหนึ่งรายการขึ้นไปไม่สามารถยกเลิกได้ เมื่อทำการเปลี่ยนแปลงกับความสัมพันธ์คุณสมบัติคีย์ foreign ที่เกี่ยวข้องจะถูกตั้งค่าเป็นค่าว่าง หาก foreign-key ไม่สนับสนุนค่า Null จะต้องกำหนดความสัมพันธ์ใหม่คุณสมบัติ foreign-key ต้องกำหนดค่าอื่นที่ไม่ใช่ค่า Null หรือลบวัตถุที่ไม่เกี่ยวข้องออก

ฉันไม่เข้าใจบรรทัดนี้มากนัก:

ไม่สามารถเปลี่ยนแปลงความสัมพันธ์ได้เนื่องจากคุณสมบัติของ foreign-key หนึ่งรายการหรือมากกว่านั้นไม่สามารถยกเลิกได้

ทำไมฉันต้องเปลี่ยนความสัมพันธ์ระหว่าง 2 เอนทิตี มันควรจะยังคงเหมือนเดิมตลอดอายุการใช้งานทั้งหมด

รหัสข้อยกเว้นที่เกิดขึ้นคือการกำหนดคลาสลูกที่แก้ไขแล้วในคอลเลกชันให้กับคลาสพาเรนต์ที่มีอยู่ หวังว่านี่จะรองรับการลบชั้นเรียนของเด็กการเพิ่มเข้ามาใหม่และการปรับเปลี่ยน ฉันคิดว่า Entity Framework จัดการกับสิ่งนี้

บรรทัดของรหัสสามารถกลั่นไปที่:

var thisParent = _repo.GetById(1);
thisParent.ChildItems = modifiedParent.ChildItems();
_repo.Save();

ฉันพบว่าคำตอบของฉันซื้อโดยใช้โซลูชัน # 2 ในบทความด้านล่างโดยทั่วไปฉันสร้างเพิ่มคีย์หลักในตารางลูกสำหรับการอ้างอิงไปยังตารางหลัก (ดังนั้นจึงมี 2 คีย์หลัก (คีย์ต่างประเทศสำหรับตารางหลักและ ID สำหรับตารางเด็ก) c-sharpcorner.com/UploadFile/ff2f08/…
yougotiger

@jaffa ฉันพบคำตอบของฉันที่นี่stackoverflow.com/questions/22858491/…
antonio

คำตอบ:


159

คุณควรลบรายการย่อยเก่าthisParent.ChildItemsทีละรายการด้วยตนเอง Entity Framework ไม่ได้ทำเพื่อคุณ ในที่สุดมันก็ไม่สามารถตัดสินใจได้ว่าคุณต้องการจะทำอะไรกับไอเท็มลูกเก่า - ถ้าคุณต้องการที่จะทิ้งมันไป คุณต้องบอก Entity Framework การตัดสินใจของคุณ แต่หนึ่งในสองการตัดสินใจที่คุณต้องทำเนื่องจากเอนทิตีลูกไม่สามารถอยู่คนเดียวโดยไม่มีการอ้างอิงถึงพาเรนต์ใด ๆ ในฐานข้อมูล (เนื่องจากข้อ จำกัด คีย์ต่างประเทศ) นั่นเป็นสิ่งที่ข้อยกเว้นบอกไว้

แก้ไข

ฉันควรทำอย่างไรหากสามารถเพิ่มอัปเดตและลบรายการเด็กได้

public void UpdateEntity(ParentItem parent)
{
    // Load original parent including the child item collection
    var originalParent = _dbContext.ParentItems
        .Where(p => p.ID == parent.ID)
        .Include(p => p.ChildItems)
        .SingleOrDefault();
    // We assume that the parent is still in the DB and don't check for null

    // Update scalar properties of parent,
    // can be omitted if we don't expect changes of the scalar properties
    var parentEntry = _dbContext.Entry(originalParent);
    parentEntry.CurrentValues.SetValues(parent);

    foreach (var childItem in parent.ChildItems)
    {
        var originalChildItem = originalParent.ChildItems
            .Where(c => c.ID == childItem.ID && c.ID != 0)
            .SingleOrDefault();
        // Is original child item with same ID in DB?
        if (originalChildItem != null)
        {
            // Yes -> Update scalar properties of child item
            var childEntry = _dbContext.Entry(originalChildItem);
            childEntry.CurrentValues.SetValues(childItem);
        }
        else
        {
            // No -> It's a new child item -> Insert
            childItem.ID = 0;
            originalParent.ChildItems.Add(childItem);
        }
    }

    // Don't consider the child items we have just added above.
    // (We need to make a copy of the list by using .ToList() because
    // _dbContext.ChildItems.Remove in this loop does not only delete
    // from the context but also from the child collection. Without making
    // the copy we would modify the collection we are just interating
    // through - which is forbidden and would lead to an exception.)
    foreach (var originalChildItem in
                 originalParent.ChildItems.Where(c => c.ID != 0).ToList())
    {
        // Are there child items in the DB which are NOT in the
        // new child item collection anymore?
        if (!parent.ChildItems.Any(c => c.ID == originalChildItem.ID))
            // Yes -> It's a deleted child item -> Delete
            _dbContext.ChildItems.Remove(originalChildItem);
    }

    _dbContext.SaveChanges();
}

หมายเหตุ: นี่ไม่ได้ทดสอบ ICollectionมันสมมติว่าคอลเลกชันรายการเด็กเป็นประเภท (ฉันมักจะมีIListแล้วรหัสที่มีลักษณะแตกต่างกันเล็กน้อย) ฉันยังได้แยก abstractions ที่เก็บทั้งหมดออกเพื่อให้ง่าย

ฉันไม่รู้ว่านี่เป็นวิธีแก้ปัญหาที่ดีหรือไม่ แต่ฉันเชื่อว่าการทำงานอย่างหนักในสายเหล่านี้จะต้องทำเพื่อดูแลการเปลี่ยนแปลงทุกชนิดในคอลเลกชันการนำทาง ฉันก็ยินดีที่จะเห็นวิธีที่ง่ายกว่าในการทำเช่นนั้น


ดังนั้นจะเกิดอะไรขึ้นถ้าบางอย่างเปลี่ยนไปเท่านั้น? หมายความว่าฉันยังต้องลบออกและเพิ่มอีกครั้งหรือไม่
jaffa

@ จอน: ไม่คุณสามารถอัปเดตรายการที่มีอยู่แน่นอน ฉันได้เพิ่มตัวอย่างว่าฉันจะอัปเดตคอลเลกชันลูกได้อย่างไรโปรดดูที่ส่วนแก้ไขด้านบน
Slauma

@Slauma: ฮ่า ๆ ถ้าผมรู้ว่าคุณกำลังจะไปปรับเปลี่ยนคำตอบของคุณฉันจะไม่เขียนคำตอบของฉัน ...
Ladislav Mrnka

@ Ladislav: ไม่ไม่ฉันดีใจที่คุณเขียนคำตอบของคุณเอง อย่างน้อยตอนนี้ฉันก็รู้ว่ามันไม่ได้ไร้สาระสมบูรณ์และซับซ้อนเกินกว่าที่ฉันได้ทำไป
Slauma

1
ฉันจะเพิ่มเงื่อนไขเมื่อดึงข้อมูล originalChildItem ใน foreach: ... โดยที่ (c => c.ID == childItem.ID && c.ID! = 0) มิฉะนั้นจะส่งคืนเด็กที่เพิ่มเข้ามาใหม่ถ้า childItem.ID == 0.
perfect_element

116

เหตุผลที่คุณกำลังเผชิญนี้เป็นเพราะความแตกต่างระหว่างองค์ประกอบและการรวมตัว

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

ในการรวมตัววัตถุเด็กสามารถอยู่ได้โดยไม่คำนึงถึงผู้ปกครอง หากพาเรนต์ถูกทำลายวัตถุลูกก็ยังคงมีอยู่เพราะมันอาจถูกเพิ่มลงในพาเรนต์อื่นในภายหลัง เช่นความสัมพันธ์ระหว่างเพลย์ลิสต์กับเพลงในเพลย์ลิสต์นั้น หากลบเพลย์ลิสต์ไม่ควรลบเพลง อาจถูกเพิ่มไปยังเพลย์ลิสต์อื่น

วิธีที่ Entity Framework สร้างความแตกต่างระหว่างความสัมพันธ์ในการรวมและองค์ประกอบมีดังนี้:

  • สำหรับการจัดวาง: คาดว่าวัตถุลูกจะมีคีย์หลักรวม (ParentID, ChildID) นี่คือการออกแบบโดย ID ของเด็กควรจะอยู่ในขอบเขตของพ่อแม่ของพวกเขา

  • สำหรับการรวม: คาดว่าคุณสมบัติ foreign key ในวัตถุลูกจะเป็นโมฆะ

ดังนั้นเหตุผลที่คุณมีปัญหานี้เป็นเพราะวิธีการที่คุณตั้งค่าคีย์หลักของคุณในตารางลูกของคุณ มันควรจะเป็นคอมโพสิต แต่ไม่ใช่ ดังนั้น Entity Framework จึงเห็นการเชื่อมโยงนี้เป็นการรวมซึ่งหมายความว่าเมื่อคุณลบหรือล้างวัตถุลูกมันจะไม่ลบระเบียนลูก มันจะลบการเชื่อมโยงและตั้งค่าคอลัมน์คีย์ต่างประเทศที่เกี่ยวข้องเป็น NULL (เพื่อให้ระเบียนลูกเหล่านั้นสามารถเชื่อมโยงกับผู้ปกครองอื่นในภายหลัง) เนื่องจากคอลัมน์ของคุณไม่อนุญาตให้มีค่า NULL คุณจะได้รับข้อยกเว้นตามที่ระบุไว้

Solutions:

1- ถ้าคุณมีเหตุผลที่ดีที่ไม่ต้องการใช้คีย์ผสมคุณต้องลบวัตถุลูกอย่างชัดเจน และสิ่งนี้สามารถทำได้ง่ายกว่าโซลูชันที่แนะนำไว้ก่อนหน้านี้:

context.Children.RemoveRange(parent.Children);

2- มิฉะนั้นโดยการตั้งค่าคีย์หลักที่เหมาะสมบนตารางลูกรหัสของคุณจะมีความหมายมากขึ้น:

parent.Children.Clear();

9
ฉันพบว่าคำอธิบายนี้มีประโยชน์มากที่สุด
Booji Boy

7
คำอธิบายที่ดีสำหรับการจัดวางองค์ประกอบและการรวมและความสัมพันธ์ของกรอบงานเอนทิตี
Chrysalis

# 1 มีจำนวนรหัสน้อยที่สุดที่จำเป็นในการแก้ไขปัญหา ขอบคุณ!
ryanulit

73

นี่เป็นปัญหาใหญ่มาก สิ่งที่เกิดขึ้นจริงในรหัสของคุณคือ:

  • คุณโหลดParentจากฐานข้อมูลและรับเอนทิตีที่แนบมา
  • คุณแทนที่คอลเลกชันย่อยเป็นคอลเลกชันใหม่ของลูกเดี่ยว
  • คุณบันทึกการเปลี่ยนแปลง แต่ในระหว่างการดำเนินการนี้เด็ก ๆ ทุกคนจะได้รับการพิจารณาว่าเป็นเด็กที่เพิ่มเนื่องจากEF ไม่รู้เกี่ยวกับพวกเขาจนถึงเวลานี้ ดังนั้น EF จึงพยายามตั้งค่า null ให้เป็น foreign key ของเด็กโตและแทรก children ใหม่ทั้งหมด => แถวที่ซ้ำกัน

ตอนนี้โซลูชันขึ้นอยู่กับสิ่งที่คุณต้องการทำและคุณต้องการทำอย่างไร

หากคุณใช้ ASP.NET MVC คุณสามารถลองใช้UpdateModel หรือ TryUpdateModelได้

หากคุณต้องการเพียงอัปเดตเด็กที่มีอยู่ด้วยตนเองคุณสามารถทำสิ่งต่อไปนี้ได้:

foreach (var child in modifiedParent.ChildItems)
{
    context.Childs.Attach(child); 
    context.Entry(child).State = EntityState.Modified;
}

context.SaveChanges();

ไม่จำเป็นต้องModifiedทำการแนบ(การตั้งค่าสถานะเป็นจะแนบเอนทิตีด้วย) แต่ฉันชอบเพราะมันทำให้กระบวนการชัดเจนขึ้น

หากคุณต้องการแก้ไขที่มีอยู่ให้ลบที่มีอยู่แล้วใส่ลูกใหม่คุณต้องทำสิ่งต่อไปนี้:

var parent = context.Parents.GetById(1); // Make sure that childs are loaded as well
foreach(var child in modifiedParent.ChildItems)
{
    var attachedChild = FindChild(parent, child.Id);
    if (attachedChild != null)
    {
        // Existing child - apply new values
        context.Entry(attachedChild).CurrentValues.SetValues(child);
    }
    else
    {
        // New child
        // Don't insert original object. It will attach whole detached graph
        parent.ChildItems.Add(child.Clone());
    }
}

// Now you must delete all entities present in parent.ChildItems but missing
// in modifiedParent.ChildItems
// ToList should make copy of the collection because we can't modify collection
// iterated by foreach
foreach(var child in parent.ChildItems.ToList())
{
    var detachedChild = FindChild(modifiedParent, child.Id);
    if (detachedChild == null)
    {
        parent.ChildItems.Remove(child);
        context.Childs.Remove(child); 
    }
}

context.SaveChanges();

1
แต่มีข้อสังเกตที่น่าสนใจเกี่ยวกับการใช้ของ.Clone()คุณ คุณมีกรณีที่ChildItemมีคุณสมบัติการนำทางย่อยอื่น ๆ หรือไม่? แต่ในกรณีนั้นเราจะไม่ต้องการให้กราฟย่อยทั้งหมดเชื่อมโยงกับบริบทเนื่องจากเราคาดหวังว่า sub-child ทั้งหมดเป็นวัตถุใหม่หากเด็กนั้นเป็นของใหม่ (ดีอาจจะแตกต่างจากรุ่นต่อรุ่น แต่สมมติกรณีที่ย่อยของเด็กคือ "ขึ้นอยู่กับ" จากเด็กเช่นพระเกศาจะขึ้นอยู่จากแม่.)
Slauma

มันอาจจะต้องมีการ "ฉลาด" โคลน
Ladislav Mrnka

1
ถ้าคุณไม่ต้องการมีคอลเลกชันของเด็กในบริบทของคุณ http://stackoverflow.com/questions/20233994/do-i-need-to-create-a-dbset-for-every-table-so-that-i-can-persist-child-entitie
Kirsten Greed

1
parent.ChildItems.Remove (เด็ก); context.Childs.Remove (เด็ก); ลบสองครั้งนี้คงออกอาจขอบคุณ ทำไมเราจึงต้องนำทั้งสองออก เหตุใดการลบออกจากผู้ปกครองเด็กรายการนี้ไม่ได้เกิดขึ้นเนื่องจากเด็กมีชีวิตเพียงเป็นเด็ก
เฟอร์นันโดตอร์เรส

40

ฉันพบว่าคำตอบนี้มีประโยชน์มากสำหรับข้อผิดพลาดเดียวกัน ดูเหมือนว่า EF ไม่ชอบเมื่อคุณลบมันชอบลบ

คุณสามารถลบคอลเลกชันของระเบียนที่แนบกับระเบียนเช่นนี้

order.OrderDetails.ToList().ForEach(s => db.Entry(s).State = EntityState.Deleted);

ในตัวอย่างระเบียนรายละเอียดทั้งหมดที่แนบกับคำสั่งซื้อจะมีสถานะเป็นลบ (กำลังเตรียมที่จะเพิ่มรายละเอียดที่อัปเดตแล้วกลับซึ่งเป็นส่วนหนึ่งของการอัปเดตคำสั่งซื้อ)


ฉันเชื่อว่ามันเป็นคำตอบที่เหมาะสม
desmati

วิธีการแก้ปัญหาเชิงตรรกะและตรงไปตรงมา
sairfan

19

ฉันไม่รู้ว่าทำไมคำตอบอีกสองคำถึงได้รับความนิยมมาก!

ฉันเชื่อว่าคุณมีสิทธิ์ที่จะสมมติกรอบ ORM ควรจัดการ - นั่นคือสิ่งที่มันสัญญาว่าจะส่งมอบ มิฉะนั้นรูปแบบโดเมนของคุณจะได้รับความเสียหายเนื่องจากความกังวลที่ยังมีอยู่ NHibernate จัดการสิ่งนี้อย่างมีความสุขหากคุณตั้งค่าการตั้งค่าแบบเรียงซ้อนอย่างถูกต้อง ใน Entity Framework เป็นไปได้ด้วยพวกเขาคาดหวังว่าคุณจะปฏิบัติตามมาตรฐานที่ดีกว่าเมื่อตั้งค่าโมเดลฐานข้อมูลของคุณโดยเฉพาะเมื่อพวกเขาต้องสรุปว่าควรจะทำอย่างไร

คุณต้องกำหนดความสัมพันธ์พาเรนต์ - ลูกอย่างถูกต้องโดยใช้ "การระบุความสัมพันธ์ "

ถ้าคุณทำเช่นนี้ Entity Framework รู้ว่าวัตถุลูกถูกระบุโดยผู้ปกครองและดังนั้นมันจะต้องเป็นสถานการณ์ "cascade-delete-orphans"

นอกเหนือจากข้างต้นคุณอาจต้อง (จากประสบการณ์ของ NHibernate)

thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);

แทนการแทนที่รายการทั้งหมด

UPDATE

@ ความคิดเห็นของ Slauma เตือนฉันว่าสิ่งที่แยกออกเป็นอีกส่วนหนึ่งของปัญหาโดยรวม ในการแก้ปัญหานั้นคุณสามารถใช้วิธีการใช้ตัวยึดโมเดลแบบกำหนดเองที่สร้างแบบจำลองของคุณโดยพยายามโหลดจากบริบท โพสต์บล็อกนี้แสดงตัวอย่างสิ่งที่ฉันหมายถึง


การตั้งค่าเป็นการระบุความสัมพันธ์จะไม่ช่วยได้ที่นี่เพราะสถานการณ์ในคำถามต้องจัดการกับเอนทิตีที่แยกออกมา ( "รายการใหม่ของฉันซึ่งมาจากมุมมอง MVC" ) คุณยังคงต้องโหลดลูกเดิมจากฐานข้อมูลค้นหารายการที่ถูกลบในคอลเลกชันนั้นโดยยึดตามคอลเลกชันเดี่ยวและจากนั้นลบออกจากฐานข้อมูล แต่ที่แตกต่างคือมีความสัมพันธ์ที่ระบุคุณสามารถเรียกแทนparent.ChildItems.Remove _dbContext.ChildItems.Removeยังคงมี (EF <= 6) ไม่มีการรองรับในตัวจาก EF เพื่อหลีกเลี่ยงรหัสที่มีความยาวเหมือนคำตอบอื่น
Slauma

ฉันเข้าใจประเด็นของคุณ อย่างไรก็ตามฉันเชื่อว่าด้วยตัวยึดโมเดลแบบกำหนดเองที่โหลดเอนทิตีจากบริบทหรือส่งคืนอินสแตนซ์ใหม่ที่วิธีการดังกล่าวใช้งานได้ ฉันจะอัปเดตคำตอบของฉันเพื่อแนะนำวิธีแก้ปัญหานั้น
Andre Luus

ใช่คุณสามารถใช้เครื่องผูกโมเดล แต่คุณต้องทำสิ่งต่าง ๆ จากคำตอบอื่น ๆ ในเครื่องผูกโมเดลตอนนี้ มันแค่ย้ายปัญหาจาก repo / service layer ไปยัง model binder อย่างน้อยฉันก็ไม่เห็นความเรียบง่ายจริง ๆ
Slauma

การทำให้เข้าใจง่ายคือการลบเอนทิตีที่กำพร้าออกโดยอัตโนมัติ สิ่งที่คุณต้องการในเครื่องผูกโมเดลนั้นเทียบเท่ากับreturn context.Items.Find(id) ?? new Item()
Andre Luus

ผลตอบรับที่ดีสำหรับทีม EF แต่โซลูชันที่คุณนำเสนอไม่ได้แก้ปัญหาอะไรในที่ดิน EF
Chris Moschini

9

ถ้าคุณใช้ AutoMapper กับ Entity Framework ในคลาสเดียวกันคุณอาจประสบปัญหานี้ เช่นถ้าชั้นเรียนของคุณเป็น

class A
{
    public ClassB ClassB { get; set; }
    public int ClassBId { get; set; }
}

AutoMapper.Map<A, A>(input, destination);

สิ่งนี้จะพยายามคัดลอกคุณสมบัติทั้งสองอย่าง ในกรณีนี้ ClassBId ไม่เป็น Nullable เนื่องจาก AutoMapper จะคัดลอกdestination.ClassB = input.ClassB;สิ่งนี้จะทำให้เกิดปัญหา

ตั้งค่าClassBคุณสมบัติAutoMapper เป็นละเว้น

 cfg.CreateMap<A, A>()
     .ForMember(m => m.ClassB, opt => opt.Ignore()); // We use the ClassBId

ฉันกำลังเผชิญกับปัญหาที่คล้ายกันกับ AutoMapper แต่สิ่งนี้ไม่ได้ผลสำหรับฉัน :( ดูstackoverflow.com/q/41430679/613605
J86

4

ฉันเพิ่งมีข้อผิดพลาดเดียวกัน ฉันมีสองตารางที่มีความสัมพันธ์ลูกแม่ แต่ฉันกำหนดค่า "ในการลบทั้งหมด" ในคอลัมน์คีย์ต่างประเทศในคำนิยามตารางของตารางเด็ก ดังนั้นเมื่อฉันลบแถวผู้ปกครอง (ผ่าน SQL) ในฐานข้อมูลด้วยตนเองจะลบแถวลูกโดยอัตโนมัติ

อย่างไรก็ตามสิ่งนี้ไม่ได้ผลใน EF ข้อผิดพลาดที่อธิบายในเธรดนี้ปรากฏขึ้น เหตุผลสำหรับเรื่องนี้คือในโมเดลข้อมูลเอนทิตีของฉัน (ไฟล์ edmx) คุณสมบัติของการเชื่อมโยงระหว่างพาเรนต์และตารางลูกไม่ถูกต้อง End1 OnDeleteตัวเลือกที่ได้รับการกำหนดค่าให้เป็นnone( "End1" ในรูปแบบของฉันคือท้ายที่สุดซึ่งมีหลายหลาก 1)

ฉันเปลี่ยนตัวEnd1 OnDeleteเลือกเป็นCascadeและมันใช้งานได้ด้วยตนเอง ฉันไม่รู้ว่าทำไม EF จึงไม่สามารถรับได้เมื่อฉันอัปเดตโมเดลจากฐานข้อมูล (ฉันมีฐานข้อมูลรุ่นแรก)

เพื่อความสมบูรณ์นี่คือลักษณะที่โค้ดของฉันที่จะลบ:

   public void Delete(int id)
    {
        MyType myObject = _context.MyTypes.Find(id);

        _context.MyTypes.Remove(myObject);
        _context.SaveChanges(); 
   }    

หากฉันไม่ได้กำหนดการลบเรียงซ้อนฉันจะต้องลบแถวลูกด้วยตนเองก่อนที่จะลบแถวพาเรนต์


4

สิ่งนี้เกิดขึ้นเนื่องจากเอนทิตี Child ถูกทำเครื่องหมายเป็น Modified แทนที่จะถูกลบ

และการปรับเปลี่ยนที่ EF ไม่ให้กิจการเด็กเมื่อมีการดำเนินการเป็นเพียงการตั้งค่าการอ้างอิงไปยังผู้ปกครองในการparent.Remove(child)null

คุณสามารถตรวจสอบ EntityState ของเด็กโดยพิมพ์รหัสต่อไปนี้ลงในหน้าต่างทันทีของ Visual Studio เมื่อเกิดข้อยกเว้นหลังจากดำเนินการSaveChanges():

_context.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Modified).ElementAt(X).Entity

โดยที่ X ควรถูกแทนที่ด้วย Entity ที่ถูกลบ

หากคุณไม่มีสิทธิ์เข้าถึงเพื่อObjectContextดำเนินการ_context.ChildEntity.Remove(child)คุณสามารถแก้ไขปัญหานี้ได้โดยกำหนดให้ foreign key เป็นส่วนหนึ่งของคีย์หลักบนตารางลูก

Parent
 ________________
| PK    IdParent |
|       Name     |
|________________|

Child
 ________________
| PK    IdChild  |
| PK,FK IdParent |
|       Name     |
|________________|

ด้วยวิธีนี้หากคุณดำเนินการparent.Remove(child)EF จะทำเครื่องหมายเอนทิตีว่าถูกลบอย่างถูกต้อง


2

วิธีแก้ปัญหาแบบนี้ทำเพื่อฉัน:

Parent original = db.Parent.SingleOrDefault<Parent>(t => t.ID == updated.ID);
db.Childs.RemoveRange(original.Childs);
updated.Childs.ToList().ForEach(c => original.Childs.Add(c));
db.Entry<Parent>(original).CurrentValues.SetValues(updated);

มันสำคัญที่จะบอกว่านี่เป็นการลบข้อมูลทั้งหมดและใส่มันอีกครั้ง แต่สำหรับกรณีของฉัน (น้อยกว่า 10) ก็โอเค

ฉันหวังว่ามันจะช่วย


การแทรกซ้ำเกิดขึ้นกับ ID ใหม่หรือไม่หรือจะเก็บ ID ของเด็กไว้ตั้งแต่แรก?
Pepito Fernandez

2

ฉันพบปัญหานี้ในวันนี้และต้องการแบ่งปันวิธีแก้ปัญหาของฉัน ในกรณีของฉันวิธีแก้ไขคือลบรายการชายด์ก่อนนำพาเรนต์ออกจากฐานข้อมูล

ก่อนหน้านี้ฉันทำเหมือนในรหัสด้านล่าง ฉันจะได้รับข้อความแสดงข้อผิดพลาดเดียวกันในคำถามนี้

var Parent = GetParent(parentId);
var children = Parent.Children;
foreach (var c in children )
{
     Context.Children.Remove(c);
}
Context.SaveChanges();

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

var children = GetChildren(parentId);
foreach (var c in children )
{
     Context.Children.Remove(c);
}
Context.SaveChanges();

var Parent = GetParent(parentId);
Parent.Children = //assign new entities/items here

2

คุณต้องล้างคอลเลกชัน ChildItems ด้วยตนเองและผนวกรายการใหม่เข้าไปด้วย:

thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);

หลังจากนั้นคุณสามารถเรียกใช้เมธอดส่วนขยาย DeleteOrphans ซึ่งจะจัดการกับเอนทิตีที่กำพร้า (ต้องถูกเรียกระหว่างเมธอด DetectChanges และ SaveChanges)

public static class DbContextExtensions
{
    private static readonly ConcurrentDictionary< EntityType, ReadOnlyDictionary< string, NavigationProperty>> s_navPropMappings = new ConcurrentDictionary< EntityType, ReadOnlyDictionary< string, NavigationProperty>>();

    public static void DeleteOrphans( this DbContext source )
    {
        var context = ((IObjectContextAdapter)source).ObjectContext;
        foreach (var entry in context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified))
        {
            var entityType = entry.EntitySet.ElementType as EntityType;
            if (entityType == null)
                continue;

            var navPropMap = s_navPropMappings.GetOrAdd(entityType, CreateNavigationPropertyMap);
            var props = entry.GetModifiedProperties().ToArray();
            foreach (var prop in props)
            {
                NavigationProperty navProp;
                if (!navPropMap.TryGetValue(prop, out navProp))
                    continue;

                var related = entry.RelationshipManager.GetRelatedEnd(navProp.RelationshipType.FullName, navProp.ToEndMember.Name);
                var enumerator = related.GetEnumerator();
                if (enumerator.MoveNext() && enumerator.Current != null)
                    continue;

                entry.Delete();
                break;
            }
        }
    }

    private static ReadOnlyDictionary<string, NavigationProperty> CreateNavigationPropertyMap( EntityType type )
    {
        var result = type.NavigationProperties
            .Where(v => v.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many)
            .Where(v => v.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One || (v.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne && v.FromEndMember.GetEntityType() == v.ToEndMember.GetEntityType()))
            .Select(v => new { NavigationProperty = v, DependentProperties = v.GetDependentProperties().Take(2).ToArray() })
            .Where(v => v.DependentProperties.Length == 1)
            .ToDictionary(v => v.DependentProperties[0].Name, v => v.NavigationProperty);

        return new ReadOnlyDictionary<string, NavigationProperty>(result);
    }
}

นี้ทำงานได้ดีสำหรับฉัน. context.DetectChanges();ผมเพียงแค่ต้องการที่จะเพิ่ม
Andy Edinborough

1

ฉันได้ลองใช้วิธีแก้ไขปัญหาเหล่านี้และอื่น ๆ อีกมากมาย แต่ไม่มีวิธีใดที่ทำได้ดีพอ เนื่องจากนี่เป็นคำตอบแรกบน google ฉันจะเพิ่มโซลูชันของฉันที่นี่

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

var toDelete = db.Parents.Find(parentObject.ID);
db.Parents.Remove(toDelete);
db.SaveChanges();

โปรดทราบว่าสิ่งนี้ถือว่าคีย์ต่างประเทศถูกตั้งค่าด้วย ON DELETE CASCADE ดังนั้นเมื่อลบแถวพาเรนต์ลูกจะถูกล้างข้อมูลโดยฐานข้อมูล


1

ฉันใช้วิธีการแก้ปัญหาของ Moshแต่ฉันก็ไม่เห็นชัดเจนว่าจะใช้รหัสองค์ประกอบอย่างถูกต้องในรหัสก่อนได้อย่างไร

ดังนั้นนี่คือทางออก:

public class Holiday
{
    [Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int HolidayId { get; set; }
    [Key, Column(Order = 1), ForeignKey("Location")]
    public LocationEnum LocationId { get; set; }

    public virtual Location Location { get; set; }

    public DateTime Date { get; set; }
    public string Name { get; set; }
}

1

ฉันมีปัญหาเดียวกัน แต่ฉันรู้ว่ามันใช้ได้ดีในกรณีอื่น ๆ ดังนั้นฉันจึงลดปัญหานี้:

parent.OtherRelatedItems.Clear();  //this worked OK on SaveChanges() - items were being deleted from DB
parent.ProblematicItems.Clear();   // this was causing the mentioned exception on SaveChanges()
  • OtherRelatedItemsมีคีย์หลักผสม (parentId + คอลัมน์ท้องถิ่นบางส่วน) และทำงานได้ดี
  • ProblematicItemsมีคีย์หลักแบบคอลัมน์เดี่ยวของตนเองและ parentId เป็นเพียง FK นี่เป็นสาเหตุของข้อยกเว้นหลังจาก Clear ()

สิ่งที่ฉันต้องทำก็คือทำให้ ParentId เป็นส่วนหนึ่งของ PK คอมโพสิตเพื่อระบุว่าเด็ก ๆ จะไม่มีตัวตนหากไม่มีพ่อแม่ ฉันใช้โมเดล DB แรกเพิ่ม PK และทำเครื่องหมายคอลัมน์ parentId เป็น EntityKey (ดังนั้นฉันต้องอัปเดตทั้งใน DB และ EF - ไม่แน่ใจว่า EF เพียงอย่างเดียวจะเพียงพอหรือไม่

ฉันขอ RequestId เป็นส่วนหนึ่งของ PK จากนั้นอัปเดตโมเดล EF และตั้งค่าคุณสมบัติอื่นเป็นส่วนหนึ่งของรหัส Entity

เมื่อคุณคิดถึงมันเป็นความแตกต่างที่หรูหรามากที่ EF ใช้ในการตัดสินใจว่าเด็ก ๆ "เข้าท่า" โดยไม่มีพ่อแม่ (ในกรณีนี้ชัดเจน () จะไม่ลบพวกเขาและโยนข้อยกเว้นเว้นแต่คุณจะตั้งค่า ParentId เป็นอย่างอื่น / พิเศษ ) หรือ - เช่นเดียวกับคำถามเดิม - เราคาดว่ารายการจะถูกลบเมื่อรายการเหล่านั้นถูกลบออกจากรายการหลัก


0

ปัญหานี้เกิดขึ้นเพราะเราพยายามที่จะลบตารางแม่ยังคงมีข้อมูลตารางลูก เราแก้ปัญหาด้วยความช่วยเหลือในการลบน้ำตก

ในรูปแบบการสร้างวิธีการในคลาส dbcontext

 modelBuilder.Entity<Job>()
                .HasMany<JobSportsMapping>(C => C.JobSportsMappings)
                .WithRequired(C => C.Job)
                .HasForeignKey(C => C.JobId).WillCascadeOnDelete(true);
            modelBuilder.Entity<Sport>()
                .HasMany<JobSportsMapping>(C => C.JobSportsMappings)
                  .WithRequired(C => C.Sport)
                  .HasForeignKey(C => C.SportId).WillCascadeOnDelete(true);

หลังจากนั้นในการโทร API ของเรา

var JobList = Context.Job                       
          .Include(x => x.JobSportsMappings)                                     .ToList();
Context.Job.RemoveRange(JobList);
Context.SaveChanges();

น้ำตกลบตัวเลือกลบผู้ปกครองตลอดจนผู้ปกครองที่เกี่ยวข้องกับตารางเด็กด้วยรหัสนี้ง่าย ลองใช้วิธีง่ายๆ

ลบช่วงซึ่งใช้สำหรับลบรายการของระเบียนในฐานข้อมูลขอบคุณ


0

ฉันยังแก้ไขปัญหาของฉันด้วยคำตอบของ Moshและฉันคิดว่าคำตอบของ PeterBนั้นเล็กน้อยเพราะใช้ enum เป็นคีย์ต่างประเทศ จำไว้ว่าคุณจะต้องเพิ่มการย้ายข้อมูลใหม่หลังจากเพิ่มรหัสนี้

ฉันยังสามารถแนะนำโพสต์บล็อกนี้สำหรับโซลูชั่นอื่น ๆ :

http://www.kianryan.co.uk/2013/03/orphaned-child/

รหัส:

public class Child
{
    [Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public string Heading { get; set; }
    //Add other properties here.

    [Key, Column(Order = 1)]
    public int ParentId { get; set; }

    public virtual Parent Parent { get; set; }
}

0

การใช้โซลูชันของ Slauma ฉันสร้างฟังก์ชันทั่วไปบางอย่างเพื่อช่วยปรับปรุงวัตถุลูกและชุดวัตถุย่อย

วัตถุถาวรทั้งหมดของฉันใช้อินเตอร์เฟสนี้

/// <summary>
/// Base interface for all persisted entries
/// </summary>
public interface IBase
{
    /// <summary>
    /// The Id
    /// </summary>
    int Id { get; set; }
}

ด้วยสิ่งนี้ฉันได้ใช้ฟังก์ชั่นทั้งสองนี้ในที่เก็บของฉัน

    /// <summary>
    /// Check if orgEntry is set update it's values, otherwise add it
    /// </summary>
    /// <param name="set">The collection</param>
    /// <param name="entry">The entry</param>
    /// <param name="orgEntry">The original entry found in the database (can be <code>null</code> is this is a new entry)</param>
    /// <returns>The added or updated entry</returns>
    public T AddOrUpdateEntry<T>(DbSet<T> set, T entry, T orgEntry) where T : class, IBase
    {
        if (entry.Id == 0 || orgEntry == null)
        {
            entry.Id = 0;
            return set.Add(entry);
        }
        else
        {
            Context.Entry(orgEntry).CurrentValues.SetValues(entry);
            return orgEntry;
        }
    }

    /// <summary>
    /// check if each entry of the new list was in the orginal list, if found, update it, if not found add it
    /// all entries found in the orignal list that are not in the new list are removed
    /// </summary>
    /// <typeparam name="T">The type of entry</typeparam>
    /// <param name="set">The database set</param>
    /// <param name="newList">The new list</param>
    /// <param name="orgList">The original list</param>
    public void AddOrUpdateCollection<T>(DbSet<T> set, ICollection<T> newList, ICollection<T> orgList) where T : class, IBase
    {
        // attach or update all entries in the new list
        foreach (T entry in newList)
        {
            // Find out if we had the entry already in the list
            var orgEntry = orgList.SingleOrDefault(e => e.Id != 0 && e.Id == entry.Id);

            AddOrUpdateEntry(set, entry, orgEntry);
        }

        // Remove all entries from the original list that are no longer in the new list
        foreach (T orgEntry in orgList.Where(e => e.Id != 0).ToList())
        {
            if (!newList.Any(e => e.Id == orgEntry.Id))
            {
                set.Remove(orgEntry);
            }
        }
    }

หากต้องการใช้ฉันทำดังต่อไปนี้:

var originalParent = _dbContext.ParentItems
    .Where(p => p.Id == parent.Id)
    .Include(p => p.ChildItems)
    .Include(p => p.ChildItems2)
    .SingleOrDefault();

// Add the parent (including collections) to the context or update it's values (except the collections)
originalParent = AddOrUpdateEntry(_dbContext.ParentItems, parent, originalParent);

// Update each collection
AddOrUpdateCollection(_dbContext.ChildItems, parent.ChildItems, orgiginalParent.ChildItems);
AddOrUpdateCollection(_dbContext.ChildItems2, parent.ChildItems2, orgiginalParent.ChildItems2);

หวังว่านี่จะช่วยได้


พิเศษ: คุณสามารถสร้างคลาส DbContextExtentions (หรือบริบทด้านล่างของคุณเอง) แยก:

public static void DbContextExtentions {
    /// <summary>
    /// Check if orgEntry is set update it's values, otherwise add it
    /// </summary>
    /// <param name="_dbContext">The context object</param>
    /// <param name="set">The collection</param>
    /// <param name="entry">The entry</param>
    /// <param name="orgEntry">The original entry found in the database (can be <code>null</code> is this is a new entry)</param>
    /// <returns>The added or updated entry</returns>
    public static T AddOrUpdateEntry<T>(this DbContext _dbContext, DbSet<T> set, T entry, T orgEntry) where T : class, IBase
    {
        if (entry.IsNew || orgEntry == null) // New or not found in context
        {
            entry.Id = 0;
            return set.Add(entry);
        }
        else
        {
            _dbContext.Entry(orgEntry).CurrentValues.SetValues(entry);
            return orgEntry;
        }
    }

    /// <summary>
    /// check if each entry of the new list was in the orginal list, if found, update it, if not found add it
    /// all entries found in the orignal list that are not in the new list are removed
    /// </summary>
    /// <typeparam name="T">The type of entry</typeparam>
    /// <param name="_dbContext">The context object</param>
    /// <param name="set">The database set</param>
    /// <param name="newList">The new list</param>
    /// <param name="orgList">The original list</param>
    public static void AddOrUpdateCollection<T>(this DbContext _dbContext, DbSet<T> set, ICollection<T> newList, ICollection<T> orgList) where T : class, IBase
    {
        // attach or update all entries in the new list
        foreach (T entry in newList)
        {
            // Find out if we had the entry already in the list
            var orgEntry = orgList.SingleOrDefault(e => e.Id != 0 && e.Id == entry.Id);

            AddOrUpdateEntry(_dbContext, set, entry, orgEntry);
        }

        // Remove all entries from the original list that are no longer in the new list
        foreach (T orgEntry in orgList.Where(e => e.Id != 0).ToList())
        {
            if (!newList.Any(e => e.Id == orgEntry.Id))
            {
                set.Remove(orgEntry);
            }
        }
    }
}

และใช้มันเช่น:

var originalParent = _dbContext.ParentItems
    .Where(p => p.Id == parent.Id)
    .Include(p => p.ChildItems)
    .Include(p => p.ChildItems2)
    .SingleOrDefault();

// Add the parent (including collections) to the context or update it's values (except the collections)
originalParent = _dbContext.AddOrUpdateEntry(_dbContext.ParentItems, parent, originalParent);

// Update each collection
_dbContext.AddOrUpdateCollection(_dbContext.ChildItems, parent.ChildItems, orgiginalParent.ChildItems);
_dbContext.AddOrUpdateCollection(_dbContext.ChildItems2, parent.ChildItems2, orgiginalParent.ChildItems2);

คุณสามารถสร้างคลาส extention สำหรับบริบทของคุณด้วยฟังก์ชันเหล่านี้:
Bluemoon74

0

ฉันประสบปัญหาเดียวกันเมื่อฉันจะลบบันทึกของฉันกว่าปัญหาบางอย่างเกิดขึ้นสำหรับการแก้ปัญหานี้คือเมื่อคุณจะลบบันทึกของคุณมากกว่าที่คุณหายไปบางสิ่งบางอย่างก่อนที่จะลบส่วนหัว / บันทึกต้นแบบคุณต้องเขียนรหัส ลบรายละเอียดก่อนส่วนหัว / ปริญญาโทฉันหวังว่าคุณจะได้รับการแก้ไข


-1

ฉันพบปัญหานี้มาแล้วหลายชั่วโมงและลองทุกอย่าง แต่ในกรณีของฉันการแก้ปัญหานั้นแตกต่างจากรายการด้านบน

หากคุณใช้เอนทิตีที่ดึงมาแล้วจากฐานข้อมูลและพยายามแก้ไขเป็นลูกข้อผิดพลาดจะเกิดขึ้น แต่ถ้าคุณได้รับสำเนาใหม่ของเอนทิตีจากฐานข้อมูลไม่ควรมีปัญหาใด ๆ อย่าใช้สิ่งนี้:

 public void CheckUsersCount(CompanyProduct companyProduct) 
 {
     companyProduct.Name = "Test";
 }

ใช้สิ่งนี้:

 public void CheckUsersCount(Guid companyProductId)
 {
      CompanyProduct companyProduct = CompanyProductManager.Get(companyProductId);
      companyProduct.Name = "Test";
 }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.