EF'de bir üst varlığı güncellerken alt varlıklar nasıl eklenir / güncellenir


151

İki varlık bire çok ilişkidir (ilk akıcı API koduyla oluşturulur).

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 denetleyicimde (iyi çalışıyor) bir üst varlık oluşturmak ve (bazı sorunları olan) bir üst varlığı güncellemek için eylemlerim var. Güncelleme işlemi şuna benzer:

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

Şu anda iki fikrim var:

  1. existingTarafından adlandırılan izlenen bir üst öğe alın ve öğeye tek tek model.Iddeğerler atayın model. Bu aptalca geliyor. Ve model.Childrenhangi çocuğun yeni olduğunu bilmiyorum, hangi çocuk değiştirildi (hatta silindi).

  2. Aracılığıyla yeni bir üst varlık oluşturun modelve bunu DbContext'e ekleyin ve kaydedin. Ama DbContext çocukların durumunu nasıl bilebilir (yeni ekleme / silme / değiştirilmiş)?

Bu özelliği uygulamanın doğru yolu nedir?


Yinelenen bir soruda GraphDiff ile ilgili örneğe de bakınız stackoverflow.com/questions/29351401/…
Michael Freidgeim

Yanıtlar:


219

WebApi denetleyicisine gönderilen model herhangi bir varlık çerçevesi (EF) bağlamından ayrıldığından, tek seçenek nesne grafiğini (alt öğeleri de dahil olmak üzere) veritabanından yüklemek ve hangi çocukların eklendiğini, silindiğini veya güncellenmiş. (Bence aşağıdakilerden daha karmaşık olan müstakil durumda (tarayıcıda veya herhangi bir yerde) kendi izleme mekanizmanızla değişiklikleri izlemezseniz.) Şöyle görünebilir:

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.SetValuesherhangi bir nesneyi alabilir ve özellik adına bağlı olarak özellik değerlerini ekli objeye eşleyebilir. Modelinizdeki özellik adları varlıktaki adlardan farklıysa bu yöntemi kullanamazsınız ve değerleri tek tek atamanız gerekir.


35
Ama neden ef'nin daha "parlak" bir yolu yok? Ef, çocuğun değiştirilmiş / silinmiş / eklenmiş olup olmadığını tespit edebileceğini düşünüyorum, yukarıdaki IMO kodunuz EF çerçevesinin bir parçası olabilir ve daha genel bir çözüm haline gelebilir.
Cheng Chen

7
@DannyChen: Bağlantısı kesilen varlıkların güncellenmesinin EF tarafından daha rahat bir şekilde desteklenmesi uzun bir istektir ( entityframework.codeplex.com/workitem/864 ), ancak yine de çerçevenin bir parçası değildir. Şu anda yalnızca bu kod çözme çalışma öğesinde belirtilen üçüncü taraf lib "GraphDiff" i deneyebilir veya yukarıdaki cevabımdaki gibi manuel kod yazabilirsiniz.
Slauma

7
Eklenecek bir şey var: Güncelleme ve alt existingParent.Children.Add(newChild)öğe ekleme işlemlerinin her birinde, yapamazsınız çünkü mevcutChild linq araması son eklenen varlığı döndürür ve böylece varlık güncellenir. Sadece geçici bir listeye ekleyip eklemeniz yeterlidir.
Erre Efe

3
@ RandolfRincónFadul Bu konuyla henüz karşılaştım. Biraz daha az çaba olan düzeltmem, existingChildLINQ sorgusundaki nerede yan tümcesini değiştirmektir :.Where(c => c.ID == childModel.ID && c.ID != default(int))
Gavin Ward

2
@RalphWillgoss 2.2'de bahsettiğiniz sorun nedir?
Jan Paolo Go

11

Böyle bir şeyle uğraşıyorum ...

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

hangi gibi bir şey ile arayabilirsiniz:

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

Ne yazık ki, çocuk türünde de güncellenmesi gereken toplama özellikleri varsa bu tür düşer. UpdateChildCollection'u kendi başına çağırmaktan sorumlu olacak bir IRepository (temel CRUD yöntemleri ile) ileterek bunu çözmeye çalışmayı düşünmek. DbContext.Entry doğrudan çağrıları yerine repo çağırır.

Tüm bunların ölçekte nasıl performans göstereceğine dair bir fikriniz yok, ancak bu sorunla başka ne yapacağınızdan emin değilsiniz.


1
Harika bir çözüm! Ancak birden fazla yeni öğe eklerseniz başarısız olur, güncellenmiş sözlük iki kez sıfır kimliğe sahip olamaz. Etrafında biraz çalışma gerekiyor. Ayrıca ilişki N -> N ise de başarısız olur, aslında öğe veritabanına eklenir, ancak N -> N tablosu değiştirilmez.
RenanStr

1
toAdd.ForEach(i => (selector(dbItem) as ICollection<Tchild>).Add(i.Value));n -> n problemini çözmelidir.
RenanStr

