wake-up-neo.com

Hinzufügen / Aktualisieren von untergeordneten Entitäten beim Aktualisieren einer übergeordneten Entität in EF

Die beiden Entitäten sind eine Eins-zu-Viele-Beziehung (erstellt durch Code First Fluent 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; }
}

In meinem WebApi-Controller stehen Aktionen zum Erstellen einer übergeordneten Entität (die ordnungsgemäß funktioniert) und zum Aktualisieren einer übergeordneten Entität (die ein Problem aufweist) zur Verfügung. Die Update-Aktion sieht folgendermaßen aus:

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

Derzeit habe ich zwei Ideen:

  1. Rufen Sie eine nachverfolgte übergeordnete Entität namens existing mit model.Id Ab und weisen Sie der Entität nacheinander Werte in model zu. Das klingt dumm. Und in model.Children Weiß ich nicht, welches Kind neu ist, welches Kind geändert (oder sogar gelöscht) wird.

  2. Erstellen Sie über model eine neue übergeordnete Entität, fügen Sie sie dem DbContext hinzu und speichern Sie sie. Aber wie kann der DbContext den Status von Kindern erkennen (neu hinzufügen/löschen/modifizieren)?

Wie kann diese Funktion korrekt implementiert werden?

128
Cheng Chen

Da das Modell, das an den WebApi-Controller gesendet wird, nicht mit einem Entity-Framework-Kontext (EF) verknüpft ist, besteht die einzige Option darin, das Objektdiagramm (übergeordnetes Element einschließlich seiner untergeordneten Elemente) aus der Datenbank zu laden und zu vergleichen, welche untergeordneten Elemente hinzugefügt, gelöscht oder gelöscht wurden Aktualisiert. (Es sei denn, Sie würden die Änderungen mit Ihrem eigenen Verfolgungsmechanismus im gelösten Zustand (im Browser oder wo auch immer) verfolgen, der meiner Meinung nach komplexer ist als der folgende.) Es könnte so aussehen:

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 kann ein beliebiges Objekt übernehmen und der angehängten Entität auf der Grundlage des Eigenschaftsnamens Eigenschaftswerte zuordnen. Wenn sich die Eigenschaftsnamen in Ihrem Modell von den Namen in der Entität unterscheiden, können Sie diese Methode nicht verwenden und müssen die Werte einzeln zuweisen.

183
Slauma

Ich habe mit so etwas rumgespielt ...

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

was du mit so etwas wie anrufen kannst:

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

Leider stürzt dies ein bisschen ab, wenn es Auflistungseigenschaften für den untergeordneten Typ gibt, die ebenfalls aktualisiert werden müssen. Überlegen Sie, ob Sie dieses Problem lösen möchten, indem Sie ein IRepository (mit grundlegenden CRUD-Methoden) übergeben, das für den eigenständigen Aufruf von UpdateChildCollection verantwortlich ist. Würde das Repo anstelle von direkten Aufrufen von DbContext.Entry aufrufen.

Habe keine Ahnung, wie sich das alles in großem Maßstab auswirkt, bin mir aber nicht sicher, was ich sonst mit diesem Problem anfangen soll.

10
brettman

Wenn Sie EntityFrameworkCore verwenden, können Sie in Ihrer Controller-Post-Aktion Folgendes ausführen (Die Attach-Methode fügt rekursiv Navigationseigenschaften einschließlich Sammlungen hinzu):

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

Es wird angenommen, dass für jede Entität, die aktualisiert wurde, alle Eigenschaften festgelegt und in den Post-Daten vom Client bereitgestellt wurden (z. B. funktioniert dies nicht für eine teilweise Aktualisierung einer Entität).

Sie müssen auch sicherstellen, dass Sie für diesen Vorgang einen neuen/dedizierten Entity Framework-Datenbankkontext verwenden.

7
hallz

Okay Leute. Ich hatte diese Antwort einmal, habe sie aber auf dem Weg verloren. Absolute Folter, wenn Sie wissen, dass es einen besseren Weg gibt, sich aber nicht daran erinnern oder ihn finden können! Es ist sehr einfach. Ich habe es nur auf verschiedene Arten getestet.

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

Sie können die gesamte Liste durch eine neue ersetzen! Der SQL-Code entfernt und fügt bei Bedarf Entitäten hinzu. Sie müssen sich nicht darum kümmern. Achten Sie darauf, dass Sie eine Kindersammlung oder keine Würfel hinzufügen. Viel Glück!

3

Es gibt einige Projekte, die die Interaktion zwischen Client und Server erleichtern, da sie das Speichern eines gesamten Objektgraphen betreffen.

Hier sind zwei, die Sie sich ansehen möchten:

Beide oben genannten Projekte erkennen die getrennten Entitäten, wenn sie an den Server zurückgesendet werden, erkennen und speichern die Änderungen und kehren zu den vom Client betroffenen Daten zurück.

2
Shimmy

Nur Proof of Concept Controler.UpdateModel funktioniert nicht richtig.

Volle Klasse hier :

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
Mertuarez