DjangoRESTフレームワークのネストされた自己参照オブジェクト


88

私は次のようなモデルを持っています:

class Category(models.Model):
    parentCategory = models.ForeignKey('self', blank=True, null=True, related_name='subcategories')
    name = models.CharField(max_length=200)
    description = models.CharField(max_length=500)

私はシリアライザーですべてのカテゴリーのフラットなjson表現を得ることができました:

class CategorySerializer(serializers.HyperlinkedModelSerializer):
    parentCategory = serializers.PrimaryKeyRelatedField()
    subcategories = serializers.ManyRelatedField()

    class Meta:
        model = Category
        fields = ('parentCategory', 'name', 'description', 'subcategories')

今私がやりたいのは、サブカテゴリリストにIDの代わりにサブカテゴリのインラインjson表現を持たせることです。django-rest-frameworkでそれをどのように行うのですか?ドキュメントで見つけようとしましたが、不完全なようです。

回答:


70

ManyRelatedFieldを使用する代わりに、ネストされたシリアライザーをフィールドとして使用します。

class SubCategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = ('name', 'description')

class CategorySerializer(serializers.ModelSerializer):
    parentCategory = serializers.PrimaryKeyRelatedField()
    subcategories = serializers.SubCategorySerializer()

    class Meta:
        model = Category
        fields = ('parentCategory', 'name', 'description', 'subcategories')

任意にネストされたフィールドを処理する場合は、ドキュメントのデフォルトフィールド部分のカスタマイズを確認する必要があります。現在、シリアライザーをそれ自体のフィールドとして直接宣言することはできませんが、これらのメソッドを使用して、デフォルトで使用されるフィールドをオーバーライドできます。

class CategorySerializer(serializers.ModelSerializer):
    parentCategory = serializers.PrimaryKeyRelatedField()

    class Meta:
        model = Category
        fields = ('parentCategory', 'name', 'description', 'subcategories')

        def get_related_field(self, model_field):
            # Handles initializing the `subcategories` field
            return CategorySerializer()

実際、あなたが指摘したように、上記は完全に正しくありません。これはちょっとしたハックですが、シリアライザーがすでに宣言された後でフィールドを追加してみてください。

class CategorySerializer(serializers.ModelSerializer):
    parentCategory = serializers.PrimaryKeyRelatedField()

    class Meta:
        model = Category
        fields = ('parentCategory', 'name', 'description', 'subcategories')

CategorySerializer.base_fields['subcategories'] = CategorySerializer()

再帰的な関係を宣言するメカニズムは、追加する必要があるものです。


編集:この種のユースケースを具体的に扱うサードパーティのパッケージが利用可能になっていることに注意してください。djangorestframework-recursiveを参照してください。


3
わかりました、これはdepth = 1で機能します。オブジェクトツリーにさらにレベルがある場合はどうなりますか?カテゴリにはサブカテゴリがあり、サブカテゴリがありますか?任意の深さの木全体をインラインオブジェクトで表現したいと思います。あなたのアプローチを使用して、SubCategorySerializerでサブカテゴリフィールドを定義することはできません。
Jacek Chmielewski 2012年

自己参照シリアライザーに関する詳細情報で編集。
トムクリスティー

今私は得たKeyError at /api/category/ 'subcategories'。ところで、超高速の返信をありがとう:)
Jacek Chmielewski 2012年

4
この質問を初めて見た人は、再帰レベルが増えるごとに、2回目の編集で最後の行を繰り返す必要があることに気付きました。奇妙な回避策ですが、うまくいくようです。
Jeremy Blalock 2013

19
指摘したいのですが、「base_fields」は機能しなくなりました。DRF 3.1.0では、「_ declared_fields」が魔法の場所です。
Travis Swientek 2015年

50

@wjinのソリューションは、to_nativeを廃止するDjango RESTフレームワーク3.0.0にアップグレードするまで、私にとってはうまく機能していました。これが私のDRF3.0ソリューションです。これはわずかな変更です。

たとえば、「返信」と呼ばれるプロパティにスレッド化されたコメントなど、自己参照フィールドを持つモデルがあるとします。このコメントスレッドのツリー表現があり、ツリーをシリアル化したい

まず、再利用可能なRecursiveFieldクラスを定義します

class RecursiveField(serializers.Serializer):
    def to_representation(self, value):
        serializer = self.parent.parent.__class__(value, context=self.context)
        return serializer.data

