Mettre à jour les relations lors de l'enregistrement des modifications des objets EF4 POCO

107

Entity Framework 4, objets POCO et ASP.Net MVC2. J'ai une relation plusieurs à plusieurs, disons entre les entités BlogPost et Tag. Cela signifie que dans ma classe POCO BlogPost générée par T4, j'ai:

public virtual ICollection<Tag> Tags {
    // getter and setter with the magic FixupCollection
}
private ICollection<Tag> _tags;

Je demande un BlogPost et les balises associées à partir d'une instance de l'ObjectContext et je l'envoie à une autre couche (vue dans l'application MVC). Plus tard, je récupère le BlogPost mis à jour avec des propriétés modifiées et des relations modifiées. Par exemple, il avait des balises "A", "B" et "C", et les nouvelles balises sont "C" et "D". Dans mon exemple particulier, il n'y a pas de nouvelles balises et les propriétés des balises ne changent jamais, donc la seule chose qui devrait être enregistrée est les relations modifiées. Maintenant, je dois enregistrer cela dans un autre ObjectContext. (Mise à jour: maintenant, j'ai essayé de le faire dans la même instance de contexte et j'ai également échoué.)

Le problème: je ne peux pas le faire enregistrer correctement les relations. J'ai essayé tout ce que j'ai trouvé:

  • Controller.UpdateModel et Controller.TryUpdateModel ne fonctionnent pas.
  • Récupérer l'ancien BlogPost du contexte puis modifier la collection ne fonctionne pas. (avec différentes méthodes à partir du point suivant)
  • Cela fonctionnerait probablement, mais j'espère que ce n'est qu'une solution de contournement, pas la solution :(.
  • J'ai essayé les fonctions Attach / Add / ChangeObjectState pour BlogPost et / ou Tags dans toutes les combinaisons possibles. Échoué.
  • Cela ressemble à ce dont j'ai besoin, mais cela ne fonctionne pas (j'ai essayé de le réparer, mais je ne peux pas pour mon problème).
  • J'ai essayé ChangeState / Add / Attach / ... les objets de relation du contexte. Échoué.

«Ne fonctionne pas» signifie dans la plupart des cas que j'ai travaillé sur la «solution» donnée jusqu'à ce qu'elle ne produise aucune erreur et enregistre au moins les propriétés de BlogPost. Ce qui se passe avec les relations varie: généralement, les balises sont ajoutées à nouveau à la table des balises avec de nouveaux PK et le BlogPost enregistré fait référence à celles-ci et non à celles d'origine. Bien sûr, les balises renvoyées ont des PK, et avant les méthodes de sauvegarde / mise à jour, je vérifie les PK et ils sont égaux à ceux de la base de données, donc EF pense probablement qu'il s'agit de nouveaux objets et que ces PK sont ceux temporaires.

