Kerangka sisa Django bersarang objek referensi sendiri

90

Saya memiliki model yang terlihat seperti ini:

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)

Saya berhasil mendapatkan representasi flat json dari semua kategori dengan serializer:

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

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

Sekarang yang ingin saya lakukan adalah agar daftar subkategori memiliki representasi subkategori inline json, bukan id mereka. Bagaimana saya melakukannya dengan django-rest-framework? Saya mencoba mencarinya di dokumentasi, tetapi sepertinya tidak lengkap.

Jacek Chmielewski
sumber

Jawaban:

70

Alih-alih menggunakan ManyRelatedField, gunakan serializer bersarang sebagai bidang Anda:

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')

Jika Anda ingin berurusan dengan bidang bertingkat secara sewenang-wenang, Anda harus melihat bagian menyesuaikan bidang default di dokumen. Saat ini Anda tidak dapat secara langsung mendeklarasikan sebuah serializer sebagai bidang pada dirinya sendiri, tetapi Anda dapat menggunakan metode ini untuk mengganti bidang apa yang digunakan secara default.

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()

Sebenarnya, seperti yang Anda catat di atas kurang tepat. Ini sedikit hack, tetapi Anda dapat mencoba menambahkan bidang setelah serializer sudah dinyatakan.

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

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

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

Mekanisme deklarasi hubungan rekursif adalah sesuatu yang perlu ditambahkan.


Sunting : Perhatikan bahwa sekarang tersedia paket pihak ketiga yang secara khusus menangani kasus penggunaan semacam ini. Lihat djangorestframework-recursive .

Tom Christie
sumber
4
Oke, ini berfungsi untuk kedalaman = 1. Bagaimana jika saya memiliki lebih banyak level di pohon objek - kategori memiliki subkategori yang memiliki subkategori? Saya ingin merepresentasikan seluruh pohon dengan kedalaman sembarang dengan objek sebaris. Menggunakan pendekatan Anda, saya tidak dapat menentukan bidang subkategori di SubCategorySerializer.
Jacek Chmielewski
Diedit dengan informasi lebih lanjut tentang pembuat serial referensi mandiri.
Tom Christie
4
Bagi siapa pun yang baru melihat pertanyaan ini, saya menemukan bahwa untuk setiap level rekursif ekstra, saya harus mengulang baris terakhir di edit kedua. Solusi yang aneh, tetapi tampaknya berhasil.
Jeremy Blalock
1
@TomChristie Anda masih mendapatkan anak diulang di root tho? Bagaimana saya bisa menghentikan ini?
Prometheus
20
Saya hanya ingin menunjukkan, "base_fields" tidak lagi berfungsi. Dengan DRF 3.1.0 "_declared_fields" adalah tempat keajaiban berada.
Travis Swientek
50

Solusi @ wjin bekerja dengan baik untuk saya sampai saya meningkatkan ke Django REST framework 3.0.0, yang tidak lagi menggunakan to_native . Inilah solusi DRF 3.0 saya, yang sedikit modifikasi.

Misalkan Anda memiliki model dengan kolom referensi sendiri, misalnya komentar beralur di properti yang disebut "balasan". Anda memiliki representasi pohon dari utas komentar ini, dan Anda ingin membuat serial pohon

Pertama, tentukan kelas RecursiveField Anda yang dapat digunakan kembali

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

Kemudian, untuk serializer Anda, gunakan RecursiveField untuk membuat serial nilai "replies"

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

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

Sangat mudah, dan Anda hanya perlu 4 baris kode untuk solusi yang dapat digunakan kembali.

CATATAN: Jika struktur data Anda lebih rumit daripada pohon, seperti misalnya grafik asiklik terarah (FANCY!) Maka Anda dapat mencoba paket @ wjin - lihat solusinya. Tapi saya tidak punya masalah dengan solusi ini untuk pohon berbasis MPTTModel.

