web-dev-qa-db-fra.com

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

Les deux entités sont une relation un-à-plusieurs (construite par code api fluent d'abord).

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 (ce qui pose 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é parente suivie nommée existing par model.Id, et affectez les valeurs dans model une par une à l'entité. Cela semble stupide. Et dans model.Children je ne sais pas quel enfant est nouveau, quel enfant est modifié (ou même supprimé).

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

Quelle est la bonne façon de mettre en œuvre cette fonctionnalité?

128
Cheng Chen

Le modèle publié sur le contrôleur WebApi étant détaché de tout contexte EF (entité-cadre), la seule option consiste à charger le graphe d'objet (le parent, y compris ses enfants) à partir de la base de données et à comparer les enfants ajoutés, supprimés ou supprimés. mis à jour. (À moins que vous ne suiviez les changements 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.SetValues peut prendre n'importe quel objet et mapper les valeurs de propriété sur l'entité attachée en fonction du nom de la propriété. Si les noms de propriété dans votre modèle sont différents de ceux de l'entité, vous ne pouvez pas utiliser cette méthode et devez affecter les valeurs une par une.

183
Slauma

J'ai déconné 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 un peu s'il y a des propriétés de collection sur le type enfant qui doivent également être mises à jour. Essayez de résoudre ce problème en passant un IRepository (avec les méthodes de base CRUD) qui serait chargé d'appeler UpdateChildCollection par lui-même. Appellerait le repo au lieu d'appels directs à DbContext.Entry.

Je ne sais pas du tout comment cela fonctionnera à grande échelle, mais je ne sais pas quoi faire avec ce problème.

10
brettman

Si vous utilisez EntityFrameworkCore, vous pouvez effectuer les opérations suivantes dans l'action de publication du contrôleur (La méthode Attach attache de manière récursive des propriétés de navigation, y compris des 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();

Il est supposé que chaque entité mise à jour possède toutes les propriétés définies et fournies dans les données de publication du client (par exemple, cela ne fonctionnera pas pour la mise à jour partielle d'une entité).

Vous devez également vous assurer que vous utilisez un nouveau contexte/une base de données d'infrastructure dédiée pour cette opération.

7
hallz

Ok les gars. J'ai eu cette réponse une fois mais je l'ai perdue en cours de route. torture absolue lorsque vous savez qu'il existe un meilleur moyen, mais que vous ne pouvez vous en souvenir ou le trouver! C'est très simple. Je viens de le tester de multiples 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 si nécessaire. Pas besoin de vous en préoccuper. Assurez-vous d'inclure la collecte d'enfants ou pas de dés. Bonne chance!

3
Charles McIntosh

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

Voici deux que vous voudriez regarder:

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, puis retournent aux données affectées par le client.

2
Shimmy

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

}
0
Mertuarez