Comment contourner le problème de référence circulaire avec JSON et Entity


13

J'ai expérimenté la création d'un site Web qui exploite MVC avec JSON pour ma couche de présentation et le cadre d'entité pour le modèle de données / base de données. Mon problème entre en jeu avec la sérialisation de mes objets Model en JSON.

J'utilise la méthode code first pour créer ma base de données. Lors de la première méthode de code, une relation un à plusieurs (parent / enfant) nécessite que l'enfant ait une référence au parent. (Exemple de code mon être une faute de frappe, mais vous obtenez l'image)

class parent
{
   public List<child> Children{get;set;}
   public int Id{get;set;}

}
class child
{
    public int ParentId{get;set;}
    [ForeignKey("ParentId")]
    public parent MyParent{get;set;}
    public string name{get;set;}
 }

Lors du retour d'un objet "parent" via un JsonResult, une erreur de référence circulaire est levée car "enfant" a une propriété de classe parent.

J'ai essayé l'attribut ScriptIgnore mais je perds la possibilité de regarder les objets enfants. J'aurai besoin d'afficher des informations dans une vue enfant parent à un moment donné.

J'ai essayé de créer des classes de base pour les parents et les enfants qui n'ont pas de référence circulaire. Malheureusement, lorsque j'essaie d'envoyer les basesParent et baseChild, celles-ci sont lues par l'analyseur JSON comme leurs classes dérivées (je suis presque sûr que ce concept m'échappe).

Base.baseParent basep = (Base.baseParent)parent;
return Json(basep, JsonRequestBehavior.AllowGet);

La seule solution que j'ai trouvée est de créer des modèles "View". Je crée des versions simples des modèles de base de données qui n'incluent pas la référence à la classe parente. Ces modèles de vue ont chacun une méthode pour renvoyer la version de la base de données et un constructeur qui prend le modèle de base de données comme paramètre (viewmodel.name = databasemodel.name). Cette méthode semble forcée bien qu'elle fonctionne.

REMARQUE: je poste ici parce que je pense que cette discussion mérite d'être discutée. Je pourrais tirer parti d'un modèle de conception différent pour surmonter ce problème ou cela pourrait être aussi simple que d'utiliser un attribut différent sur mon modèle. Dans ma recherche, je n'ai pas vu de bonne méthode pour surmonter ce problème.

Mon objectif final serait d'avoir une belle application MVC qui exploite fortement JSON pour communiquer avec le serveur et afficher les données. Tout en maintenant un modèle cohérent à travers les couches (ou du mieux que je peux trouver).

Réponses:


6

Je vois deux sujets distincts dans votre question:

  • Comment gérer les références circulaires lors de la sérialisation en JSON?
  • Est-il sûr d'utiliser des entités EF comme entités de modèle dans vos vues?

Concernant les références circulaires, je suis désolé de dire qu'il n'y a pas de solution simple. D'abord parce que JSON ne peut pas être utilisé pour représenter des références circulaires, le code suivant:

var aParent = {Children : []}, aChild  = {Parent : aParent};
aParent.Children.push(aChild);
JSON.stringify(aParent);

Résulte en: TypeError: Converting circular structure to JSON

Le seul choix que vous avez est de ne garder que le composant composite -> composant de la composition et de supprimer le composant "navigation arrière" -> composite, ainsi dans votre exemple:

class parent
{
    public List<child> Children{get;set;}
    public int Id{get;set;}
}
class child
{
    public int ParentId{get;set;}
    [ForeignKey("ParentId"), ScriptIgnore]
    public parent MyParent{get;set;}
    public string name{get;set;}
}

Rien ne vous empêche de recomposer cette propriété de navigation côté client, ici en utilisant jQuery:

$.each(parent.Children, function(i, child) {
  child.Parent = parent;  
})

Mais ensuite, vous devrez le rejeter à nouveau avant de le renvoyer au serveur, car JSON.stringify ne pourra pas sérialiser la référence circulaire:

$.each(parent.Children, function(i, child) {
  delete child.Parent;  
})

Il y a maintenant le problème de l'utilisation des entités EF comme entités de modèle de vue.

Tout d'abord, EF est susceptible d'utiliser des proxys dynamiques de votre classe pour implémenter des comportements tels que la détection des modifications ou le chargement paresseux, vous devez les désactiver si vous souhaitez sérialiser les entités EF.

De plus, l'utilisation d'entités EF dans l'interface utilisateur peut être à risque car tout le classeur par défaut mappera chaque champ de la demande aux champs d'entités, y compris ceux que vous ne vouliez pas que l'utilisateur définisse.

