Cara menambah / memperbarui entitas anak saat memperbarui entitas induk di EF

151

Kedua entitas tersebut adalah hubungan satu-ke-banyak (dibangun dengan kode pertama fasih 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; }
}

Di pengontrol WebApi saya, saya memiliki tindakan untuk membuat entitas induk (yang berfungsi dengan baik) dan memperbarui entitas induk (yang memiliki beberapa masalah). Tindakan pembaruan terlihat seperti:

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

Saat ini saya punya dua ide:

  1. Dapatkan entitas induk terlacak yang dinamai existingoleh model.Id, dan tetapkan nilai modelsatu per satu ke entitas tersebut. Ini kedengarannya bodoh. Dan model.Childrensaya tidak tahu anak mana yang baru, anak mana yang dimodifikasi (atau bahkan dihapus).

  2. Buat entitas induk baru melalui model, dan lampirkan ke DbContext dan simpan. Tetapi bagaimana DbContext bisa mengetahui keadaan anak-anak (baru tambah / hapus / modifikasi)?

Apa cara yang benar dalam mengimplementasikan fitur ini?

Cheng Chen
sumber
Lihat juga contoh dengan GraphDiff dalam pertanyaan duplikat stackoverflow.com/questions/29351401/…
Michael Freidgeim

Jawaban:

219

Karena model yang akan diposting ke pengontrol WebApi terlepas dari konteks entitas-kerangka kerja (EF), satu-satunya pilihan adalah memuat grafik objek (induk termasuk anak-anaknya) dari database dan membandingkan mana yang telah ditambahkan, dihapus atau diperbarui. (Kecuali jika Anda akan melacak perubahan dengan mekanisme pelacakan Anda sendiri selama keadaan terpisah (di browser atau di mana pun) yang menurut saya lebih kompleks daripada yang berikut ini.) Ini bisa terlihat seperti ini:

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.SetValuesdapat mengambil objek apa pun dan memetakan nilai properti ke entitas terlampir berdasarkan nama properti. Jika nama properti dalam model Anda berbeda dari nama-nama di entitas Anda tidak dapat menggunakan metode ini dan harus menetapkan nilai satu per satu.

Slauma
sumber
35
Tetapi mengapa ef tidak memiliki cara yang lebih "brilian"? Saya pikir ef dapat mendeteksi jika anak dimodifikasi / dihapus / ditambahkan, IMO kode Anda di atas dapat menjadi bagian dari kerangka kerja EF dan menjadi solusi yang lebih umum.
Cheng Chen
7
@DannyChen: Memang merupakan permintaan panjang bahwa memperbarui entitas yang terputus harus didukung oleh EF dengan cara yang lebih nyaman ( entitasframework.codeplex.com/workitem/864 ), tetapi itu masih bukan bagian dari kerangka kerja. Saat ini Anda hanya dapat mencoba lib pihak ketiga "GraphDiff" yang disebutkan dalam codeplex workitem itu atau menulis kode manual seperti pada jawaban saya di atas.
Slauma
7
Satu hal yang harus ditambahkan: Dalam upaya memperbarui dan menyisipkan anak-anak, Anda tidak dapat melakukannya existingParent.Children.Add(newChild)karena pencarian linq anak yang ada akan mengembalikan entitas yang baru ditambahkan, sehingga entitas tersebut akan diperbarui. Anda hanya perlu memasukkan ke dalam daftar sementara dan kemudian menambahkan.
Erre Efe
3
@ RandolfRincónFadul Saya baru saja menemukan masalah ini. Perbaikan saya yang sedikit kurang usaha adalah untuk mengubah klausa di mana dalam existingChildpermintaan LINQ:.Where(c => c.ID == childModel.ID && c.ID != default(int))
Gavin Ward
2
@RalphWillgoss Apa perbaikan di 2.2 yang Anda bicarakan?
Jan Paolo Go
11

Saya telah mengacaukan dengan sesuatu seperti ini ...

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

yang dapat Anda hubungi dengan sesuatu seperti:

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

Sayangnya, ini agak jatuh jika ada properti koleksi pada tipe anak yang juga perlu diperbarui. Mempertimbangkan mencoba menyelesaikan ini dengan mengirimkan IRepository (dengan metode CRUD dasar) yang akan bertanggung jawab untuk memanggil UpdateChildCollection sendiri. Akan memanggil repo bukannya panggilan langsung ke DbContext.Entry.

Tidak tahu bagaimana ini semua akan tampil pada skala, tetapi tidak yakin apa lagi yang harus dilakukan dengan masalah ini.

brettman
sumber
1
Solusi bagus! Tetapi gagal jika menambahkan lebih dari satu item baru, kamus yang diperbarui tidak dapat memiliki nol id dua kali. Perlu beberapa pekerjaan diseluruh. Dan juga gagal jika hubungan N -> N, pada kenyataannya, item ditambahkan ke database, tetapi N -> N table tidak dimodifikasi.
RenanStr
1
toAdd.ForEach(i => (selector(dbItem) as ICollection<Tchild>).Add(i.Value));harus menyelesaikan masalah n -> n.
RenanStr
10

