Khung phần còn lại Django các đối tượng tự tham chiếu lồng nhau


88

Tôi có mô hình trông như thế này:

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)

Tôi quản lý để có được đại diện json phẳng của tất cả các danh mục với bộ nối tiếp:

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

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

Bây giờ những gì tôi muốn làm là để danh sách các danh mục phụ có biểu diễn json nội tuyến của các danh mục con thay vì id của chúng. Làm cách nào để làm điều đó với django-rest-framework? Tôi đã cố gắng tìm nó trong tài liệu, nhưng nó có vẻ không đầy đủ.

Câu trả lời:


70

Thay vì sử dụng ManyRelatedField, hãy sử dụng bộ nối tiếp lồng nhau làm trường của bạn:

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

Nếu bạn muốn xử lý các trường lồng nhau tùy ý, bạn nên xem phần tùy chỉnh trường mặc định của tài liệu. Bạn hiện không thể trực tiếp khai báo bộ tuần tự như một trường trên chính nó, nhưng bạn có thể sử dụng các phương pháp này để ghi đè những trường được sử dụng theo mặc định.

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

Trên thực tế, như bạn đã lưu ý ở trên không hoàn toàn đúng. Đây là một chút hack, nhưng bạn có thể thử thêm trường vào sau khi bộ tuần tự đã được khai báo.

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

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

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

Một cơ chế khai báo quan hệ đệ quy là điều cần được bổ sung.


Chỉnh sửa : Lưu ý rằng hiện đã có gói của bên thứ ba đề cập cụ thể đến loại trường hợp sử dụng này. Xem djangorestframework-đệ quy .


3
Ok, điều này hoạt động cho độ sâu = 1. Điều gì sẽ xảy ra nếu tôi có nhiều cấp hơn trong cây đối tượng - danh mục có danh mục con có danh mục con? Tôi muốn đại diện cho toàn bộ cây có độ sâu tùy ý với các đối tượng nội tuyến. Sử dụng cách tiếp cận của bạn, tôi không thể xác định trường danh mục con trong SubCategorySerializer.
Jacek Chmielewski 14/11/12

Đã chỉnh sửa với nhiều thông tin hơn về bộ tuần tự tham chiếu tự tham chiếu.
Tom Christie

Bây giờ tôi có KeyError at /api/category/ 'subcategories'. Btw cảm ơn cho trả lời siêu nhanh của bạn :)
Jacek Chmielewski

4
Đối với bất kỳ ai mới xem câu hỏi này, tôi thấy rằng đối với mỗi cấp đệ quy bổ sung, tôi phải lặp lại dòng cuối cùng trong lần chỉnh sửa thứ hai. Cách giải quyết kỳ lạ, nhưng có vẻ hiệu quả.
Jeremy Blalock

19
Tôi chỉ muốn chỉ ra rằng, "base_fields" không còn hoạt động nữa. Với DRF 3.1.0 "_declared_fields" là nơi điều kỳ diệu.
Travis Swientek

50

Giải pháp của @ wjin đã hoạt động hiệu quả đối với tôi cho đến khi tôi nâng cấp lên khuôn khổ Django REST 3.0.0, không dùng nữa to_native . Đây là giải pháp DRF 3.0 của tôi, là một sửa đổi nhỏ.

Giả sử bạn có một mô hình với trường tự tham chiếu, chẳng hạn như nhận xét theo chuỗi trong thuộc tính có tên "trả lời". Bạn có một đại diện cây của chuỗi nhận xét này và bạn muốn tuần tự hóa cây

Đầu tiên, xác định lớp RecursiveField có thể tái sử dụng của bạn

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

Sau đó, đối với bộ tuần tự của bạn, hãy sử dụng RecursiveField để tuần tự hóa giá trị của "câu trả lời"

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

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

Dễ dàng và bạn chỉ cần 4 dòng mã cho một giải pháp có thể sử dụng lại.

LƯU Ý: Nếu cấu trúc dữ liệu của bạn phức tạp hơn dạng cây, chẳng hạn như đồ thị xoay chiều có hướng (FANCY!) Thì bạn có thể thử gói của @ wjin - xem giải pháp của anh ấy. Nhưng tôi không gặp bất kỳ vấn đề nào với giải pháp này cho cây dựa trên MPTTModel.


