ฉันจะหยุด Entity Framework ไม่ให้พยายามบันทึก / แทรกวัตถุลูกได้อย่างไร


105

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

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

เหตุใดฉันจึงไม่ต้องการบันทึก / แทรกวัตถุลูก

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

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

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

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


เท่าที่ฉันรู้ว่าคุณจะต้องโมฆะวัตถุลูก
Johan

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

อิ่มอกอิ่มใจที่ไม่มีประโยชน์อย่างสมบูรณ์
Mark Micallef

@Euphoric แม้ว่าจะไม่ได้เปลี่ยนออบเจ็กต์ลูก แต่ EF ก็ยังพยายามแทรกตามค่าเริ่มต้นและไม่เพิกเฉยหรืออัปเดต
Johan

สิ่งที่ทำให้ฉันรำคาญจริงๆก็คือถ้าฉันออกนอกลู่นอกทางเพื่อทำให้วัตถุเหล่านั้นเป็นโมฆะจริง ๆ มันก็จะบ่นแทนที่จะรู้ตัวว่าฉันต้องการให้มันทิ้งมันไว้เฉยๆ เนื่องจากวัตถุลูกเหล่านั้นเป็นทางเลือกทั้งหมด (เป็นโมฆะในฐานข้อมูล) มีวิธีบังคับให้ EF ลืมว่าฉันมีวัตถุเหล่านั้นหรือไม่? เช่นล้างบริบทหรือแคชอย่างใด
Mark Micallef

คำตอบ:


57

เท่าที่ฉันรู้คุณมีสองทางเลือก

ตัวเลือกที่ 1)

ลบวัตถุลูกทั้งหมดให้เป็นโมฆะสิ่งนี้จะช่วยให้ EF ไม่ต้องเพิ่มอะไรเลย นอกจากนี้ยังจะไม่ลบอะไรออกจากฐานข้อมูลของคุณ

ทางเลือกที่ 2)

ตั้งค่าวัตถุลูกที่แยกออกจากบริบทโดยใช้รหัสต่อไปนี้

 context.Entry(yourObject).State = EntityState.Detached

โปรดทราบว่าคุณจะไม่สามารถแยกออก/List Collectionคุณจะต้องวนซ้ำรายการของคุณและแยกแต่ละรายการในรายการของคุณออกเช่นนั้น

foreach (var item in properties)
{
     db.Entry(item).State = EntityState.Detached;
}

สวัสดีโยฮันฉันได้ลองแยกคอลเลกชันหนึ่งออกมาและเกิดข้อผิดพลาดต่อไปนี้: ประเภทเอนทิตี HashSet'1 ไม่ได้เป็นส่วนหนึ่งของโมเดลสำหรับบริบทปัจจุบัน
Mark Micallef

@MarkyMark อย่าถอดคอลเลกชั่น คุณจะต้องวนซ้ำคอลเลกชันและแยกวัตถุออกสำหรับวัตถุ (ฉันจะอัปเดตคำตอบทันที)
Johan

11
น่าเสียดายที่แม้จะมีรายการลูปเพื่อแยกทุกอย่าง แต่ดูเหมือนว่า EF จะยังคงพยายามแทรกลงในตารางที่เกี่ยวข้อง ณ จุดนี้ฉันพร้อมที่จะฉีก EF ออกและเปลี่ยนกลับไปใช้ SQL ซึ่งอย่างน้อยก็ทำงานได้อย่างสมเหตุสมผล ช่างเป็นความเจ็บปวด
Mark Micallef

2
ฉันสามารถใช้ context.Entry (yourObject) .State ก่อนเพิ่มได้หรือไม่
Thomas Klammer

1
@ mirind4 ฉันไม่ได้ใช้สถานะ ถ้าฉันแทรกวัตถุฉันแน่ใจว่าชายด์ทั้งหมดเป็นโมฆะ ในการอัปเดตฉันได้รับวัตถุก่อนโดยไม่มีลูก
Thomas Klammer

42

เรื่องสั้นขนาดยาว: ใช้ Foreign key แล้วจะช่วยชีวิตคุณได้

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

เริ่มแรกคุณอาจกำหนดเอนทิตีของคุณดังนี้:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [Required]
    public City City { get; set; }
}

และคุณอาจทำการแทรกSchoolเช่นนี้ (สมมติว่าคุณมีคุณสมบัติของเมืองที่กำหนดให้กับnewItem แล้ว ):

public School Insert(School newItem)
{
    using (var context = new DatabaseContext())
    {
        context.Set<School>().Add(newItem);
        // use the following statement so that City won't be inserted
        context.Entry(newItem.City).State = EntityState.Unchanged;
        context.SaveChanges();
        return newItem;
    }
}

วิธีการข้างต้นอาจใช้ได้ผลดีในกรณีนี้อย่างไรก็ตามฉันชอบแนวทางForeign Keyซึ่งสำหรับฉันแล้วชัดเจนและยืดหยุ่นมากกว่า ดูโซลูชันที่อัปเดตด้านล่าง:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [ForeignKey("City_Id")]
    public City City { get; set; }

    [Required]
    public int City_Id { get; set; }
}

