วิธีเพิ่ม / อัพเดตเอนทิตีชายด์เมื่ออัพเดตเอนทิตีพาเรนต์ใน EF


151

เอนทิตีทั้งสองนี้มีความสัมพันธ์แบบหนึ่งต่อหลายคน (สร้างโดยรหัส API แรกได้อย่างคล่องแคล่ว)

public class Parent
{
    public Parent()
    {
        this.Children = new List<Child>();
    }

    public int Id { get; set; }

    public virtual ICollection<Child> Children { get; set; }
}

public class Child
{
    public int Id { get; set; }

    public int ParentId { get; set; }

    public string Data { get; set; }
}

ในตัวควบคุม WebApi ของฉันฉันมีการดำเนินการเพื่อสร้างเอนทิตีหลัก (ซึ่งทำงานได้ดี) และอัปเดตเอนทิตีหลัก (ซึ่งมีปัญหา) การดำเนินการอัพเดตดูเหมือนว่า:

public void Update(UpdateParentModel model)
{
    //what should be done here?
}

ปัจจุบันฉันมีสองแนวคิด:

  1. รับเอนทิตีพาเรนต์ที่ถูกติดตามที่ตั้งชื่อexistingโดยmodel.Idและกำหนดค่าในmodelแต่ละรายการให้กับเอนทิตี ฟังดูงี่เง่า และในmodel.Childrenฉันไม่รู้ว่าลูกคนไหนเป็นลูกใหม่ลูกคนไหนที่ถูกปรับเปลี่ยน (หรือลบไป)

  2. สร้างเอนทิตีพาเรนต์ใหม่ผ่านmodelและแนบไปกับ DbContext และบันทึก แต่ DbContext จะทราบสถานะของเด็กอย่างไร (ใหม่เพิ่ม / ลบ / แก้ไข)

วิธีที่ถูกต้องในการใช้คุณสมบัตินี้คืออะไร?


ดูตัวอย่างด้วย GraphDiff ในคำถามที่ซ้ำกันstackoverflow.com/questions/29351401/ …
Michael Freidgeim

คำตอบ:


219

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

public void Update(UpdateParentModel model)
{
    var existingParent = _dbContext.Parents
        .Where(p => p.Id == model.Id)
        .Include(p => p.Children)
        .SingleOrDefault();

    if (existingParent != null)
    {
        // Update parent
        _dbContext.Entry(existingParent).CurrentValues.SetValues(model);

        // Delete children
        foreach (var existingChild in existingParent.Children.ToList())
        {
            if (!model.Children.Any(c => c.Id == existingChild.Id))
                _dbContext.Children.Remove(existingChild);
        }

        // Update and Insert children
        foreach (var childModel in model.Children)
        {
            var existingChild = existingParent.Children
                .Where(c => c.Id == childModel.Id)
                .SingleOrDefault();

            if (existingChild != null)
                // Update child
                _dbContext.Entry(existingChild).CurrentValues.SetValues(childModel);
            else
            {
                // Insert child
                var newChild = new Child
                {
                    Data = childModel.Data,
                    //...
                };
                existingParent.Children.Add(newChild);
            }
        }

        _dbContext.SaveChanges();
    }
}

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


35
แต่ทำไม ef ไม่มีวิธี "ยอดเยี่ยม" มากกว่านี้? ฉันคิดว่า ef สามารถตรวจจับได้ว่าเด็กถูกแก้ไข / ลบ / เพิ่ม IMO รหัสของคุณด้านบนสามารถเป็นส่วนหนึ่งของกรอบการทำงานของ EF และกลายเป็นโซลูชันทั่วไป
เฉินเฉิน

7
@DannyChen: มันเป็นคำร้องขอที่ยาวนานที่ EF ควรปรับปรุงเอนทิตีที่ยกเลิกการเชื่อมต่ออย่างสะดวกสบาย ( entityframework.codeplex.com/workitem/864 ) แต่ก็ยังไม่ได้เป็นส่วนหนึ่งของกรอบงาน ขณะนี้คุณสามารถลอง "GraphDiff" ของบุคคลที่สามที่กล่าวถึงใน codeplex workitem หรือเขียนรหัสด้วยตนเองเช่นเดียวกับคำตอบของฉันด้านบน
Slauma

7
สิ่งหนึ่งที่ควรเพิ่ม: ภายในส่วนหนึ่งของการอัปเดตและการแทรกลูก ๆ คุณไม่สามารถทำได้existingParent.Children.Add(newChild)เพราะการค้นหาที่มีอยู่ของเด็ก ๆจะส่งคืนเอนทิตีที่เพิ่งเพิ่มเข้าไปและเพื่อให้เอนทิตีนั้นได้รับการอัพเดต คุณเพียงแค่ต้องแทรกลงในรายการชั่วคราวแล้วเพิ่ม
Erre Efe