1
Dòng serializer = self.parent.parent .__ class __ (value, context = self.context) làm gì. Nó có phải là phương thức to_representation () không?
Mauricio

Dòng này là phần quan trọng nhất - nó cho phép biểu diễn trường tham chiếu bộ nối tiếp chính xác. Trong ví dụ này, tôi tin rằng nó sẽ là CommentSerializer.
Mark Chackerian

1
Tôi xin lỗi. Tôi không thể hiểu mã này đang làm gì. Tôi đã chạy nó và nó hoạt động. Nhưng tôi không biết nó thực sự hoạt động như thế nào.
Mauricio

Hãy thử đặt một số báo cáo in như print self.parent.parent.__class__print self.parent.parent
Mark Chackerian

Giải pháp hoạt động nhưng đầu ra đếm của bộ nối tiếp của tôi bị sai. Nó chỉ tính các nút gốc. Có ý kiến ​​gì không? Điều này cũng tương tự với djangorestframework-đệ quy.
Lucas Veiga

37

Một tùy chọn khác hoạt động với 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
Tại sao đây không phải là câu trả lời được chấp nhận? Hoạt động hoàn hảo.
Karthik RP

5
Điều này hoạt động rất đơn giản, tôi đã có một thời gian làm việc này dễ dàng hơn nhiều so với các giải pháp khác đã đăng.
Nick BL

Giải pháp này không cần đến các lớp học thêm và dễ hiểu hơn nhiều parent.parent.__class__thứ. Tôi thích nó nhất.
SergiyKolesnikov

27

Trễ trò chơi ở đây, nhưng đây là giải pháp của tôi. Giả sử tôi đang đăng nhiều kỳ Blah, với nhiều đứa trẻ cũng thuộc loại Blah.

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

Sử dụng trường này, tôi có thể tuần tự hóa các đối tượng được định nghĩa đệ quy của mình có nhiều đối tượng con

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

Tôi đã viết một trường đệ quy cho DRF3.0 và đóng gói nó cho pip https://pypi.python.org/pypi/djangorestframework-recursive/


1
Hoạt động với việc tuần tự hóa một MPTTModel. Đẹp!
Mark Chackerian

2
Bạn vẫn nhận được con lặp lại ở gốc tho? Làm thế nào tôi có thể ngăn chặn điều này?
Prometheus

Xin lỗi @Sputnik, tôi không hiểu ý bạn. Những gì tôi đưa ra ở đây phù hợp với trường hợp bạn có một lớp Blahvà nó có một trường được gọi là trường child_blahsbao gồm danh sách các Blahđối tượng.
wjin

4
Điều này hoạt động tốt cho đến khi tôi nâng cấp lên DRF 3.0, vì vậy tôi đã đăng một biến thể 3.0.
Mark Chackerian

1
@ Falcon1 Bạn có thể lọc bộ truy vấn và chỉ chuyển các nút gốc trong các dạng xem như queryset=Class.objects.filter(level=0). Nó tự xử lý phần còn lại của mọi thứ.
chhantyal

13

Tôi đã có thể đạt được kết quả này bằng cách sử dụng a serializers.SerializerMethodField. Tôi không chắc đây có phải là cách tốt nhất hay không, nhưng đã hiệu quả với tôi:

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
Đối với tôi, nó đi đến sự lựa chọn giữa giải pháp này và giải pháp của yprez . Chúng đều rõ ràng và đơn giản hơn các giải pháp được đăng trước đó. Giải pháp ở đây đã thành công bởi vì tôi thấy rằng đó là cách tốt nhất để giải quyết vấn đề được OP trình bày ở đây và đồng thời hỗ trợ giải pháp này để chọn động các trường được tuần tự hóa . Giải pháp của Yprez gây ra một đệ quy vô hạn hoặc yêu cầu các biến chứng bổ sung để tránh đệ quy và chọn đúng các trường.
Louis

9

Một tùy chọn khác sẽ là đệ quy trong dạng xem tuần tự hóa mô hình của bạn. Đây là một ví dụ:

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)

