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

151

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 habe ich Aktionen zum Erstellen einer übergeordneten Entität (die einwandfrei funktioniert) und zum Aktualisieren einer übergeordneten Entität (die ein Problem aufweist). Die Aktualisierungsaktion 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 ab, die existingvon benannt ist model.Id, und weisen Sie modelder Entität nacheinander Werte zu. Das klingt dumm. Und in model.ChildrenIch weiß nicht, welches Kind neu ist, welches Kind geändert (oder sogar gelöscht) wird.

  2. Erstellen Sie eine neue übergeordnete Entität über model, hängen Sie sie an den DbContext an und speichern Sie sie. Aber wie kann der DbContext den Status von Kindern kennen (neu hinzufügen / löschen / geändert)?

Wie kann diese Funktion richtig implementiert werden?

Cheng Chen
quelle
Siehe auch Beispiel mit GraphDiff in einer doppelten Frage stackoverflow.com/questions/29351401/…
Michael Freidgeim

Antworten:

219

Da das Modell, das auf dem WebApi-Controller veröffentlicht wird, von jedem Entity-Framework-Kontext (EF) getrennt 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 hinzugefügt wurden Aktualisiert. (Es sei denn, Sie würden die Änderungen mit Ihrem eigenen Verfolgungsmechanismus während des getrennten Zustands (im Browser oder wo auch immer) verfolgen, was 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.SetValueskann jedes Objekt übernehmen und Eigenschaftswerte basierend auf dem Eigenschaftsnamen der angehängten Entität 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.

Slauma
quelle
35
Aber warum hat ef keinen "brillanteren" Weg? Ich denke, ef kann erkennen, ob das Kind geändert / gelöscht / hinzugefügt wurde. IMO Ihr obiger Code kann Teil des EF-Frameworks sein und eine allgemeinere Lösung werden.
Cheng Chen
7
@DannyChen: Es ist in der Tat eine lange Anfrage, dass das Aktualisieren nicht verbundener Entitäten von EF auf komfortablere Weise unterstützt werden soll ( entityframework.codeplex.com/workitem/864 ), aber es ist immer noch nicht Teil des Frameworks. Derzeit können Sie nur die in diesem Codeplex-Workitem erwähnte Drittanbieter-Bibliothek "GraphDiff" ausprobieren oder manuellen Code wie in meiner obigen Antwort schreiben.
Slauma
7
Eine Sache, die hinzugefügt werden muss: Im Rahmen der Aktualisierung und Einfügung von existingParent.Children.Add(newChild)untergeordneten Elementen können Sie dies nicht tun, da dann die Suche mit der vorhandenen Kinderlinie die kürzlich hinzugefügte Entität zurückgibt und diese Entität aktualisiert wird. Sie müssen nur in eine temporäre Liste einfügen und dann hinzufügen.
Erre Efe
3
@ RandolfRincónFadul Ich bin gerade auf dieses Problem gestoßen. Mein Fix, der etwas weniger Aufwand ist, besteht darin, die where-Klausel in der existingChildLINQ-Abfrage zu ändern :.Where(c => c.ID == childModel.ID && c.ID != default(int))
Gavin Ward
2
@RalphWillgoss Was ist der Fix in 2.2, über den Sie gesprochen haben?
Jan Paolo Go
11

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 Sie mit so etwas wie anrufen können:

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

Leider fällt dies irgendwie um, wenn es Sammlungseigenschaften für den untergeordneten Typ gibt, die ebenfalls aktualisiert werden müssen. Überlegen Sie, ob Sie versuchen möchten, dieses Problem zu lösen, 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.

Ich habe keine Ahnung, wie sich das alles im Maßstab auswirkt, bin mir aber nicht sicher, was ich sonst mit diesem Problem anfangen soll.

Brettman
quelle
1
Tolle Lösung! Aber schlägt fehl, wenn mehr als ein neues Element hinzugefügt wird. Das aktualisierte Wörterbuch kann nicht zweimal die Null-ID haben. Brauchen Sie etwas Arbeit um. Und schlägt auch fehl, wenn die Beziehung N -> N ist. Tatsächlich wird das Element zur Datenbank hinzugefügt, aber die Tabelle N -> N wird nicht geändert.
RenanStr
1
toAdd.ForEach(i => (selector(dbItem) as ICollection<Tchild>).Add(i.Value));sollte n -> n Problem lösen.
RenanStr
10

Okay Leute. Ich hatte diese Antwort einmal, verlor sie aber auf dem Weg. 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 gerade 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 Entitäten nach Bedarf hinzu. Sie müssen sich damit nicht befassen. Achten Sie darauf, Kindersammlung oder keine Würfel einzuschließen. Viel Glück!

Charles McIntosh
quelle
Genau das, was ich brauche, da die Anzahl der Kinder in meinem Modell im Allgemeinen recht gering ist. Wenn Linq also zunächst alle ursprünglichen Kinder aus der Tabelle löscht und dann alle neuen Kinder hinzufügt, ist die Auswirkung auf die Leistung kein Problem.
William T. Mallard
@ Charles McIntosh. Ich verstehe nicht, warum Sie Kinder wieder setzen, während Sie es in die anfängliche Abfrage aufnehmen?
Pantonis
1
@pantonis Ich füge die untergeordnete Sammlung hinzu, damit sie zur Bearbeitung geladen werden kann. Wenn ich mich auf das faule Laden verlasse, um es herauszufinden, funktioniert es nicht. Ich habe die untergeordneten Elemente (einmal) festgelegt, da ich anstelle des manuellen Löschens und Hinzufügens von Elementen zur Sammlung einfach die Liste ersetzen kann und entityframework Elemente für mich hinzufügt und löscht. Der Schlüssel besteht darin, den Status der Entität auf "Geändert" zu setzen und dem Entityframework das schwere Heben zu ermöglichen.
Charles McIntosh
@ CharlesMcIntosh Ich verstehe immer noch nicht, was Sie mit den Kindern dort erreichen wollen. Sie haben es in die erste Anfrage aufgenommen (Include (p => p.Children). Warum fordern Sie es erneut an?
pantonis
@pantonis, ich musste die alte Liste mit .include () abrufen, damit sie geladen und als Sammlung aus der Datenbank angehängt wird. So wird das verzögerte Laden aufgerufen. Ohne sie würden Änderungen an der Liste nicht nachverfolgt, wenn ich entitystate.modified verwendet hätte. Um es noch einmal zu wiederholen, setze ich die aktuelle untergeordnete Sammlung auf eine andere untergeordnete Sammlung. Zum Beispiel, wenn ein Manager ein paar neue Mitarbeiter hat oder ein paar verloren hat. Ich würde eine Abfrage verwenden, um diese neuen Mitarbeiter einzuschließen oder auszuschließen, und einfach die alte Liste durch eine neue Liste ersetzen und dann EF nach Bedarf von der Datenbankseite hinzufügen oder löschen lassen.
Charles McIntosh
9

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 davon ausgegangen, dass für jede aktualisierte Entität alle Eigenschaften festgelegt und in den Post-Daten des Clients angegeben sind (z. B. funktioniert dies nicht für die teilweise Aktualisierung einer Entität).

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

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

So habe ich dieses Problem gelöst. Auf diese Weise weiß EF, welche zu aktualisieren sind.

Witz
quelle
Lief wie am Schnürchen! Vielen Dank.
Inktkiller
2

Es gibt einige Projekte, die die Interaktion zwischen Client und Server vereinfachen, wenn es darum geht, ein gesamtes Objektdiagramm zu speichern.

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

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

Shimmy Weitzhandler
quelle
1

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

}
Mertuarez
quelle
0

@ Charles McIntosh gab mir wirklich die Antwort auf meine Situation, indem das übergebene Modell abgetrennt wurde. Für mich hat es letztendlich funktioniert, zuerst das übergebene Modell zu speichern ... und dann die Kinder weiter hinzuzufügen, wie ich es bereits zuvor war:

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

Für VB.NET-Entwickler Verwenden Sie dieses generische Sub, um den untergeordneten Status zu markieren, der einfach zu verwenden ist

Anmerkungen:

  • PromatCon: das Entitätsobjekt
  • amList: ist die untergeordnete Liste, die Sie hinzufügen oder ändern möchten
  • rList: ist die untergeordnete Liste, die Sie entfernen möchten
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()
Basilikum
quelle
0
var parent = context.Parent.FirstOrDefault(x => x.Id == modelParent.Id);
if (parent != null)
{
  parent.Childs = modelParent.Childs;
}

Quelle

Alex
quelle
0

Hier ist mein Code, der gut funktioniert.

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