ด้วยวิธีนี้คุณต้องกำหนดอย่างชัดเจนว่าSchoolมีCity_Idคีย์ต่างประเทศและหมายถึงเอนทิตีเมือง ดังนั้นเมื่อพูดถึงการแทรกโรงเรียนคุณสามารถทำได้:

    public School Insert(School newItem, int cityId)
    {
        if(cityId <= 0)
        {
            throw new Exception("City ID no provided");
        }

        newItem.City = null;
        newItem.City_Id = cityId;

        using (var context = new DatabaseContext())
        {
            context.Set<School>().Add(newItem);
            context.SaveChanges();
            return newItem;
        }
    }

ในกรณีนี้คุณอย่างชัดเจนระบุCity_Idของบันทึกใหม่และลบซิตี้จากกราฟเพื่อให้ EF จะไม่รำคาญที่จะเพิ่มเข้าไปในบริบทพร้อมกับโรงเรียน

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

หวังว่านี่จะเป็นประโยชน์กับคุณ


คำตอบที่ดีช่วยฉันได้มาก! แต่จะไม่เป็นการดีกว่าที่จะระบุค่าต่ำสุด 1 สำหรับ City_Id โดยใช้[Range(1, int.MaxValue)]แอตทริบิวต์
แดนรังสี

ราวกับความฝัน!! ขอบคุณล้าน!
CJH

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

1
นี่เป็นประโยชน์กับฉันมาก EntityState.Unchaged คือสิ่งที่ฉันต้องการในการกำหนดอ็อบเจ็กต์ที่แสดงถึงคีย์ต่างประเทศของตารางการค้นหาในกราฟออบเจ็กต์ขนาดใหญ่ที่ฉันบันทึกเป็นธุรกรรมเดียว EF Core แสดงข้อความแสดงข้อผิดพลาดน้อยกว่าที่ใช้งานง่าย IMO ฉันแคชตารางการค้นหาของฉันที่ไม่ค่อยมีการเปลี่ยนแปลงเนื่องจากเหตุผลด้านประสิทธิภาพ คุณสันนิษฐานว่าเป็นเพราะ PK ของวัตถุตารางการค้นหานั้นเหมือนกับสิ่งที่อยู่ในฐานข้อมูลที่รู้อยู่แล้วว่าไม่มีการเปลี่ยนแปลงและกำหนด FK ให้กับรายการที่มีอยู่ แทนที่จะพยายามแทรกวัตถุตารางการค้นหาใหม่
tnk479

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

21

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

using (var ctx = new MyContext())
{
    ctx.Parents.Attach(parent);
    ctx.Entry(parent).State = EntityState.Added;  // or EntityState.Modified
    ctx.SaveChanges();
}

บรรทัดแรกจะแนบอ็อบเจ็กต์พาเรนต์และกราฟทั้งหมดของอ็อบเจ็กต์ลูกที่ขึ้นอยู่กับบริบทในUnchangedสถานะ

บรรทัดที่สองจะเปลี่ยนสถานะสำหรับอ็อบเจ็กต์พาเรนต์เท่านั้นโดยปล่อยให้ลูกอยู่ในUnchangedสถานะ

โปรดทราบว่าฉันใช้บริบทที่สร้างขึ้นใหม่ดังนั้นจึงหลีกเลี่ยงการบันทึกการเปลี่ยนแปลงอื่น ๆ ในฐานข้อมูล


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

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

14

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

class Company{
    public int Id{get;set;}
    public Virtual Department department{get; set;}
}
class Department{
    public int Id{get; set;}
    public String Name{get; set;}
}

บันทึกลงในฐานข้อมูล:

 Company company = new Company();
 company.department = new Department(){Id = 45}; 
 //an Department object with Id = 45 exists in database.    

 using(CompanyContext db = new CompanyContext()){
      Department department = db.Departments.Find(company.department.Id);
      company.department = department;
      db.Companies.Add(company);
      db.SaveChanges();
  }

Microsoft ขอให้สิ่งนี้เป็นคุณสมบัติ แต่ฉันพบว่าสิ่งนี้น่ารำคาญ หากออบเจ็กต์แผนกที่เกี่ยวข้องกับอ็อบเจ็กต์ของ บริษัท มี Id ที่มีอยู่แล้วในฐานข้อมูลเหตุใด EF จึงไม่เชื่อมโยงอ็อบเจ็กต์ของ บริษัท กับอ็อบเจ็กต์ฐานข้อมูล ทำไมเราต้องดูแลสมาคมด้วยตัวเอง? การดูแลคุณสมบัติการนำทางระหว่างการเพิ่มอ็อบเจกต์ใหม่ก็เหมือนกับการย้ายการดำเนินการฐานข้อมูลจาก SQL ไปยัง C # ซึ่งเป็นเรื่องยุ่งยากสำหรับนักพัฒนา


