web-dev-qa-db-ja.com

JSONとエンティティで循環参照の問題を回避する方法

プレゼンテーションレイヤーにJSONを使用したMVCと、データモデル/データベースにエンティティフレームワークを利用するWebサイトの作成を実験しています。私の問題は、モデルオブジェクトをJSONにシリアル化することで始まります。

私はデータベースを作成するためにコードファーストの方法を使用しています。コードを最初に実行する場合、1対多の関係(親/子)では、子は親への参照を持つ必要があります。 (サンプルコードはタイプミスかもしれませんが、写真が表示されます)

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

JsonResultを介して「親」オブジェクトを返すと、「子」に親クラスのプロパティがあるため、循環参照エラーがスローされます。

ScriptIgnore属性を試しましたが、子オブジェクトを見ることができません。ある時点で、親子ビューに情報を表示する必要があります。

私は循環参照を持たない親と子の両方の基本クラスを作成しようとしました。残念ながら、baseParentとbaseChildを送信しようとすると、これらはJSONパーサーによって派生クラスとして読み取られます(この概念が私をエスケープしていると確信しています)。

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

私が思いついた1つのソリューションは、「ビュー」モデルを作成することです。親クラスへの参照を含まないデータベースモデルの単純なバージョンを作成します。これらのビューモデルにはそれぞれ、データベースバージョンを返すメソッドと、データベースモデルをパラメーターとして取るコンストラクター(viewmodel.name = databasemodel.name)があります。この方法は機能しますが、強制されているようです。

注:これは、より議論に値すると思うので、ここに投稿しています。別のデザインパターンを利用してこの問題を解決することも、モデルで別の属性を使用するのと同じくらい簡単にすることもできます。私の検索では、この問題を克服するための良い方法を見たことがありません。

私の最終目標は、サーバーとの通信とデータの表示にJSONを多用するNice MVCアプリケーションを作成することです。レイヤー間で一貫性のあるモデルを維持しながら(または私が思いつく限り)。

13
DanScan

あなたの質問には2つの異なる主題があります。

  • JSONへのシリアル化時に循環参照を管理する方法は?
  • ビューでモデルエンティティとしてEFエンティティを使用することはどの程度安全ですか?

循環参照については、簡単な解決策はないと言って申し訳ありません。最初に、JSONを使用して循環参照を表すことができないため、次のコード:

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

結果:TypeError: Converting circular structure to JSON

唯一の選択肢は、コンポジットのコンポジット->コンポーネント部分のみを保持し、「戻るナビゲーション」コンポーネント->コンポジットを破棄することです。したがって、この例では、

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

ここではjQueryを使用して、クライアント側でこのナビゲーションプロパティを再構成することを妨げるものはありません。

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

ただし、JSON.stringifyは循環参照をシリアル化できないため、サーバーに送信する前に再度破棄する必要があります。

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

ここで、EFエンティティをビューモデルエンティティとして使用する問題があります。

最初のEFは、クラスの動的プロキシを使用して、変更検出や遅延読み込みなどの動作を実装する可能性が高いため、EFエンティティをシリアル化する場合は、それらを無効にする必要があります。

さらに、UIでEFエンティティを使用すると、すべてのデフォルトバインダーがリクエストからのすべてのフィールドを、ユーザーが設定したくないフィールドを含むエンティティフィールドにマッピングするため、危険にさらされる可能性があります。

したがって、MVCアプリを適切に設計する場合は、専用のビューモデルを使用して、内部ビジネスモデルの「根性」がクライアントに公開されないようにすることをお勧めします。したがって、特定のビューモデルをお勧めします。

7
Julien Ch.

オブジェクトのシリアル化を試みるより簡単な方法は、親/子オブジェクトのシリアル化を無効にすることです。代わりに、必要に応じて、関連付けられた親/子オブジェクトをフェッチするための個別の呼び出しを行うことができます。これはアプリケーションにとって理想的ではないかもしれませんが、それはオプションです。

これを行うには、DataContractSerializerを設定し、データモデルクラスのコンストラクターで DataContractSerializer.PreserveObjectReferences プロパティを 'false'に設定します。これは、HTTP応答のシリアライズ時にオブジェクト参照を保持しないことを指定します。

例:

JSON形式:

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

XML形式:

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

つまり、子オブジェクトが参照されているアイテムをフェッチした場合、子オブジェクトはシリアル化されません。

DataContractsSerializer クラスも参照してください。

2

循環参照を扱うJSONシリアライザー

以下は、最初の発生をシリアル化して* JSONSerializerを後続のすべての発生で最初の発生に格納することにより循環参照を処理するカスタムジャクソンreferenceの例です。

Jacksonでオブジェクトをシリアル化するときに循環参照を扱う

上記の記事からの関連する部分的なスニペット:

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();
    }
}
1
user7519

私が思いついた1つのソリューションは、「ビュー」モデルを作成することです。親クラスへの参照を含まないデータベースモデルの単純なバージョンを作成します。これらのビューモデルにはそれぞれ、データベースバージョンを返すメソッドと、データベースモデルをパラメーターとして取るコンストラクター(viewmodel.name = databasemodel.name)があります。この方法は機能しますが、強制されているようです。

最小限のデータを送信することが唯一の正しい答えです。データベースからデータを送信する場合、通常、すべての関連付けを持つすべての列を送信しても意味がありません。コンシューマは、データベースの関連付けや構造、つまりデータベースを扱う必要はありません。これにより、帯域幅が節約されるだけでなく、保守、読み取り、使用がはるかに簡単になります。データをクエリし、実際に式を送信するために必要なデータをモデル化します。必要最小限。

0
Dante