Comment ajouter / mettre à jour des entités enfants lors de la mise à jour d'une entité parente dans EF

151

Les deux entités sont une relation un-à-plusieurs (construite par le 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; }
}

Dans mon contrôleur WebApi, j'ai des actions pour créer une entité parente (qui fonctionne bien) et mettre à jour une entité parente (qui a un problème). L'action de mise à jour ressemble à ceci:

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

Actuellement, j'ai deux idées:

  1. Obtenez une entité parent suivie nommée existingpar model.Idet attribuez des valeurs modelune par une à l'entité. Cela semble stupide. Et dans model.Childrenje ne sais pas quel enfant est nouveau, quel enfant est modifié (ou même supprimé).

  2. Créez une nouvelle entité parente via model, attachez-la au DbContext et enregistrez-la. Mais comment le DbContext peut-il connaître l'état des enfants (nouvel ajout / suppression / modification)?

Quelle est la bonne façon d'implémenter cette fonctionnalité?

Cheng Chen
la source
Voir aussi l'exemple avec GraphDiff dans une question en double stackoverflow.com/questions/29351401/...
Michael Freidgeim

Réponses:

220

Étant donné que le modèle qui est publié sur le contrôleur WebApi est détaché de tout contexte de structure d'entité (EF), la seule option est de charger le graphique d'objet (parent, y compris ses enfants) à partir de la base de données et de comparer les enfants qui ont été ajoutés, supprimés ou actualisé. (À moins que vous ne suiviez les modifications avec votre propre mécanisme de suivi pendant l'état détaché (dans le navigateur ou ailleurs), ce qui à mon avis est plus complexe que ce qui suit.) Cela pourrait ressembler à ceci:

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.SetValuespeut prendre n'importe quel objet et mappe les valeurs de propriété à l'entité attachée en fonction du nom de la propriété. Si les noms de propriété de votre modèle sont différents des noms de l'entité, vous ne pouvez pas utiliser cette méthode et devez attribuer les valeurs une par une.

Slauma
la source
35
Mais pourquoi ef n'a-t-il pas un moyen plus «brillant»? Je pense que ef peut détecter si l'enfant est modifié / supprimé / ajouté, IMO votre code ci-dessus peut faire partie du cadre EF et devenir une solution plus générique.
Cheng Chen
7
@DannyChen: C'est en effet une longue demande que la mise à jour des entités déconnectées soit prise en charge par EF d'une manière plus confortable ( entityframework.codeplex.com/workitem/864 ) mais cela ne fait toujours pas partie du cadre. Actuellement, vous ne pouvez essayer que la bibliothèque tierce "GraphDiff" qui est mentionnée dans cet élément de travail codeplex ou écrire du code manuel comme dans ma réponse ci-dessus.
Slauma
7
Une chose à ajouter: dans le foreach de mise à jour et d'insertion d'enfants, vous ne pouvez pas le faire existingParent.Children.Add(newChild)car alors la recherche existanteChild linq retournera l'entité récemment ajoutée, et donc cette entité sera mise à jour. Il vous suffit d'insérer dans une liste temporaire, puis d'ajouter.
Erre Efe
3
@ RandolfRincónFadul Je viens de rencontrer ce problème. Ma solution, qui existingChilddemande un peu moins d'effort, consiste à modifier la clause where dans la requête LINQ:.Where(c => c.ID == childModel.ID && c.ID != default(int))
Gavin Ward
2
@RalphWillgoss Quel est le correctif de la version 2.2 dont vous parliez?
Jan Paolo Go
11

J'ai joué avec quelque chose comme ça ...

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

que vous pouvez appeler avec quelque chose comme:

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

Malheureusement, cela tombe en quelque sorte s'il y a des propriétés de collection sur le type enfant qui doivent également être mises à jour. Envisager d'essayer de résoudre cela en passant un IRepository (avec des méthodes CRUD de base) qui serait responsable d'appeler UpdateChildCollection seul. Appellerait le dépôt au lieu d'appels directs à DbContext.Entry.

Je n'ai aucune idée de la façon dont tout cela fonctionnera à grande échelle, mais je ne sais pas quoi faire d'autre avec ce problème.

Brettman
la source
1
Excellente solution! Mais échoue si ajouter plus d'un nouvel élément, le dictionnaire mis à jour ne peut pas avoir deux fois zéro id. Besoin d'un peu de travail autour. Et échoue également si la relation est N -> N, en fait, l'élément est ajouté à la base de données, mais la table N -> N n'est pas modifiée.
RenanStr
1
toAdd.ForEach(i => (selector(dbItem) as ICollection<Tchild>).Add(i.Value));devrait résoudre le problème n -> n.
RenanStr
10

Ok les gars. J'ai eu cette réponse une fois mais je l'ai perdue en cours de route. torture absolue quand vous savez qu'il existe un meilleur moyen mais que vous ne pouvez pas vous en souvenir ou le trouver! C'est très simple. Je viens de le tester de plusieurs façons.

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

Vous pouvez remplacer toute la liste par une nouvelle! Le code SQL supprimera et ajoutera des entités selon les besoins. Inutile de vous en préoccuper. Assurez-vous d'inclure la collection enfant ou pas de dés. Bonne chance!

Charles McIntosh
la source
Juste ce dont j'ai besoin, car le nombre d'enfants dans mon modèle est généralement assez petit, donc en supposant que Linq supprimera tous les enfants d'origine de la table au départ, puis ajoutera tous les nouveaux, l'impact sur les performances n'est pas un problème.
William
@Charles McIntosh. Je ne comprends pas pourquoi vous définissez à nouveau Children pendant que vous l'incluez dans la requête initiale?
pantonis
1
@pantonis J'inclus la collection enfant afin qu'elle puisse être chargée pour l'édition. Si je compte sur le chargement paresseux pour le comprendre, cela ne fonctionne pas. J'ai défini les enfants (une fois) car au lieu de supprimer et d'ajouter manuellement des éléments à la collection, je peux simplement remplacer la liste et entityframework ajoutera et supprimera des éléments pour moi. La clé est de définir l'état de l'entité sur modifié et de permettre au cadre d'entité de faire le gros du travail.
Charles McIntosh
@CharlesMcIntosh Je ne comprends toujours pas ce que vous essayez d'accomplir avec les enfants là-bas. Vous l'avez inclus dans la première demande (Inclure (p => p. Enfants). Pourquoi demandez-vous à nouveau?
pantonis
@pantonis, j'ai dû extraire l'ancienne liste en utilisant .include () pour qu'elle soit chargée et jointe en tant que collection de la base de données. C'est ainsi que le chargement paresseux est invoqué. sans cela, les modifications apportées à la liste ne seraient pas suivies lorsque j'utilisais entitystate.modified. pour réitérer, ce que je fais est de définir la collection enfant actuelle sur une collection enfant différente. comme si un manager avait un tas de nouveaux employés ou en perdait quelques-uns. J'utiliserais une requête pour inclure ou exclure ces nouveaux employés et remplacer simplement l'ancienne liste par une nouvelle liste, puis laisser EF ajouter ou supprimer si nécessaire du côté de la base de données.
Charles McIntosh
9

Si vous utilisez EntityFrameworkCore, vous pouvez effectuer les opérations suivantes dans l'action de publication de votre contrôleur (la méthode Attach attache de manière récursive les propriétés de navigation, y compris les collections):

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

On suppose que chaque entité qui a été mise à jour a toutes les propriétés définies et fournies dans les données de publication du client (par exemple, ne fonctionnera pas pour une mise à jour partielle d'une entité).

Vous devez également vous assurer que vous utilisez un nouveau contexte de base de données de structure d'entités dédié pour cette opération.

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

C'est ainsi que j'ai résolu ce problème. De cette façon, EF sait lequel ajouter et lequel mettre à jour.

Jokeur
la source
A travaillé comme un charme! Merci.
Inktkiller
2

Il existe quelques projets qui facilitent l'interaction entre le client et le serveur en ce qui concerne la sauvegarde d'un graphe d'objets entier.

En voici deux que vous voudriez examiner:

Les deux projets ci-dessus reconnaissent les entités déconnectées lorsqu'elles sont renvoyées au serveur, détectent et enregistrent les modifications et retournent aux données affectées par le client.

Shimmy Weitzhandler
la source
1

Une simple preuve de concept Controler.UpdateModel ne fonctionnera pas correctement.

Classe complète ici :

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
la source
0

@Charles McIntosh m'a vraiment donné la réponse à ma situation en ce que le modèle passé était détaché. Pour moi, ce qui a finalement fonctionné a été de sauvegarder d'abord le modèle passé ... puis de continuer à ajouter les enfants comme je l'étais déjà auparavant:

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

Pour les développeurs VB.NET Utilisez ce sous générique pour marquer l'état enfant, facile à utiliser

Remarques:

  • PromatCon: l'objet entité
  • amList: est la liste enfant que vous souhaitez ajouter ou modifier
  • rList: est la liste enfant que vous souhaitez supprimer
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()
Basilic
la source
0
var parent = context.Parent.FirstOrDefault(x => x.Id == modelParent.Id);
if (parent != null)
{
  parent.Childs = modelParent.Childs;
}

la source

Alex
la source
0

Voici mon code qui fonctionne très bien.

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;
        }
Développeur
la source