ฉันยอมรับ 100% ว่ามันไร้สาระที่ EF พยายามสร้างสถิติใหม่เมื่อมีการให้ ID หากมีวิธีเติมตัวเลือกรายการที่เลือกด้วยวัตถุจริงที่เราจะดำเนินธุรกิจ
T3.0

11

ก่อนอื่นคุณต้องทราบว่ามีสองวิธีในการอัปเดตเอนทิตีใน EF

  • วัตถุที่แนบมา

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

  • วัตถุที่ไม่ได้เชื่อมต่อ

หากคุณกำลังทำงานกับวัตถุที่ไม่ได้เชื่อมต่อคุณต้องจัดการการซิงโครไนซ์ด้วยตนเอง

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

นั่นหมายความว่าคุณกำลังทำงานกับวัตถุตัดการเชื่อมต่อ แต่ก็ยังไม่ชัดเจนว่าคุณกำลังใช้การเชื่อมโยงที่เป็นอิสระหรือสมาคมต่างประเทศที่สำคัญ

  • เพิ่ม

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

      db.Entity(entity.ChildObject).State = EntityState.Modified;
      db.Entity(entity).State = EntityState.Added;
    
  • อัปเดต

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

      db.Entity(entity).State = EntityState.Modified;
    

ความแตกต่างของกราฟ

หากคุณต้องการที่จะลดความซับซ้อนของรหัสเมื่อทำงานกับวัตถุที่เชื่อมต่อคุณสามารถให้ลองไปกราฟห้องสมุด diff

นี่คือการแนะนำแนะนำ GraphDiff สำหรับ Entity Framework รหัสครั้งแรก - ช่วยให้การปรับปรุงอัตโนมัติของกราฟของหน่วยงานเดี่ยว

ตัวอย่างรหัส

  • แทรกเอนทิตีหากไม่มีอยู่มิฉะนั้นให้อัปเดต

      db.UpdateGraph(entity);
    
  • แทรกเอนทิตีหากไม่มีอยู่มิฉะนั้นให้อัปเดตและแทรกวัตถุลูกหากไม่มีอยู่มิฉะนั้นให้อัปเดต

      db.UpdateGraph(entity, map => map.OwnedEntity(x => x.ChildObject));
    

3

วิธีที่ดีที่สุดในการดำเนินการนี้คือการแทนที่ฟังก์ชัน SaveChanges ในบริบทข้อมูลของคุณ

    public override int SaveChanges()
    {
        var added = this.ChangeTracker.Entries().Where(e => e.State == System.Data.EntityState.Added);

        // Do your thing, like changing the state to detached
        return base.SaveChanges();
    }

2

ฉันมีปัญหาเดียวกันเมื่อพยายามบันทึกโปรไฟล์ฉันมีตารางคำทักทายและใหม่ในการสร้างโปรไฟล์ เมื่อฉันแทรกโปรไฟล์มันจะแทรกลงในคำทักทายด้วย ดังนั้นฉันจึงลองแบบนี้ก่อนที่จะบันทึกการเปลี่ยนแปลง ()

db.Entry (Profile.Salutation) .State = EntityState.Unchanged;


1

สิ่งนี้ใช้ได้ผลสำหรับฉัน:

// temporarily 'detach' the child entity/collection to have EF not attempting to handle them
var temp = entity.ChildCollection;
entity.ChildCollection = new HashSet<collectionType>();

.... do other stuff

context.SaveChanges();

entity.ChildCollection = temp;

0

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


0

ฉันรู้ว่ามันเป็นโพสต์เก่า แต่ถ้าคุณใช้วิธีโค้ดเป็นอันดับแรกคุณอาจได้ผลลัพธ์ที่ต้องการโดยใช้โค้ดต่อไปนี้ในไฟล์การแมปของคุณ

Ignore(parentObject => parentObject.ChildObjectOrCollection);

โดยพื้นฐานแล้วจะบอกให้ EF ยกเว้นคุณสมบัติ "ChildObjectOrCollection" จากโมเดลเพื่อไม่ให้แมปกับฐานข้อมูล


"ละเว้น" ถูกใช้ในบริบทใด ดูเหมือนจะไม่มีอยู่ในบริบทที่สันนิษฐานไว้
T3.0

0

ฉันมีความท้าทายที่คล้ายกันในการใช้ Entity Framework Core 3.1.0 ตรรกะ repo ของฉันค่อนข้างทั่วไป

สิ่งนี้ใช้ได้ผลสำหรับฉัน:

builder.Entity<ChildEntity>().HasOne(c => c.ParentEntity).WithMany(l =>
       l.ChildEntity).HasForeignKey("ParentEntityId");

โปรดทราบว่า "ParentEntityId" คือชื่อคอลัมน์คีย์ต่างประเทศในเอนทิตีลูก ฉันเพิ่มบรรทัดโค้ดที่กล่าวถึงข้างต้นในวิธีนี้:

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