Ainsi, si vous souhaitez que votre application MVC soit correctement conçue, je vous recommande d'utiliser un modèle de vue dédié pour éviter que les "entrailles" de votre modèle d'entreprise interne ne soient exposées au client, je vous recommanderais donc un modèle de vue spécifique.


Existe-t-il un moyen sophistiqué avec des techniques orientées objet que je peux contourner à la fois la référence circulaire et le problème EF.
DanScan

Existe-t-il un moyen sophistiqué avec des techniques orientées objet que je peux contourner la référence circulaire et le problème EF? Comme BaseObject est hérité par entityObject et par viewObject. Ainsi, entityObject aurait la référence circulaire mais viewObject n'aurait pas la référence circulaire. J'ai contourné cela en construisant viewObject à partir de entityObject (viewObject.name = entityObject.name) mais cela semble être une perte de temps. Comment contourner ce problème?
DanScan

Ils vous aiment beaucoup. Votre explication était très claire et facile à comprendre.
Nick

2

Une alternative plus simple à la tentative de sérialisation des objets serait de désactiver la sérialisation des objets parent / enfant. Au lieu de cela, vous pouvez effectuer un appel distinct pour récupérer les objets parent / enfant associés au fur et à mesure de vos besoins. Ce n'est peut-être pas idéal pour votre application, mais c'est une option.

Pour ce faire, vous pouvez configurer un DataContractSerializer et définir la propriété DataContractSerializer.PreserveObjectReferences sur «false» dans le constructeur de votre classe de modèle de données. Cela spécifie que les références d'objet ne doivent pas être conservées lors de la sérialisation des réponses HTTP.

Exemples:

Format Json:

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
json.SerializerSettings.PreserveReferencesHandling = 
    Newtonsoft.Json.PreserveReferencesHandling.None;

Format XML:

var xml = GlobalConfiguration.Configuration.Formatters.XmlFormatter;
var dcs = new DataContractSerializer(typeof(Employee), null, int.MaxValue, 
    false, /* preserveObjectReferences: */ false, null);
xml.SetSerializer<Employee>(dcs);

Cela signifie que si vous récupérez un élément dont les objets enfants sont référencés, les objets enfants ne seront pas sérialisés.

Voir aussi la classe DataContractsSerializer .


1

Sérialiseur JSON qui traite des références circulaires

Voici un exemple de Jackson personnalisé JSONSerializerqui traite des références circulaires en sérialisant la première occurrence et en stockant un * referenceà la première occurrence sur toutes les occurrences suivantes.

Gérer les références circulaires lors de la sérialisation d'objets avec Jackson

Extrait partiel pertinent de l'article ci-dessus:

private final Set<ObjectName> seen;

/**
 * Serialize an ObjectName with all its attributes or only its String representation if it is a circular reference.
 * @param on ObjectName to serialize
 * @param jgen JsonGenerator to build the output
 * @param provider SerializerProvider
 * @throws IOException
 * @throws JsonProcessingException
 */
@Override
public void serialize(@Nonnull final ObjectName on, @Nonnull final JsonGenerator jgen, @Nonnull final SerializerProvider provider) throws IOException, JsonProcessingException
{
    if (this.seen.contains(on))
    {
        jgen.writeString(on.toString());
    }
    else
    {
        this.seen.add(on);
        jgen.writeStartObject();
        final List<MBeanAttributeInfo> ais = this.getAttributeInfos(on);
        for (final MBeanAttributeInfo ai : ais)
        {
            final Object attribute = this.getAttribute(on, ai.getName());
            jgen.writeObjectField(ai.getName(), attribute);
        }
        jgen.writeEndObject();
    }
}

0

La seule solution que j'ai trouvée est de créer des modèles "View". Je crée des versions simples des modèles de base de données qui n'incluent pas la référence à la classe parente. Ces modèles de vue ont chacun une méthode pour renvoyer la version de la base de données et un constructeur qui prend le modèle de base de données comme paramètre (viewmodel.name = databasemodel.name). Cette méthode semble forcée bien qu'elle fonctionne.

L'envoi du strict minimum de données est la seule bonne réponse. Lorsque vous envoyez des données à partir de la base de données, il n'est généralement pas judicieux d'envoyer chaque colonne avec toutes les associations. Les consommateurs ne devraient pas avoir à gérer les associations et les structures de bases de données, c'est-à-dire les bases de données. Non seulement cela permettra d'économiser de la bande passante, mais il est également beaucoup plus facile à entretenir, à lire et à consommer. Recherchez les données, puis modélisez-les pour ce dont vous avez réellement besoin pour envoyer l'eq. le strict minimum.


Plus de temps de traitement requis lorsque vous parlez de Big Data, car vous devez maintenant tout transformer deux fois.
David van Dugteren

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.