3
@ RandolfRincónFadulฉันเพิ่งเจอปัญหานี้ การแก้ไขของฉันซึ่งเป็นความพยายามที่น้อยน้อยกว่าคือการเปลี่ยนที่ข้อในexistingChildแบบสอบถาม LINQ:.Where(c => c.ID == childModel.ID && c.ID != default(int))
กาวินวอร์ด

2
@RalphWillgoss การแก้ไขใน 2.2 ที่คุณพูดถึงคืออะไร?
Jan Paolo ไป

11

ฉันยุ่งกับสิ่งนี้ ...

protected void UpdateChildCollection<Tparent, Tid , Tchild>(Tparent dbItem, Tparent newItem, Func<Tparent, IEnumerable<Tchild>> selector, Func<Tchild, Tid> idSelector) where Tchild : class
    {
        var dbItems = selector(dbItem).ToList();
        var newItems = selector(newItem).ToList();

        if (dbItems == null && newItems == null)
            return;

        var original = dbItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();
        var updated = newItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();

        var toRemove = original.Where(i => !updated.ContainsKey(i.Key)).ToArray();
        var removed = toRemove.Select(i => DbContext.Entry(i.Value).State = EntityState.Deleted).ToArray();

        var toUpdate = original.Where(i => updated.ContainsKey(i.Key)).ToList();
        toUpdate.ForEach(i => DbContext.Entry(i.Value).CurrentValues.SetValues(updated[i.Key]));

        var toAdd = updated.Where(i => !original.ContainsKey(i.Key)).ToList();
        toAdd.ForEach(i => DbContext.Set<Tchild>().Add(i.Value));
    }

ซึ่งคุณสามารถโทรหาสิ่งที่ชอบ:

UpdateChildCollection(dbCopy, detached, p => p.MyCollectionProp, collectionItem => collectionItem.Id)

น่าเสียดายที่สิ่งนี้ตกไปหากมีคุณสมบัติการรวบรวมในประเภทย่อยซึ่งจำเป็นต้องได้รับการปรับปรุงด้วย พิจารณาการพยายามแก้ไขปัญหานี้โดยส่ง IRepository (ด้วยวิธี CRUD พื้นฐาน) ซึ่งจะรับผิดชอบในการเรียก UpdateChildCollection ด้วยตนเอง จะเรียก repo แทนการโทรโดยตรงไปยัง DbContext.Entry

ไม่มีความคิดว่าสิ่งนี้จะดำเนินการในระดับใด แต่ไม่แน่ใจว่าจะทำอย่างไรกับปัญหานี้


1
สุดยอดทางออก! แต่ล้มเหลวหากเพิ่มมากกว่าหนึ่งรายการใหม่พจนานุกรมที่อัปเดตไม่สามารถมีศูนย์ id สองครั้ง ต้องการการทำงานบางอย่าง และยังล้มเหลวหากความสัมพันธ์คือ N -> N อันที่จริงไอเท็มถูกเพิ่มไปยังฐานข้อมูล แต่ตาราง N -> N ไม่ได้ถูกแก้ไข
RenanStr

1
toAdd.ForEach(i => (selector(dbItem) as ICollection<Tchild>).Add(i.Value));ควรแก้ปัญหา n -> n
RenanStr

10

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

var parent = _dbContext.Parents
  .Where(p => p.Id == model.Id)
  .Include(p => p.Children)
  .FirstOrDefault();

parent.Children = _dbContext.Children.Where(c => <Query for New List Here>);
_dbContext.Entry(parent).State = EntityState.Modified;

_dbContext.SaveChanges();

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


สิ่งที่ฉันต้องการเนื่องจากจำนวนเด็กในแบบจำลองของฉันค่อนข้างเล็กดังนั้นสมมติว่า Linq จะลบเด็กที่เป็นต้นฉบับทั้งหมดออกจากตารางในขั้นต้นแล้วเพิ่มเด็กใหม่ทั้งหมดที่มีผลกระทบต่อประสิทธิภาพไม่ใช่ปัญหา
William T. Mallard

@Charles McIntosh ฉันไม่ได้แยกแยะว่าทำไมคุณจึงตั้งค่าเด็กอีกครั้งในขณะที่คุณรวมไว้ในแบบสอบถามเริ่มต้น?
pantonis

1
@pantonis ฉันรวมคอลเลกชันของลูกไว้ด้วยเพื่อให้สามารถโหลดได้เพื่อแก้ไข ถ้าฉันพึ่งพาการโหลดที่ขี้เกียจเพื่อหาว่ามันไม่ทำงาน ฉันตั้งค่าเด็ก ๆ (หนึ่งครั้ง) เพราะแทนที่จะลบด้วยตนเองและเพิ่มรายการลงในคอลเลกชันฉันสามารถแทนที่รายการและเอนทิตีเฟรมเวิร์คจะเพิ่มและลบรายการสำหรับฉัน กุญแจสำคัญคือการตั้งค่าสถานะของเอนทิตีเพื่อแก้ไขและอนุญาตให้เอนทิตีเฟรมเวิร์กทำการยกของหนัก
Charles McIntosh