Điều này thật tuyệt, tôi đã có một cây sâu tùy ý mà tôi cần nối tiếp và điều này hoạt động như một cái duyên!
Víðir Orri Reynisson

Câu trả lời hay và rất hữu ích. Khi nhận được phần tử con trên ModelSerializer, bạn không thể chỉ định bộ truy vấn để nhận các phần tử con. Trong trường hợp này, bạn có thể làm điều đó.
Efrin

8

Gần đây tôi đã gặp vấn đề tương tự và đã đưa ra một giải pháp có vẻ hiệu quả cho đến nay, ngay cả đối với độ sâu tùy ý. Giải pháp là một sửa đổi nhỏ từ 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()

Tuy nhiên, tôi không chắc nó có thể hoạt động một cách đáng tin cậy trong mọi tình huống ...


1
Kể từ 2.3.8, không có phương thức convert_object. Nhưng điều tương tự cũng có thể được thực hiện bằng cách ghi đè phương thức to_native.
abhaga

6

Đây là bản chuyển thể từ giải pháp caipirginka hoạt động trên drf 3.0.5 và 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')

Lưu ý rằng CategorySerializer ở dòng thứ 6 được gọi với đối tượng và thuộc tính many = True.


Thật tuyệt vời, điều này đã làm việc cho tôi. Tuy nhiên, tôi nghĩ điều if 'branches'này nên được đổi thànhif 'subcategories'
vabada

5

Tôi nghĩ rằng tôi sẽ tham gia vào niềm vui!

Qua wjinMark Chackerian, tôi đã tạo ra một giải pháp tổng quát hơn, giải pháp này hoạt động cho các mô hình cây trực tiếp và cấu trúc cây có mô hình xuyên suốt. Tôi không chắc điều này có thuộc về câu trả lời của riêng nó hay không nhưng tôi nghĩ tôi cũng có thể đặt nó ở đâu đó. Tôi đã bao gồm một tùy chọn max_depth sẽ ngăn chặn đệ quy vô hạn, ở cấp độ sâu nhất, phần tử con được biểu diễn dưới dạng URLS (đó là mệnh đề khác cuối cùng nếu bạn muốn nó không phải là 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'])

Đây là một giải pháp rất triệt để, tuy nhiên, cần lưu ý rằng elseđiều khoản của bạn đưa ra những giả định nhất định về chế độ xem. Tôi đã phải thay thế của tôi bằng return value.pkđể nó trả lại các khóa chính thay vì cố gắng đảo ngược tra cứu chế độ xem.
Soviut

4

Với Django REST framework 3.3.1, tôi cần mã sau để thêm các danh mục con vào danh mục:

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

Giải pháp này gần như tương tự với các giải pháp khác được đăng ở đây nhưng có một chút khác biệt về vấn đề lặp lại trẻ em ở cấp cơ sở (nếu bạn nghĩ nó là một vấn đề). Ví dụ

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

và nếu bạn có quan điểm này

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

Điều này sẽ tạo ra kết quả sau,

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

Ở đây biểu diễn parent categorycó a child categoryvà json chính xác là những gì chúng ta muốn nó biểu diễn.

nhưng bạn có thể thấy có sự lặp lại child categoryở cấp cơ sở.

Như một số người đang hỏi trong phần nhận xét của các câu trả lời đã đăng ở trên rằng làm thế nào chúng tôi có thể ngăn chặn sự lặp lại con này ở cấp cơ sở , chỉ cần lọc bộ truy vấn của bạn với parent=None, như sau

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

nó sẽ giải quyết vấn đề.

LƯU Ý: Câu trả lời này có thể không liên quan trực tiếp đến câu hỏi, nhưng vấn đề có liên quan bằng cách nào đó. Ngoài ra, phương pháp sử dụng RecursiveSerializernày là tốn kém. Tốt hơn nếu bạn sử dụng các tùy chọn khác thiên về hiệu suất.


Bộ truy vấn với bộ lọc đã gây ra lỗi cho tôi. Nhưng điều này đã giúp loại bỏ trường lặp lại. Ghi đè phương thức to_representation trong lớp serializer: stackoverflow.com/questions/37985581/…
Aaron
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.