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

10

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 qui doit être mis à 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 .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 déjà rencontré ce problème 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. Une SaveChanges()fois exécuté, il le remplacera essentiellement.

C'est une approche plutôt malodorante car si je continue cette approche de mon Movieobjet, 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 le de nouveaux objets.

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

Je peux ignorer le détachement / attachement en fournissant des .Update()méthodes sur chaque objet complexe que je peux utiliser en combinaison avec 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.

Jeroen Vannevel
la source
Pourquoi ne pouvez-vous pas simplement mettre à jour l'entité suivie à partir de vos données API et enregistrer les modifications sans remplacer / détacher / faire correspondre / attacher des entités?
Si-N
@ Si-N: pouvez-vous développer comment cela se passerait exactement?
Jeroen Vannevel
Ok maintenant vous avez ajouté que le dernier paragraphe a plus de sens, vous essayez d'éviter de récupérer les entités avant de les mettre à jour? Il n'y a rien qui pourrait être votre PK dans votre cours de cinéma? Comment comparez-vous les films de l'API externe à vos entités? De toute façon, vous pouvez récupérer toutes les entités dont vous avez besoin pour mettre à jour en un seul appel de base de données, ne serait-ce pas la solution la plus simple qui ne devrait pas entraîner une baisse importante des performances (sauf si vous parlez d'un grand nombre de films à mettre à jour)?
Si-N
Ma classe Movie a un PK idet les films de l'API externe sont mis en correspondance avec ceux locaux en utilisant le champ tmdbid. Je ne peux pas récupérer toutes les entités qui doivent être mises à jour en un seul appel, car il s'agit de films, de genres, de langues, de mots clés, etc. Chacun d'eux a un PK et peut déjà exister dans la base de données.
Jeroen Vannevel

Réponses:

8

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 seed()méthode 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, TMDbIdmais j'ai plutôt choisi de simplement ignorer mon propre ID et d'utiliser plutôt 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 réellement mises à jour sous le capot.

Tout ce qui reste est d'appeler AddOrUpdatechaque 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: /programming/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. Il s'agit d' un problème connu mais apparemment de faible priorité et qui n'a pas été résolu pour autant que je sache.

Au final, j'ai décidé d'opter pour l'approche où j'ai des Update(T)méthodes 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 Update()méthode 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 refactorings de plus, 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);
}
Jeroen Vannevel
la source
Comment gérez-vous les suppressions dans la relation 1-M? par exemple, 1-Movie peut avoir plusieurs langues; si l'une des langues est supprimée, votre code la supprime-t-il? Il semble que votre solution n'insère et / ou ne met à jour que (mais ne supprime pas?)
joedotnot
0

Étant donné que vous traitez avec différents domaines idet tmbid, je suggère que la mise à jour de l'API pour faire un index unique et séparé de toutes les informations comme les genres, les langues, les mots clés, etc. toutes les informations sur un objet spécifique dans votre classe Movie.

Snazzy Sanoj
la source
1
Je ne suis pas le fil de la pensée ici. Pouvez-vous vous étendre? Notez que l'API externe est entièrement hors de mon contrôle.
Jeroen Vannevel