@CharlesMcIntosh ฉันยังไม่เข้าใจสิ่งที่คุณพยายามที่จะบรรลุกับเด็ก ๆ ที่นั่น คุณรวมไว้ในคำขอแรก (รวม (p => p.Children) ทำไมคุณขออีกครั้ง
pantonis

@ pantonis ฉันต้องดึงรายการเก่าโดยใช้. include () ดังนั้นมันจึงถูกโหลดและแนบเป็นคอลเลกชันจากฐานข้อมูล มันเป็นวิธีการโหลดที่ขี้เกียจถูกเรียก หากไม่มีมันการเปลี่ยนแปลงใด ๆ ในรายการจะไม่ถูกติดตามเมื่อฉันใช้ entitystate.modified เพื่อย้ำสิ่งที่ฉันกำลังทำอยู่คือการตั้งค่าคอลเลกชันย่อยปัจจุบันเป็นคอลเล็กชั่นย่อยอื่น เช่นถ้าผู้จัดการมีพนักงานใหม่จำนวนมากหรือสูญเสียไปเล็กน้อย ฉันจะใช้แบบสอบถามเพื่อรวมหรือแยกพนักงานใหม่เหล่านั้นและเพียงแทนที่รายการเก่าด้วยรายการใหม่จากนั้นให้ EF เพิ่มหรือลบตามที่ต้องการจากด้านฐานข้อมูล
Charles McIntosh

9

หากคุณใช้ EntityFrameworkCore คุณสามารถทำสิ่งต่อไปนี้ในการดำเนินการโพสต์คอนโทรลเลอร์ของคุณ ( วิธีการแนบแนบคุณสมบัติการนำทางซ้ำรวมถึงคอลเลกชัน):

_context.Attach(modelPostedToController);

IEnumerable<EntityEntry> unchangedEntities = _context.ChangeTracker.Entries().Where(x => x.State == EntityState.Unchanged);

foreach(EntityEntry ee in unchangedEntities){
     ee.State = EntityState.Modified;
}

await _context.SaveChangesAsync();

สันนิษฐานว่าแต่ละเอนทิตีที่ได้รับการปรับปรุงมีการตั้งค่าคุณสมบัติทั้งหมดและให้ไว้ในข้อมูลการโพสต์จากลูกค้า (เช่น. จะไม่ทำงานสำหรับการอัพเดทบางส่วนของเอนทิตี)

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


5
public async Task<IHttpActionResult> PutParent(int id, Parent parent)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            if (id != parent.Id)
            {
                return BadRequest();
            }

            db.Entry(parent).State = EntityState.Modified;

            foreach (Child child in parent.Children)
            {
                db.Entry(child).State = child.Id == 0 ? EntityState.Added : EntityState.Modified;
            }

            try
            {
                await db.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!ParentExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return Ok(db.Parents.Find(id));
        }

นี่คือวิธีที่ฉันแก้ไขปัญหานี้ ด้วยวิธีนี้ EF รู้ว่าต้องเพิ่มสิ่งใดเพื่อปรับปรุง


ทำงานเหมือนจับใจ! ขอบคุณ
Inktkiller

2

มีโครงการไม่กี่โครงการที่ทำให้การโต้ตอบระหว่างไคลเอนต์และเซิร์ฟเวอร์ง่ายขึ้นเท่าที่เกี่ยวข้องกับการบันทึกกราฟวัตถุทั้งหมด

นี่คือสองสิ่งที่คุณต้องการดู:

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


1

การพิสูจน์แนวคิด Controler.UpdateModelไม่ทำงานอย่างถูกต้อง

ชั้นเต็มที่นี่ :

const string PK = "Id";
protected Models.Entities con;
protected System.Data.Entity.DbSet<T> model;

private void TestUpdate(object item)
{
    var props = item.GetType().GetProperties();
    foreach (var prop in props)
    {
        object value = prop.GetValue(item);
        if (prop.PropertyType.IsInterface && value != null)
        {
            foreach (var iItem in (System.Collections.IEnumerable)value)
            {
                TestUpdate(iItem);
            }
        }
    }

    int id = (int)item.GetType().GetProperty(PK).GetValue(item);
    if (id == 0)
    {
        con.Entry(item).State = System.Data.Entity.EntityState.Added;
    }
    else
    {
        con.Entry(item).State = System.Data.Entity.EntityState.Modified;
    }

}

0

@ Charles McIntosh ให้คำตอบกับสถานการณ์ของฉันอย่างแท้จริงในแบบที่ผ่านไปแล้ว สำหรับฉันสิ่งที่ทำงานได้ในที่สุดคือการบันทึกรุ่นที่ส่งผ่านก่อน ... จากนั้นจึงเพิ่มเด็ก ๆ อย่างที่ฉันเคยเป็นมาก่อน:

public async Task<IHttpActionResult> GetUPSFreight(PartsExpressOrder order)
{
    db.Entry(order).State = EntityState.Modified;
    db.SaveChanges();
  ...
}

0

สำหรับนักพัฒนา VB.NET ใช้ย่อยทั่วไปนี้เพื่อทำเครื่องหมายสถานะลูกใช้งานง่าย

หมายเหตุ:

  • PromatCon: วัตถุเอนทิตี
  • amList: เป็นรายการลูกที่คุณต้องการเพิ่มหรือแก้ไข
  • rList: เป็นรายการลูกที่คุณต้องการลบ
updatechild(objCas.ECC_Decision, PromatCon.ECC_Decision.Where(Function(c) c.rid = objCas.rid And Not objCas.ECC_Decision.Select(Function(x) x.dcid).Contains(c.dcid)).toList)
Sub updatechild(Of Ety)(amList As ICollection(Of Ety), rList As ICollection(Of Ety))
        If amList IsNot Nothing Then
            For Each obj In amList
                Dim x = PromatCon.Entry(obj).GetDatabaseValues()
                If x Is Nothing Then
                    PromatCon.Entry(obj).State = EntityState.Added
                Else
                    PromatCon.Entry(obj).State = EntityState.Modified
                End If
            Next
        End If

        If rList IsNot Nothing Then
            For Each obj In rList.ToList
                PromatCon.Entry(obj).State = EntityState.Deleted
            Next
        End If
End Sub
PromatCon.SaveChanges()


0

นี่คือรหัสของฉันที่ใช้งานได้ดี

public async Task<bool> UpdateDeviceShutdownAsync(Guid id, DateTime shutdownAtTime, int areaID, decimal mileage,
        decimal motohours, int driverID, List<int> commission,
        string shutdownPlaceDescr, int deviceShutdownTypeID, string deviceShutdownDesc,
        bool isTransportation, string violationConditions, DateTime shutdownStartTime,
        DateTime shutdownEndTime, string notes, List<Guid> faultIDs )
        {
            try
            {
                using (var db = new GJobEntities())
                {
                    var isExisting = await db.DeviceShutdowns.FirstOrDefaultAsync(x => x.ID == id);

                    if (isExisting != null)
                    {
                        isExisting.AreaID = areaID;
                        isExisting.DriverID = driverID;
                        isExisting.IsTransportation = isTransportation;
                        isExisting.Mileage = mileage;
                        isExisting.Motohours = motohours;
                        isExisting.Notes = notes;                    
                        isExisting.DeviceShutdownDesc = deviceShutdownDesc;
                        isExisting.DeviceShutdownTypeID = deviceShutdownTypeID;
                        isExisting.ShutdownAtTime = shutdownAtTime;
                        isExisting.ShutdownEndTime = shutdownEndTime;
                        isExisting.ShutdownStartTime = shutdownStartTime;
                        isExisting.ShutdownPlaceDescr = shutdownPlaceDescr;
                        isExisting.ViolationConditions = violationConditions;

                        // Delete children
                        foreach (var existingChild in isExisting.DeviceShutdownFaults.ToList())
                        {
                            db.DeviceShutdownFaults.Remove(existingChild);
                        }

                        if (faultIDs != null && faultIDs.Any())
                        {
                            foreach (var faultItem in faultIDs)
                            {
                                var newChild = new DeviceShutdownFault
                                {
                                    ID = Guid.NewGuid(),
                                    DDFaultID = faultItem,
                                    DeviceShutdownID = isExisting.ID,
                                };

                                isExisting.DeviceShutdownFaults.Add(newChild);
                            }
                        }

                        // Delete all children
                        foreach (var existingChild in isExisting.DeviceShutdownComissions.ToList())
                        {
                            db.DeviceShutdownComissions.Remove(existingChild);
                        }

                        // Add all new children
                        if (commission != null && commission.Any())
                        {
                            foreach (var cItem in commission)
                            {
                                var newChild = new DeviceShutdownComission
                                {
                                    ID = Guid.NewGuid(),
                                    PersonalID = cItem,
                                    DeviceShutdownID = isExisting.ID,
                                };

                                isExisting.DeviceShutdownComissions.Add(newChild);
                            }
                        }

                        await db.SaveChangesAsync();

                        return true;
                    }
                }
            }
            catch (Exception ex)
            {
                logger.Error(ex);
            }

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