Mark Chackerian
sumber
1
Apa yang dilakukan line serializer = self.parent.parent .__ class __ (value, context = self.context). Apakah itu metode to_representation ()?
Mauricio
Baris ini adalah bagian terpenting - ini memungkinkan representasi bidang untuk merujuk penyambung yang benar. Dalam contoh ini, saya yakin ini adalah CommentSerializer.
Mark Chackerian
1
Maafkan saya. Saya tidak mengerti apa yang dilakukan kode ini. Saya menjalankannya dan berhasil. Tapi saya tidak tahu bagaimana cara kerjanya.
Mauricio
Cobalah memasukkan beberapa pernyataan cetak seperti print self.parent.parent.__class__danprint self.parent.parent
Mark Chackerian
Solusi berfungsi tetapi output penghitungan serializer saya salah. Ini hanya menghitung node root. Ada ide? Ini sama dengan djangorestframework-recursive.
Lucas Veiga
39

Opsi lain yang bekerja dengan 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
yprez
sumber
6
Mengapa ini bukan jawaban yang diterima? Bekerja dengan sempurna.
Karthik RP
5
Ini bekerja dengan sangat sederhana, saya memiliki waktu yang jauh lebih mudah untuk membuatnya berfungsi daripada solusi lain yang diposting.
Nick BL
Solusi ini tidak membutuhkan kelas tambahan dan lebih mudah dipahami daripada parent.parent.__class__barangnya. Saya paling menyukainya.
SergiyKolesnikov
Pada python 3, bisa jadi seperti ini:fields = super().get_fields()
Elinaldo Monteiro
30

Terlambat untuk permainan di sini, tapi inilah solusi saya. Katakanlah saya membuat serial Blah, dengan banyak anak juga tipe Blah.

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

Dengan menggunakan bidang ini saya dapat membuat serialisasi objek yang didefinisikan secara rekursif yang memiliki banyak objek anak

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

Saya menulis bidang rekursif untuk DRF3.0 dan mengemasnya untuk pip https://pypi.python.org/pypi/djangorestframework-recursive/

wjin
sumber
1
Bekerja dengan serialisasi model MPTTM. Bagus!
Mark Chackerian
2
Anda masih mendapatkan anak diulangi di akarnya? Bagaimana saya bisa menghentikan ini?
Prometheus
Maaf @Sputnik Saya tidak mengerti apa yang Anda maksud. Apa yang saya berikan di sini berfungsi untuk kasus di mana Anda memiliki kelas Blahdan memiliki bidang yang disebut child_blahsyang terdiri dari daftar Blahobjek.
wjin
4
Ini berfungsi dengan baik sampai saya meningkatkan ke DRF 3.0, jadi saya memposting variasi 3.0.
Mark Chackerian
1
@ Falcon1 Anda dapat memfilter queryset dan hanya meneruskan node root dalam tampilan seperti queryset=Class.objects.filter(level=0). Ini menangani hal-hal lain itu sendiri.
chhantyal
15

Saya dapat mencapai hasil ini menggunakan file serializers.SerializerMethodField. Saya tidak yakin apakah ini cara terbaik, tetapi berhasil untuk saya:

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
jarussi
sumber
1
Bagi saya, itu tergantung pada pilihan antara solusi ini dan solusi yprez . Keduanya lebih jelas dan sederhana daripada solusi yang diposting sebelumnya. Solusi di sini menang karena saya menemukan bahwa ini adalah cara terbaik untuk menyelesaikan masalah yang disajikan oleh OP di sini dan pada saat yang sama mendukung solusi ini untuk secara dinamis memilih bidang yang akan diserialkan . Solusi Yprez menyebabkan rekursi tak terbatas atau membutuhkan komplikasi tambahan untuk menghindari rekursi dan memilih bidang dengan benar.
Louis
9