次に、シリアライザーの場合、RecursiveFieldを使用して「返信」の値をシリアル化します。

class CommentSerializer(serializers.Serializer):
    replies = RecursiveField(many=True)

    class Meta:
        model = Comment
        fields = ('replies, ....)

簡単で、再利用可能なソリューションに必要なコードは4行だけです。

注:有向非巡回グラフ(FANCY!)のように、データ構造がツリーよりも複雑な場合は、 @ wjinのパッケージを試すことができます。彼の解決策を参照してください。しかし、MPTTModelベースのツリーのこのソリューションには問題はありませんでした。


1
line serializer = self.parent.parent .__ class __(value、context = self.context)は何をしますか。to_representation()メソッドですか?
マウリシオ2016年

この行は最も重要な部分です。これにより、フィールドの表現が正しいシリアライザーを参照できるようになります。この例では、CommentSerializerになると思います。
Mark Chackerian 2016年

1
申し訳ありません。このコードが何をしているのか理解できませんでした。私はそれを実行し、それは動作します。しかし、実際にどのように機能するのかわかりません。
マウリシオ2016年

print self.parent.parent.__class__とのようないくつかの印刷ステートメントを入れてみてくださいprint self.parent.parent
Mark Chackerian 2016年

解決策は機能しますが、シリアライザーのカウント出力が間違っています。ルートノードのみをカウントします。何か案は?djangorestframework-recursiveでも同じです。
Lucas Veiga 2017年

37

Django REST Framework 3.3.2で機能する別のオプション:

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = ('id', 'name', 'parentid', 'subcategories')

    def get_fields(self):
        fields = super(CategorySerializer, self).get_fields()
        fields['subcategories'] = CategorySerializer(many=True)
        return fields

6
なぜこれは受け入れられた答えではないのですか?完璧に動作します。
Karthik RP

5
これは非常に簡単に機能します。投稿された他のソリューションよりも、これを機能させるのにはるかに簡単な時間がありました。
ニックBL19年

このソリューションは追加のクラスを必要とせず、他のparent.parent.__class__ものよりも理解しやすいです。私はそれが一番好きです。
SergiyKolesnikov

27

ここでゲームに遅れましたが、これが私の解決策です。Blahをシリアル化していて、Blahタイプの子も複数あるとします。

    class RecursiveField(serializers.Serializer):
        def to_native(self, value):
            return self.parent.to_native(value)

このフィールドを使用して、多くの子オブジェクトを持つ再帰的に定義されたオブジェクトをシリアル化できます

    class BlahSerializer(serializers.Serializer):
        name = serializers.Field()
        child_blahs = RecursiveField(many=True)

DRF3.0の再帰フィールドを作成し、piphttps://pypi.python.org/pypi/djangorestframework-recursive/用にパッケージ化しました


1
MPTTModelのシリアル化で動作します。いいね!
Mark Chackerian 2014

2
あなたはまだ子供をルートで繰り返しますか?どうすればこれを止めることができますか?
プロメテウス

申し訳ありませんが@Sputnik私はあなたが何を意味するのか理解していません。ここで示したのは、クラスがBlahありchild_blahsBlahオブジェクトのリストで構成されるというフィールドがある場合に機能します。
wjin 2014年

4
これは、DRF 3.0にアップグレードするまではうまく機能していたので、3.0のバリエーションを投稿しました。
Mark Chackerian 2014

1
@ Falcon1クエリセットをフィルタリングして、のようなビューでのみルートノードを渡すことができますqueryset=Class.objects.filter(level=0)。それ自体が残りのことを処理します。
chhantyal 2015

13

を使用してこの結果を達成することができましたserializers.SerializerMethodField。これが最善の方法かどうかはわかりませんが、私にとってはうまくいきました。

class CategorySerializer(serializers.ModelSerializer):

    subcategories = serializers.SerializerMethodField(
        read_only=True, method_name="get_child_categories")

    class Meta:
        model = Category
        fields = [
            'name',
            'category_id',
            'subcategories',
        ]

    def get_child_categories(self, obj):
        """ self referral field """
        serializer = CategorySerializer(
            instance=obj.subcategories_set.all(),
            many=True
        )
        return serializer.data

1
私にとっては、このソリューションとyprezのソリューションのどちらを選択するかということになりました。これらは、以前に投稿されたソリューションよりも明確で単純です。ここでの解決策は、ここOPによって提示された問題を解決するための最良の方法であると同時に、シリアル化するフィールドを動的に選択するためのこの解決策をサポートすることがわかったため、勝ちました。Yprezのソリューションは、無限再帰を引き起こす、再帰を回避してフィールドを適切に選択するために追加の複雑さを必要とします。
Louis

9

もう1つのオプションは、モデルをシリアル化するビューで再帰することです。次に例を示します。

class DepartmentSerializer(ModelSerializer):
    class Meta:
        model = models.Department


class DepartmentViewSet(ModelViewSet):
    model = models.Department
    serializer_class = DepartmentSerializer

    def serialize_tree(self, queryset):
        for obj in queryset:
            data = self.get_serializer(obj).data
            data['children'] = self.serialize_tree(obj.children.all())
            yield data

    def list(self, request):
        queryset = self.get_queryset().filter(level=0)
        data = self.serialize_tree(queryset)
        return Response(data)

    def retrieve(self, request, pk=None):
        self.object = self.get_object()
        data = self.serialize_tree([self.object])
        return Response(data)

これは素晴らしいです、私はシリアル化する必要がある任意の深い木を持っていました、そしてこれは魅力のように働きました!
VíðirオッリReynisson

良いと非常に有用な答え。ModelSerializerで子を取得する場合、子要素を取得するためのクエリセットを指定することはできません。この場合、あなたはそれを行うことができます。
エフリン2014年

8

私は最近同じ問題を抱えていて、任意の深さでもこれまでのところうまくいくように見える解決策を思いつきました。解決策は、TomChristieのものを少し変更したものです。

class CategorySerializer(serializers.ModelSerializer):
    parentCategory = serializers.PrimaryKeyRelatedField()

    def convert_object(self, obj):
        #Add any self-referencing fields here (if not already done)
        if not self.fields.has_key('subcategories'):
            self.fields['subcategories'] = CategorySerializer()      
        return super(CategorySerializer,self).convert_object(obj) 

    class Meta:
        model = Category
        #do NOT include self-referencing fields here
        #fields = ('parentCategory', 'name', 'description', 'subcategories')
        fields = ('parentCategory', 'name', 'description')
#This is not needed
#CategorySerializer.base_fields['subcategories'] = CategorySerializer()

どんな状況でも確実に機能するどうかはわかりませんが...


1
2.3.8の時点では、convert_objectメソッドはありません。ただし、to_nativeメソッドをオーバーライドすることでも同じことができます。
abhaga 2013年

6

これは、drf3.0.5およびdjango2.7.4で動作するcaipirginkaソリューションからの適応です。

class CategorySerializer(serializers.ModelSerializer):

    def to_representation(self, obj):
        #Add any self-referencing fields here (if not already done)
        if 'branches' not in self.fields:
            self.fields['subcategories'] = CategorySerializer(obj, many=True)      
        return super(CategorySerializer, self).to_representation(obj) 

    class Meta:
        model = Category
        fields = ('id', 'description', 'parentCategory')

6行目のCategorySerializerは、オブジェクトとmany = True属性を使用して呼び出されることに注意してください。


素晴らしい、これは私のために働いた。ただし、次のif 'branches'ように変更する必要があると思いますif 'subcategories'
vabada 2016

5

一緒に楽しみたいと思いました!

wjinMarkChackerianを介して、より一般的なソリューションを作成しました。これは、直接ツリーのようなモデルと、スルーモデルを持つツリー構造で機能します。これがそれ自身の答えに属するかどうかはわかりませんが、どこかに置いたほうがいいと思いました。無限再帰を防ぐmax_depthオプションを含めました。最も深いレベルでは、子はURLとして表されます(URLではない場合は最後のelse句です)。

from rest_framework.reverse import reverse
from rest_framework import serializers

class RecursiveField(serializers.Serializer):
    """
    Can be used as a field within another serializer,
    to produce nested-recursive relationships. Works with
    through models, and limited and/or arbitrarily deep trees.
    """
    def __init__(self, **kwargs):
        self._recurse_through = kwargs.pop('through_serializer', None)
        self._recurse_max = kwargs.pop('max_depth', None)
        self._recurse_view = kwargs.pop('reverse_name', None)
        self._recurse_attr = kwargs.pop('reverse_attr', None)
        self._recurse_many = kwargs.pop('many', False)

        super(RecursiveField, self).__init__(**kwargs)

    def to_representation(self, value):
        parent = self.parent
        if isinstance(parent, serializers.ListSerializer):
            parent = parent.parent

        lvl = getattr(parent, '_recurse_lvl', 1)
        max_lvl = self._recurse_max or getattr(parent, '_recurse_max', None)

        # Defined within RecursiveField(through_serializer=A)
        serializer_class = self._recurse_through
        is_through = has_through = True

        # Informed by previous serializer (for through m2m)
        if not serializer_class:
            is_through = False
            serializer_class = getattr(parent, '_recurse_next', None)

        # Introspected for cases without through models.
        if not serializer_class:
            has_through = False
            serializer_class = parent.__class__

        if is_through or not max_lvl or lvl <= max_lvl: 
            serializer = serializer_class(
                value, many=self._recurse_many, context=self.context)

            # Propagate hereditary attributes.
            serializer._recurse_lvl = lvl + is_through or not has_through
            serializer._recurse_max = max_lvl

            if is_through:
                # Delay using parent serializer till next lvl.
                serializer._recurse_next = parent.__class__

            return serializer.data
        else:
            view = self._recurse_view or self.context['request'].resolver_match.url_name
            attr = self._recurse_attr or 'id'
            return reverse(view, args=[getattr(value, attr)],
                           request=self.context['request'])

これは非常に徹底的な解決策ですが、あなたのelse条項がビューについて特定の仮定をしていることに注意する価値があります。return value.pkビューを逆に検索しようとするのではなく、主キーを返すように、私のものをに置き換える必要がありました。
ソビウト2016年

4

Django RESTフレームワーク3.3.1では、サブカテゴリをカテゴリに追加するために次のコードが必要でした。

models.py

class Category(models.Model):

    id = models.AutoField(
        primary_key=True
    )

    name = models.CharField(
        max_length=45, 
        blank=False, 
        null=False
    )

    parentid = models.ForeignKey(
        'self',
        related_name='subcategories',
        blank=True,
        null=True
    )

    class Meta:
        db_table = 'Categories'

serializers.py

class SubcategorySerializer(serializers.ModelSerializer):

    class Meta:
        model = Category
        fields = ('id', 'name', 'parentid')


class CategorySerializer(serializers.ModelSerializer):
    subcategories = SubcategorySerializer(many=True, read_only=True)

    class Meta:
        model = Category
        fields = ('id', 'name', 'parentid', 'subcategories')

1

このソリューションは、ここに掲載されている他のソリューションとほぼ同じですが、ルートレベルでの子の繰り返しの問題に関してわずかな違いがあります(問題と思われる場合)。例として

class RecursiveSerializer(serializers.Serializer):
    def to_representation(self, value):
        serializer = self.parent.parent.__class__(value, context=self.context)
        return serializer.data

class CategoryListSerializer(ModelSerializer):
    sub_category = RecursiveSerializer(many=True, read_only=True)

    class Meta:
        model = Category
        fields = (
            'name',
            'slug',
            'parent', 
            'sub_category'
    )

そしてあなたがこの見解を持っているなら

class CategoryListAPIView(ListAPIView):
    queryset = Category.objects.all()
    serializer_class = CategoryListSerializer

これにより、次の結果が生成されます。

[
{
    "name": "parent category",
    "slug": "parent-category",
    "parent": null,
    "sub_category": [
        {
            "name": "child category",
            "slug": "child-category",
            "parent": 20,  
            "sub_category": []
        }
    ]
},
{
    "name": "child category",
    "slug": "child-category",
    "parent": 20,
    "sub_category": []
}
]

ここで、parent categoryはaをchild category持ち、json表現はまさに私たちが表現したいものです。

しかし、あなたはの繰り返しがあることがわかります child category、ルートレベルでのます。

上記の投稿された回答のコメントセクションで、ルートレベルでこの子の繰り返しを停止するにはどうすればよいかを尋ねている人がいるので、クエリセットを次のようにフィルタリングするだけです。parent=Noneいるので、次のように

class CategoryListAPIView(ListAPIView):
    queryset = Category.objects.filter(parent=None)
    serializer_class = CategoryListSerializer

それは問題を解決します。

注:この回答は質問に直接関連していない可能性がありますが、問題は何らかの形で関連しています。また、この使用方法RecursiveSerializerは高価です。パフォーマンスが発生しやすい他のオプションを使用する場合に適しています。


フィルタを使用したクエリセットでエラーが発生しました。しかし、これは繰り返されるフィールドを取り除くのに役立ちました。シリアライザクラスのto_representationメソッドをオーバーライドします: stackoverflow.com/questions/37985581/...
アーロン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.