10

Tamam arkadaşlar. Bu cevabı bir kez aldım ama yol boyunca kaybettim. daha iyi bir yol olduğunu bildiğinizde hatırlayamazsınız veya bulamazsınız! Çok basit. Sadece birkaç şekilde test ettim.

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

Tüm listeyi yenisiyle değiştirebilirsiniz! SQL kodu varlıkları kaldırır ve gerektiğinde ekler. Kendinizi bununla ilgilenmenize gerek yok. Çocuk koleksiyonu eklediğinizden veya zar olmadığından emin olun. İyi şanslar!


İhtiyacım olan şey, modelimdeki çocuk sayısı genellikle oldukça az olduğundan, Linq'in başlangıçta tüm orijinal çocukları tablodan sileceğini ve sonra yenilerini ekleyeceğini varsayarsak performans etkisi bir sorun değildir.
William T. Mallard

@Charles McIntosh. İlk sorguya dahil ederken neden Çocukları yeniden ayarladığınızı anlamıyorum?
pantonis

1
@pantonis Düzenleme için yüklenebilmesi için alt koleksiyonu dahil ediyorum. Bunu anlamak için tembel yüklemeye güvenirsem işe yaramaz. Çocukları (bir kez) ayarladım, çünkü koleksiyona manuel olarak silmek ve koleksiyon eklemek yerine basitçe listeyi değiştirebilirim ve entityframework benim için öğe ekleyip siler. Anahtar, varlığın durumunu değiştirilecek şekilde ayarlamak ve varlık çerçevesinin ağır kaldırmayı yapmasına izin vermektir.
Charles McIntosh

@CharlesMcIntosh Oradaki çocuklarla ne elde etmeye çalıştığınızı hala anlamıyorum. İlk talebe eklediniz (Dahil et (p => p.Children). Neden tekrar talep ediyorsunuz?
pantonis

@pantonis, veritabanından bir koleksiyon olarak yüklenir ve eklenir böylece .include () kullanarak eski listeyi çekmek zorunda kaldı. Tembel yükleme bu şekilde başlatılır. Bu olmadan, entitystate.modified öğesini kullandığımda listedeki hiçbir değişiklik izlenmez. Tekrarlamak gerekirse, şu anki çocuk koleksiyonunu farklı bir çocuk koleksiyonuna ayarlamak. bir yöneticinin bir sürü yeni çalışanı olması ya da bir kaçını kaybetmesi gibi. Bu yeni çalışanları dahil etmek veya hariç tutmak için bir sorgu kullanır ve sadece eski listeyi yeni bir listeyle değiştirir, sonra EF'nin veritabanı tarafında gerektiği gibi eklemesine veya silmesine izin veririm.
Charles McIntosh

9

EntityFrameworkCore kullanıyorsanız, denetleyici yayın işleminizde aşağıdakileri yapabilirsiniz ( Ekle yöntemi koleksiyonlar da dahil olmak üzere gezinme özelliklerini yinelemeli olarak ekler):

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

Güncellenen her bir varlığın istemciden gelen gönderi verilerinde ayarlanmış ve sağlanan tüm özelliklere sahip olduğu varsayılır (örneğin, bir varlığın kısmi güncellenmesi için çalışmaz).

Ayrıca, bu işlem için yeni / özel bir varlık çerçevesi veritabanı bağlamı kullandığınızdan emin olmanız gerekir.


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

Bu sorunu bu şekilde çözdüm. EF bu şekilde hangisinin güncelleneceğini ekleyeceğini bilir.


Bir cazibe gibi çalıştı! Teşekkürler.
Inktkiller

2

Tüm nesne grafiğini kaydetmekle ilgili olarak, istemci ve sunucu arasındaki etkileşimi kolaylaştıran birkaç proje var.

Bakmak istediğiniz iki şey:

Yukarıdaki her iki proje de sunucuya döndüğünde bağlantısı kesilen varlıkları tanır, değişiklikleri algılar ve kaydeder ve istemciden etkilenen verilere döner.


1

Sadece kavram kanıtı Controler.UpdateModel doğru çalışmaz.

Tam sınıf burada :

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 gerçekten durumumun cevabını verdi, çünkü geçen modelin ayrılması. Benim için nihayetinde işe yarayan ilk önce geçiş modelini kurtarmaktı ... daha sonra çocukları daha önce olduğu gibi eklemeye devam etmek:

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

0

VB.NET geliştiricileri için Çocuk durumunu işaretlemek için bu genel alt öğeyi kullanın, kullanımı kolay

Notlar:

  • PromatCon: varlık nesnesi
  • amList: eklemek veya değiştirmek istediğiniz alt liste
  • rList: kaldırmak istediğiniz alt liste
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
var parent = context.Parent.FirstOrDefault(x => x.Id == modelParent.Id);
if (parent != null)
{
  parent.Childs = modelParent.Childs;
}

kaynak


0

İşte benim kod iyi çalışıyor.

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;
        }
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.