web-dev-qa-db-fra.com

Puis-je mettre à jour un objet attaché à l'aide d'un objet détaché mais égal?

Je récupère les données du film à partir d'une API externe. Dans une première phase, je vais gratter chaque film et l'insérer dans ma propre base de données. Dans une deuxième phase, je mettrai périodiquement à jour ma base de données en utilisant l'API "Changes" de l'API que je peux interroger pour voir quels films ont vu leurs informations modifiées.

Ma couche ORM est Entity-Framework. La classe Movie ressemble à ceci:

class Movie
{
    public virtual ICollection<Language> SpokenLanguages { get; set; }
    public virtual ICollection<Genre> Genres { get; set; }
    public virtual ICollection<Keyword> Keywords { get; set; }
}

Le problème se pose lorsque j'ai un film à mettre à jour: ma base de données considérera l'objet suivi et le nouveau que je reçois de l'appel de l'API de mise à jour comme des objets différents, sans tenir compte de .Equals().

Cela provoque un problème car lorsque j'essaie maintenant de mettre à jour la base de données avec le film mis à jour, il l'insérera au lieu de mettre à jour le film existant.

J'ai eu ce problème auparavant avec les langues et ma solution était de rechercher les objets de langage attachés, de les détacher du contexte, de déplacer leur PK vers l'objet mis à jour et de l'attacher au contexte. Lorsque SaveChanges() est maintenant exécuté, il le remplacera essentiellement.

C'est une approche plutôt malodorante car si je continue cette approche de mon objet Movie, cela signifie que je devrai détacher le film, les langues, les genres et les mots-clés, rechercher chacun dans la base de données, transférer leurs identifiants et insérer les nouveaux objets.

Existe-t-il un moyen de le faire plus élégamment? Idéalement, je souhaite simplement passer le film mis à jour au contexte et sélectionner le film à mettre à jour en fonction de la méthode Equals(), mettre à jour tous ses champs et pour chaque objet complexe: utiliser à nouveau l'enregistrement existant en fonction sur sa propre méthode Equals() et insérer si elle n'existe pas encore.

Je peux ignorer le détachement/attachement en fournissant des méthodes .Update() sur chaque objet complexe que je peux utiliser en combinaison de la récupération de tous les objets attachés, mais cela me demandera toujours de récupérer chaque objet existant pour le mettre à jour.

10
Jeroen Vannevel

Je n'ai pas trouvé ce que j'espérais mais j'ai trouvé une amélioration par rapport à la séquence existante select-detach-update-attach.

La méthode d'extension AddOrUpdate(this DbSet) vous permet de faire exactement ce que je veux faire: insérer si elle n'est pas là et mettre à jour si elle a trouvé une valeur existante. Je ne me suis pas rendu compte que j'utilisais cela plus tôt car je ne l'ai vraiment vu que utilisé dans la méthode seed() en combinaison avec Migrations. S'il y a une raison pour laquelle je ne devrais pas l'utiliser, faites le moi savoir.

Quelque chose d'utile à noter: il y a une surcharge disponible qui vous permet de sélectionner spécifiquement comment l'égalité doit être déterminée. Ici, j'aurais pu utiliser mon TMDbId mais à la place, j'ai simplement choisi de ne pas tenir compte de mon propre ID et d'utiliser à la place un PK sur TMDbId combiné avec DatabaseGeneratedOption.None. J'utilise également cette approche sur chaque sous-collection, le cas échéant.

Partie intéressante de la source :

internalSet.InternalContext.Owner.Entry(existing).CurrentValues.SetValues(entity);

c'est ainsi que les données sont mises à jour sous le capot.

Tout ce qui reste est d'appeler AddOrUpdate sur chaque objet que je veux être affecté par ceci:

public void InsertOrUpdate(Movie movie)
{
    _context.Movies.AddOrUpdate(movie);
    _context.Languages.AddOrUpdate(movie.SpokenLanguages.ToArray());
    // Other objects/collections
    _context.SaveChanges();
}

Ce n'est pas aussi propre que je l'espérais car je dois spécifier manuellement chaque partie de mon objet qui doit être mise à jour, mais c'est à peu près aussi proche que possible.

Lecture connexe: https://stackoverflow.com/questions/15336248/entity-framework-5-updating-a-record


Mise à jour:

Il s'avère que mes tests n'étaient pas assez rigoureux. Après avoir utilisé cette technique, j'ai remarqué que bien que la nouvelle langue ait été ajoutée, elle n'était pas connectée au film. dans le tableau plusieurs-à-plusieurs. C'est n problème connu mais apparemment de faible priorité et n'a pas été corrigé pour autant que je sache.

Au final, j'ai décidé d'opter pour l'approche où j'ai des méthodes Update(T) sur chaque type et de suivre cette séquence d'événements:

  • Boucler les collections dans un nouvel objet
  • Pour chaque entrée de chaque collection, recherchez-la dans la base de données
  • S'il existe, utilisez la méthode Update() pour le mettre à jour avec les nouvelles valeurs
  • S'il n'existe pas, ajoutez-le au DbSet approprié
  • Renvoyer les objets attachés et remplacer les collections de l'objet racine par les collections des objets attachés
  • Rechercher et mettre à jour l'objet racine

C'est beaucoup de travail manuel et c'est moche donc ça va passer par quelques refactorisations supplémentaires mais maintenant mes tests indiquent que cela devrait fonctionner pour des scénarios plus rigoureux.


Après l'avoir nettoyé davantage, j'utilise maintenant cette méthode:

private IEnumerable<T> InsertOrUpdate<T, TKey>(IEnumerable<T> entities, Func<T, TKey> idExpression) where T : class
{
    foreach (var entity in entities)
    {
        var existingEntity = _context.Set<T>().Find(idExpression(entity));
        if (existingEntity != null)
        {
            _context.Entry(existingEntity).CurrentValues.SetValues(entity);
            yield return existingEntity;
        }
        else
        {
            _context.Set<T>().Add(entity);
            yield return entity;
        }
    }
    _context.SaveChanges();
}

Cela me permet de l'appeler ainsi et d'insérer/mettre à jour les collections sous-jacentes:

movie.Genres = new List<Genre>(InsertOrUpdate(movie.Genres, x => x.TmdbId));

Remarquez comment je réaffecte la valeur récupérée à l'objet racine d'origine: maintenant elle est connectée à chaque objet attaché. La mise à jour de l'objet racine (le film) se fait de la même manière:

var localMovie = _context.Movies.SingleOrDefault(x => x.TmdbId == movie.TmdbId);
if (localMovie == null)
{
    _context.Movies.Add(movie);
} 
else
{
    _context.Entry(localMovie).CurrentValues.SetValues(movie);
}
7
Jeroen Vannevel

Puisque vous traitez avec différents champs id et tmbid, je suggère de mettre à jour l'API pour faire un index unique et séparé de toutes les informations comme les genres, les langues, les mots clés etc ... Et puis effectuez un appel pour indexer et vérifier les informations plutôt que de rassembler toutes les informations sur un objet spécifique dans votre classe Movie.

0
Snazzy Sanoj