Opsi lainnya adalah menampilkan kembali tampilan yang membuat model Anda berseri. Berikut contohnya:

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)
Stefan Reinhard
sumber
Ini bagus, saya memiliki pohon dalam yang sewenang-wenang yang perlu saya buat bersambung dan ini bekerja seperti pesona!
Víðir Orri Reynisson
Jawaban yang bagus dan sangat berguna. Saat mendapatkan anak di ModelSerializer Anda tidak bisa menentukan queryset untuk mendapatkan elemen anak. Dalam hal ini Anda bisa melakukannya.
Efrin
8

Saya baru-baru ini mengalami masalah yang sama dan menemukan solusi yang tampaknya berhasil sejauh ini, bahkan untuk kedalaman yang sewenang-wenang. Solusinya adalah modifikasi kecil dari Tom Christie:

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()

Saya tidak yakin itu dapat bekerja dengan andal dalam situasi apa pun , meskipun ...

caipirginka.dll
sumber
1
Pada 2.3.8, tidak ada metode convert_object. Tetapi hal yang sama dapat dilakukan dengan mengganti metode to_native.
abhaga
6

Ini adalah adaptasi dari solusi caipirginka yang bekerja pada drf 3.0.5 dan django 2.7.4:

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')

Perhatikan bahwa CategorySerializer di baris ke-6 dipanggil dengan objek dan atribut many = True.

Wicho Valdeavellano
sumber
Luar biasa, ini berhasil untuk saya. Namun, saya pikir if 'branches'harus diubah menjadiif 'subcategories'
vabada
6

Saya pikir saya akan ikut bersenang-senang!

Melalui wjin dan Mark Chackerian, saya membuat solusi yang lebih umum, yang berfungsi untuk model dan struktur pohon langsung yang memiliki model tembus. Saya tidak yakin apakah ini termasuk dalam jawabannya sendiri tetapi saya pikir saya sebaiknya meletakkannya di suatu tempat. Saya menyertakan opsi max_depth yang akan mencegah rekursi tak terbatas, pada tingkat terdalam anak direpresentasikan sebagai URL (itu klausa final lain jika Anda lebih suka itu bukan url).

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'])
Will S
sumber
1
Ini adalah solusi yang sangat menyeluruh, namun perlu dicatat bahwa elseklausul Anda membuat asumsi tertentu tentang tampilan tersebut. Saya harus mengganti milik saya dengan return value.pksehingga mengembalikan kunci utama daripada mencoba membalikkan tampilan ke atas.
Soviut
4

Dengan Django REST framework 3.3.1, saya membutuhkan kode berikut untuk menambahkan subkategori ke kategori:

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')
AndraD
sumber
2

Solusi ini hampir mirip dengan solusi lain yang diposting di sini tetapi memiliki sedikit perbedaan dalam hal masalah pengulangan anak di tingkat akar (jika Anda menganggapnya sebagai masalah). Sebagai contoh

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'
    )

dan jika Anda memiliki pandangan ini

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

Ini akan menghasilkan hasil sebagai berikut,

[
{
    "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": []
}
]

Di sini representasi parent categoryhas a child categorydan json persis seperti yang kita ingin wakili.

tetapi Anda dapat melihat ada pengulangan child categorypada level root.

Karena beberapa orang bertanya di bagian komentar dari jawaban yang diposting di atas bahwa bagaimana kami bisa menghentikan pengulangan turunan ini di tingkat akar , cukup filter queryset Anda dengan parent=None, seperti berikut ini

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

itu akan menyelesaikan masalah.

CATATAN: Jawaban ini mungkin tidak secara langsung terkait dengan pertanyaan, tetapi masalahnya terkait. Juga pendekatan penggunaan RecursiveSerializerini mahal. Lebih baik jika Anda menggunakan opsi lain yang rawan kinerja.

Md. Tanvir Raihan
sumber
Queryset dengan filter menyebabkan kesalahan bagi saya. Tapi ini membantu menyingkirkan bidang berulang. Ganti metode to_representation di kelas serializer: stackoverflow.com/questions/37985581/…
Aaron