Un problème que je connais et qui pourrait rendre impossible la recherche d'une solution simple automatisée: lorsque la collection d'un objet POCO est modifiée, cela devrait se produire par la propriété de collection virtuelle mentionnée ci-dessus, car alors l'astuce FixupCollection mettra à jour les références inversées à l'autre extrémité de la relation plusieurs-à-plusieurs. Cependant, lorsqu'une vue «renvoie» un objet BlogPost mis à jour, cela ne se produit pas. Cela signifie qu'il n'y a peut-être pas de solution simple à mon problème, mais cela me rendrait très triste et je détesterais le triomphe EF4-POCO-MVC :(. Cela signifierait également qu'EF ne peut pas faire cela dans l'environnement MVC, quel que soit le cas Les types d'objets EF4 sont utilisés: (. Je pense que le suivi des modifications basé sur les instantanés devrait découvrir que le BlogPost modifié a des relations avec des balises avec des PK existants.

Btw: Je pense que le même problème se produit avec les relations un-à-plusieurs (google et mon collègue le disent). Je vais essayer à la maison, mais même si cela fonctionne, cela ne m'aide pas dans mes six relations plusieurs à plusieurs dans mon application :(.

Peterfoldi
la source
Veuillez poster votre code. C'est un scénario courant.
John Farrell
1
J'ai une solution automatique à ce problème, elle est cachée dans les réponses ci-dessous, donc beaucoup la manqueraient, mais jetez un coup d'œil car cela vous fera économiser un sacré emploi voir le message ici
brentmckendrick
@brentmckendrick Je pense qu'une autre approche est meilleure. Au lieu d'envoyer l'intégralité du graphe d'objets modifiés sur le fil, pourquoi ne pas simplement envoyer le delta? Vous n'auriez même pas besoin de classes DTO générées dans ce cas. Si vous avez une opinion à ce sujet, discutons-en sur stackoverflow.com/questions/1344066/calculate-object-delta .
HappyNomad

Réponses:

145

Essayons de cette façon:

  • Attachez BlogPost au contexte. Après avoir attaché un objet au contexte, l'état de l'objet, tous les objets associés et toutes les relations est défini sur Inchangé.
  • Utilisez context.ObjectStateManager.ChangeObjectState pour définir votre BlogPost sur Modified
  • Itérer dans la collection de balises
  • Utilisez context.ObjectStateManager.ChangeRelationshipState pour définir l'état de la relation entre la balise actuelle et BlogPost.
  • Sauvegarder les modifications

Éditer:

Je suppose que l'un de mes commentaires vous a donné de faux espoirs qu'EF fera la fusion pour vous. J'ai beaucoup joué avec ce problème et ma conclusion dit qu'EF ne le fera pas pour vous. Je pense que vous avez également trouvé ma question sur MSDN . En réalité, de telles questions sont nombreuses sur Internet. Le problème est qu'il n'est pas clairement indiqué comment gérer ce scénario. Alors jetons un œil sur le problème:

Contexte du problème

EF doit suivre les modifications sur les entités afin que la persistance sache quels enregistrements doivent être mis à jour, insérés ou supprimés. Le problème est qu'il est de la responsabilité d'ObjectContext de suivre les modifications. ObjectContext est capable de suivre les modifications uniquement pour les entités attachées. Les entités créées en dehors de ObjectContext ne sont pas du tout suivies.

Description du problème

Sur la base de la description ci-dessus, nous pouvons clairement affirmer que EF est plus adapté aux scénarios connectés où l'entité est toujours attachée au contexte - typique pour une application WinForm. Les applications Web nécessitent un scénario déconnecté dans lequel le contexte est fermé après le traitement de la demande et le contenu de l'entité est passé en tant que réponse HTTP au client. La prochaine requête HTTP fournit le contenu modifié de l'entité qui doit être recréé, attaché à un nouveau contexte et conservé. La récréation se produit généralement en dehors de la portée du contexte (architecture en couches avec persistance ignorée).

Solution

Alors, comment faire face à un tel scénario déconnecté? Lorsque vous utilisez les classes POCO, nous avons 3 façons de gérer le suivi des modifications:

  • Instantané - nécessite le même contexte = inutile pour le scénario déconnecté
  • Proxy de suivi dynamique - nécessite le même contexte = inutile pour le scénario déconnecté
  • Synchronisation manuelle.

La synchronisation manuelle sur une seule entité est une tâche facile. Il vous suffit d'attacher une entité et d'appeler AddObject pour l'insertion, DeleteObject pour la suppression ou définir l'état dans ObjectStateManager sur Modified pour la mise à jour. La vraie douleur survient lorsque vous devez gérer un graphique d'objets au lieu d'une seule entité. Cette douleur est encore pire lorsque vous avez affaire à des associations indépendantes (celles qui n'utilisent pas la propriété de clé étrangère) et de nombreuses à plusieurs relations. Dans ce cas, vous devez synchroniser manuellement chaque entité dans le graphe d'objets mais aussi chaque relation dans le graphe d'objets.

La synchronisation manuelle est proposée comme solution par la documentation MSDN: Attacher et détacher des objets dit:

Les objets sont attachés au contexte d'objet dans un état inchangé. Si vous devez modifier l'état d'un objet ou la relation parce que vous savez que votre objet a été modifié à l'état détaché, utilisez l'une des méthodes suivantes.

Les méthodes mentionnées sont ChangeObjectState et ChangeRelationshipState de ObjectStateManager = suivi manuel des modifications. Une proposition similaire se trouve dans un autre article de documentation MSDN: Defining and Managing Relationships dit:

Si vous travaillez avec des objets déconnectés, vous devez gérer manuellement la synchronisation.

De plus, il y a un article de blog lié à EF v1 qui critique exactement ce comportement d'EF.

Raison de la solution

EF a de nombreuses opérations et paramètres "utiles" tels que Refresh , Load , ApplyCurrentValues , ApplyOriginalValues , MergeOption, etc. Je préfère ne pas tester ces méthodes avec des types complexes imbriqués dans entity.

Autre solution proposée

Au lieu d'une véritable fonctionnalité de fusion, l'équipe EF fournit quelque chose appelé Entités d'auto-suivi (STE) qui ne résolvent pas le problème. Tout d'abord, STE ne fonctionne que si la même instance est utilisée pour l'ensemble du traitement. Dans une application Web, ce n'est pas le cas, sauf si vous stockez l'instance dans un état d'affichage ou une session. En raison de cela, je suis très mécontent d'utiliser EF et je vais vérifier les fonctionnalités de NHibernate. La première observation dit que NHibernate a peut-être une telle fonctionnalité .

Conclusion

Je terminerai ces hypothèses avec un lien unique vers une autre question connexe sur le forum MSDN. Vérifiez la réponse de Zeeshan Hirani. Il est l'auteur de Recettes Entity Framework 4.0 . S'il dit que la fusion automatique des graphiques d'objets n'est pas prise en charge, je le crois.

Mais il est toujours possible que je me trompe complètement et que certaines fonctionnalités de fusion automatique existent dans EF.

Modifier 2:

Comme vous pouvez le voir, cela a déjà été ajouté à MS Connect en tant que suggestion en 2007. MS l'a fermé comme quelque chose à faire dans la prochaine version, mais en réalité rien n'avait été fait pour améliorer cet écart, sauf STE.

Ladislav Mrnka
la source
7
C'est l'une des meilleures réponses que j'ai lues sur SO. Vous avez clairement indiqué ce que tant d'articles, de documentation et de billets de blog MSDN sur le sujet n'ont pas réussi à faire passer. EF4 ne prend pas en charge la mise à jour des relations à partir d'entités "détachées". Il ne fournit que des outils pour vous permettre de le mettre en œuvre vous-même. Je vous remercie!
tyriker
1
Alors, après quelques mois, qu'en est-il du NHibernate lié à ce problème par rapport à EF4?
CallMeLaNN
1
Ceci est très bien pris en charge dans NHibernate :-) pas besoin de fusionner manuellement, dans mon exemple, c'est un graphe d'objets profond à 3 niveaux, la question a des réponses, chaque réponse a des commentaires et la question a aussi des commentaires. NHibernate peut persister / fusionner votre graphe d'objets, quelle que soit sa complexité ienablemuch.com/2011/01/nhibernate-saves-your-whole-object.html Un autre utilisateur satisfait de NHibernate: codinginstinct.com/2009/11/…
Michael Buen
2
Une des meilleures explications que j'aie jamais lues !! Merci beaucoup
marvelTracker
2
L'équipe EF prévoit de résoudre ce problème post-EF6. Vous voudrez peut-être voter pour entityframework.codeplex.com/workitem/864
Eric J.
19

J'ai une solution au problème décrit ci-dessus par Ladislav. J'ai créé une méthode d'extension pour le DbContext qui effectuera automatiquement l'ajout / la mise à jour / la suppression en fonction d'un diff du graphique fourni et du graphique persistant.

À l'heure actuelle, en utilisant Entity Framework, vous devrez effectuer les mises à jour des contacts manuellement, vérifier si chaque contact est nouveau et ajouter, vérifier s'il est mis à jour et modifier, vérifier s'il est supprimé puis le supprimer de la base de données. Une fois que vous avez à faire cela pour quelques agrégats différents dans un grand système, vous commencez à réaliser qu'il doit y avoir une méthode meilleure et plus générique.

Veuillez jeter un œil et voir si cela peut aider http://refactorthis.wordpress.com/2012/12/11/introducing-graphdiff-for-entity-framework-code-first-allowing-automated-updates-of-a- graphe d'entités détachées /

Vous pouvez accéder directement au code ici https://github.com/refactorthis/GraphDiff

brentmckendrick
la source
Je suis sûr que vous pouvez résoudre cette question facilement, je passe un mauvais moment avec.
Shimmy Weitzhandler
1
Salut Shimmy, désolé j'ai enfin eu le temps de jeter un œil. Je vais l'examiner ce soir.
brentmckendrick
Cette bibliothèque est géniale et m'a fait gagner tellement de temps! THX!
lordjeb
9

Je sais qu'il est tard pour l'OP, mais comme c'est un problème très courant, j'ai publié ceci au cas où cela servirait quelqu'un d'autre. J'ai joué avec ce problème et je pense que j'ai une solution assez simple, ce que je fais est:

  1. Enregistrez l'objet principal (Blogs par exemple) en définissant son état sur Modifié.
  2. Recherchez dans la base de données l'objet mis à jour, y compris les collections que je dois mettre à jour.
  3. Recherchez et convertissez .ToList () les entités que je souhaite inclure dans ma collection.
  4. Mettez à jour la ou les collections de l'objet principal avec la liste que j'ai obtenue à l'étape 3.
  5. Sauvegarder les modifications();

Dans l'exemple suivant, "dataobj" et "_categories" sont les paramètres reçus par mon contrôleur "dataobj" est mon objet principal et "_categories" est un IEnumerable contenant les ID des catégories sélectionnées par l'utilisateur dans la vue.

    db.Entry(dataobj).State = EntityState.Modified;
    db.SaveChanges();
    dataobj = db.ServiceTypes.Include(x => x.Categories).Single(x => x.Id == dataobj.Id);
    var it = _categories != null ? db.Categories.Where(x => _categories.Contains(x.Id)).ToList() : null;
    dataobj.Categories = it;
    db.SaveChanges();

Cela fonctionne même pour plusieurs relations

c0y0teX
la source
7

L'équipe Entity Framework est consciente qu'il s'agit d'un problème d'utilisabilité et prévoit de le résoudre après EF6.

De l'équipe Entity Framework:

Il s'agit d'un problème de convivialité dont nous sommes conscients et auquel nous avons réfléchi et que nous prévoyons de travailler davantage sur post-EF6. J'ai créé cet élément de travail pour suivre le problème: http://entityframework.codeplex.com/workitem/864 L'élément de travail contient également un lien vers l'élément vocal de l'utilisateur pour cela - je vous encourage à voter pour cela si vous avez pas déjà fait.

Si cela vous concerne, votez pour la fonctionnalité à

http://entityframework.codeplex.com/workitem/864

Eric J.
la source
post-EF6? quelle année sera-ce alors dans le cas optimiste?
quetzalcoatl
@quetzalcoatl: Au moins, c'est sur leur radar :-) EF a parcouru un long chemin depuis EF 1 mais a encore du chemin à parcourir.
Eric J.
1

Toutes les réponses étaient excellentes pour expliquer le problème, mais aucune n'a vraiment résolu le problème pour moi.

J'ai trouvé que si je n'utilisais pas la relation dans l'entité parente, mais que je venais d'ajouter et de supprimer les entités enfants, tout fonctionnait très bien.

Désolé pour le VB mais c'est ce dans quoi le projet sur lequel je travaille est écrit.

L'entité parente "Report" a une relation un à plusieurs avec "ReportRole" et a la propriété "ReportRoles". Les nouveaux rôles sont transmis par une chaîne séparée par des virgules à partir d'un appel Ajax.

La première ligne supprimera toutes les entités enfants, et si j'utilisais "report.ReportRoles.Remove (f)" au lieu de "db.ReportRoles.Remove (f)" j'obtiendrais l'erreur.

report.ReportRoles.ToList.ForEach(Function(f) db.ReportRoles.Remove(f))
Dim newRoles = If(String.IsNullOrEmpty(model.RolesString), New String() {}, model.RolesString.Split(","))
newRoles.ToList.ForEach(Function(f) db.ReportRoles.Add(New ReportRole With {.ReportId = report.Id, .AspNetRoleId = f}))
Alan Bridges
la source