Baiklah kawan Saya punya jawaban ini sekali tetapi hilang di sepanjang jalan. penyiksaan mutlak ketika Anda tahu ada cara yang lebih baik tetapi tidak dapat mengingatnya atau menemukannya! Ini sangat sederhana. Saya baru saja mengujinya dengan berbagai cara.

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();

Anda dapat mengganti seluruh daftar dengan yang baru! Kode SQL akan menghapus dan menambahkan entitas sesuai kebutuhan. Tidak perlu khawatir dengan itu. Pastikan untuk memasukkan koleksi anak atau tidak ada dadu. Semoga berhasil!

Charles McIntosh
sumber
Hanya apa yang saya butuhkan, karena jumlah anak dalam model saya umumnya cukup kecil, jadi dengan asumsi Linq akan menghapus semua anak asli dari tabel pada awalnya dan kemudian menambahkan semua yang baru dampak kinerja tidak menjadi masalah.
William T. Mallard
@ Charles McIntosh. Saya tidak mengerti mengapa Anda menetapkan Anak lagi saat Anda Sertakan dalam permintaan awal?
pantonis
1
@pantonis Saya menyertakan koleksi anak sehingga dapat dimuat untuk diedit. Jika saya mengandalkan pemuatan malas untuk mencari tahu itu tidak berhasil. Saya mengatur anak-anak (sekali) karena alih-alih menghapus secara manual dan menambahkan item ke koleksi, saya cukup mengganti daftar dan entitasframework akan menambah dan menghapus item untuk saya. Kuncinya adalah mengatur keadaan entitas untuk dimodifikasi dan memungkinkan pekerjaan entitas untuk melakukan pengangkatan berat.
Charles McIntosh
@CharlesMcIntosh Saya masih tidak mengerti apa yang ingin Anda capai dengan anak-anak di sana. Anda memasukkannya dalam permintaan pertama (Sertakan (p => p.Children). Mengapa Anda memintanya lagi?
pantonis
@antonis, saya harus menarik daftar lama menggunakan .include () sehingga dimuat dan dilampirkan sebagai koleksi dari database. Begitulah cara pemuatan malas dilakukan. tanpanya, perubahan apa pun pada daftar tidak akan dilacak ketika saya menggunakan entitasstate.modified. untuk mengulangi, apa yang saya lakukan adalah mengatur koleksi anak saat ini ke koleksi anak yang berbeda. seperti jika seorang manajer mendapat banyak karyawan baru atau kehilangan beberapa. Saya akan menggunakan kueri untuk memasukkan atau mengecualikan karyawan baru tersebut dan cukup mengganti daftar lama dengan daftar baru lalu membiarkan EF menambah atau menghapus sesuai kebutuhan dari sisi basis data.
Charles McIntosh
9

Jika Anda menggunakan EntityFrameworkCore, Anda dapat melakukan hal berikut ini di tindakan post controller Anda ( Metode Attach secara melampirkan properti navigasi termasuk koleksi):

_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();

Diasumsikan bahwa setiap entitas yang diperbarui memiliki semua properti yang ditetapkan dan disediakan dalam data pos dari klien (mis. Tidak akan berfungsi untuk pembaruan parsial suatu entitas).

Anda juga perlu memastikan bahwa Anda menggunakan konteks basis data kerangka kerja entitas baru / khusus untuk operasi ini.

hallz
sumber
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));
        }

Inilah cara saya memecahkan masalah ini. Dengan cara ini, EF tahu mana yang akan ditambahkan untuk diperbarui.

Jokeur
sumber
Bekerja seperti pesona! Terima kasih.
Inktkiller
2

Ada beberapa proyek di luar sana yang membuat interaksi antara klien dan server lebih mudah sejauh menyangkut menyimpan seluruh grafik objek.

Berikut adalah dua yang ingin Anda lihat:

Kedua proyek di atas mengenali entitas yang terputus saat dikembalikan ke server, mendeteksi dan menyimpan perubahan, dan kembali ke data klien yang terpengaruh.

Shimmy Weitzhandler
sumber
1

Hanya bukti konsep Controler.UpdateModel tidak akan berfungsi dengan benar.

Kelas penuh di sini :

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

}
Mertuarez
sumber
0

@Charles McIntosh benar-benar memberi saya jawaban untuk situasi saya karena model yang dilewati terlepas. Bagi saya apa yang akhirnya berhasil adalah menyelamatkan model yang lulus terlebih dahulu ... kemudian terus menambahkan anak-anak seperti yang sudah saya lakukan sebelumnya:

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

Untuk pengembang VB.NET Gunakan sub generik ini untuk menandai status anak, mudah digunakan

Catatan:

  • PromatCon: objek entitas
  • amList: adalah daftar anak yang ingin Anda tambahkan atau modifikasi
  • rList: adalah daftar anak yang ingin Anda hapus
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()
Kemangi
sumber
0
var parent = context.Parent.FirstOrDefault(x => x.Id == modelParent.Id);
if (parent != null)
{
  parent.Childs = modelParent.Childs;
}

sumber

Alex
sumber
0

Ini kode saya yang berfungsi dengan baik.

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