Django Rest Framework: ส่งคืนชุดย่อยของฟิลด์แบบไดนามิก


104

ปัญหา

ตามที่แนะนำในแนวทางปฏิบัติที่ดีที่สุดสำหรับการออกแบบ Pragmatic RESTful API ที่แนะนำในบล็อกฉันต้องการเพิ่มfieldsพารามิเตอร์การสืบค้นไปยัง API ที่ใช้ Django Rest Framework ซึ่งช่วยให้ผู้ใช้สามารถเลือกเฉพาะฟิลด์ย่อยต่อทรัพยากร

ตัวอย่าง

Serializer:

class IdentitySerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = models.Identity
        fields = ('id', 'url', 'type', 'data')

แบบสอบถามปกติจะส่งคืนทุกฟิลด์

GET /identities/

[
  {
    "id": 1,
    "url": "http://localhost:8000/api/identities/1/",
    "type": 5,
    "data": "John Doe"
  },
  ...
]

แบบสอบถามที่มีfieldsพารามิเตอร์ควรส่งคืนเฉพาะส่วนย่อยของฟิลด์:

GET /identities/?fields=id,data

[
  {
    "id": 1,
    "data": "John Doe"
  },
  ...
]

แบบสอบถามที่มีฟิลด์ที่ไม่ถูกต้องควรละเว้นฟิลด์ที่ไม่ถูกต้องหรือทำให้เกิดข้อผิดพลาดของไคลเอ็นต์

เป้าหมาย

เป็นไปได้หรือไม่? ถ้าไม่วิธีที่ง่ายที่สุดในการนำไปใช้คืออะไร? มีแพ็คเกจของบุคคลที่สามที่ทำสิ่งนี้อยู่แล้วหรือไม่?

คำตอบ:


126

คุณสามารถแทนที่__init__เมธอดserializer และตั้งค่าfieldsแอตทริบิวต์แบบไดนามิกโดยยึดตามพารามิเตอร์ของคิวรี คุณสามารถเข้าถึงrequestออบเจ็กต์ได้ตลอดทั้งบริบทซึ่งส่งผ่านไปยัง serializer

นี่คือสำเนาและวางจากตัวอย่างเอกสาร Django Rest Frameworkในเรื่อง:

from rest_framework import serializers

class DynamicFieldsModelSerializer(serializers.ModelSerializer):
    """
    A ModelSerializer that takes an additional `fields` argument that
    controls which fields should be displayed.
    """

    def __init__(self, *args, **kwargs):
        # Instantiate the superclass normally
        super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)

        fields = self.context['request'].query_params.get('fields')
        if fields:
            fields = fields.split(',')
            # Drop any fields that are not specified in the `fields` argument.
            allowed = set(fields)
            existing = set(self.fields.keys())
            for field_name in existing - allowed:
                self.fields.pop(field_name)


class UserSerializer(DynamicFieldsModelSerializer, serializers.HyperlinkedModelSerializer):

    class Meta:
        model = User
        fields = ('url', 'username', 'email')

4
ในที่สุดฉันก็ใช้สิ่งนี้และมันก็ทำงานได้อย่างสมบูรณ์แบบ! ขอบคุณ. ฉันลงเอยด้วยการเขียนมิกซ์อินสำหรับสิ่งนี้องค์ประกอบมีความยืดหยุ่นมากกว่าคลาสย่อยเล็กน้อย
Danilo Bargen

8
คุณจะต้องมีการเปลี่ยนแปลงQUERY_PARAMSไปquery_paramsในรุ่นล่าสุดของ Django แต่นอกเหนือจากที่ว่านี้ทำงานเหมือนจับใจ
Myk Willis

3
คุณอาจจะตรวจสอบว่าอยู่ในฐานะสมาชิกคนหนึ่งของrequests contextในขณะที่ใช้งานจริงจะไม่เกิดขึ้นเมื่อเรียกใช้การทดสอบหน่วยที่สร้างวัตถุด้วยตนเอง
smitec

21
FYI: ตัวอย่างนี้เป็นสำเนาคำต่อคำของเอกสาร DRF พบได้ที่นี่: django-rest-framework.org/api-guide/serializers/#exampleเป็นรูปแบบที่ไม่ดีที่จะไม่ให้ลิงก์ไปยังผู้เขียนต้นฉบับ
Alex Bausk

4
เอกสาร DRFจากการที่คำตอบนี้ได้รับการคัดลอกได้รับการปรับปรุงตั้งแต่คำตอบนี้ถูกโพสต์
คริส

51

ฟังก์ชั่นนี้สามารถใช้ได้จากแพคเกจของบุคคลที่ 3

pip install djangorestframework-queryfields

ประกาศ Serializer ของคุณดังนี้:

from rest_framework.serializers import ModelSerializer
from drf_queryfields import QueryFieldsMixin

class MyModelSerializer(QueryFieldsMixin, ModelSerializer):
    ...

จากนั้นสามารถระบุฟิลด์ได้แล้ว (ฝั่งไคลเอ็นต์) โดยใช้อาร์กิวเมนต์แบบสอบถาม:

GET /identities/?fields=id,data

นอกจากนี้ยังสามารถกรองการยกเว้นได้เช่นส่งคืนทุกฟิลด์ยกเว้น id:

GET /identities/?fields!=id

ข้อจำกัดความรับผิดชอบ: ฉันเป็นผู้เขียน / ผู้ดูแล


1
สวัสดี. ความแตกต่างระหว่างสิ่งนี้กับgithub.com/dbrgn/drf-dynamic-fieldsคืออะไร(ตามที่เชื่อมโยงในความคิดเห็นของคำตอบที่เลือก)
Danilo Bargen

5
ขอบคุณฉันได้ดูการใช้งานนั้นแล้วและดูเหมือนว่าจะเป็นแนวคิดพื้นฐานเดียวกัน แต่dbrgnการดำเนินงานมีความแตกต่างบางอย่าง 1. fields!=key1,key2ไม่สนับสนุนไม่รวมกับ 2. ยังปรับเปลี่ยน serializers นอกบริบทคำขอ GET ซึ่งสามารถและจะทำลายคำขอ PUT / POST บางรายการ 3. ไม่สะสมฟิลด์ด้วยเช่นfields=key1&fields=key2ซึ่งเป็นสิ่งที่ดีสำหรับแอพ ajax นอกจากนี้ยังมีการครอบคลุมการทดสอบเป็นศูนย์ซึ่งค่อนข้างผิดปกติใน OSS
Wim

1
@wim ห้องสมุดของคุณรองรับ DRF และ Django เวอร์ชันใด ฉันไม่พบสิ่งใดในเอกสาร
pawelswiecki

1
Django 1.7-1.11 + โดยพื้นฐานแล้วการกำหนดค่าใด ๆ ที่ DRF รองรับ ความคิดเห็นนี้อาจจะออกไปจากวันดังนั้นให้ตรวจสอบเมทริกซ์ทดสอบสำหรับ CI, ที่นี่
Wim

1
ใช้งานได้ดีสำหรับฉัน: Django == 2.2.7, djangorestframework == 3.10.3, djangorestframework-queryfields == 1.0.0
Neeraj Kashyap

7

serializers.py

class DynamicFieldsSerializerMixin(object):

    def __init__(self, *args, **kwargs):
        # Don't pass the 'fields' arg up to the superclass
        fields = kwargs.pop('fields', None)

        # Instantiate the superclass normally
        super(DynamicFieldsSerializerMixin, self).__init__(*args, **kwargs)

        if fields is not None:
            # Drop any fields that are not specified in the `fields` argument.
            allowed = set(fields)
            existing = set(self.fields.keys())
            for field_name in existing - allowed:
                self.fields.pop(field_name)


class UserSerializer(DynamicFieldsSerializerMixin, serializers.HyperlinkedModelSerializer):

    password = serializers.CharField(
        style={'input_type': 'password'}, write_only=True
    )

    class Meta:
        model = User
        fields = ('id', 'username', 'password', 'email', 'first_name', 'last_name')


    def create(self, validated_data):
        user = User.objects.create(
            username=validated_data['username'],
            email=validated_data['email'],
            first_name=validated_data['first_name'],
            last_name=validated_data['last_name']
        )

        user.set_password(validated_data['password'])
        user.save()

        return user

views.py

class DynamicFieldsViewMixin(object):

 def get_serializer(self, *args, **kwargs):

    serializer_class = self.get_serializer_class()

    fields = None
    if self.request.method == 'GET':
        query_fields = self.request.QUERY_PARAMS.get("fields", None)

        if query_fields:
            fields = tuple(query_fields.split(','))


    kwargs['context'] = self.get_serializer_context()
    kwargs['fields'] = fields

    return serializer_class(*args, **kwargs)



class UserList(DynamicFieldsViewMixin, ListCreateAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

3

กำหนดค่าคลาส serializer การแบ่งหน้าใหม่

from rest_framework import pagination, serializers

class DynamicFieldsPaginationSerializer(pagination.BasePaginationSerializer):
    """
    A dynamic fields implementation of a pagination serializer.
    """
    count = serializers.Field(source='paginator.count')
    next = pagination.NextPageField(source='*')
    previous = pagination.PreviousPageField(source='*')

    def __init__(self, *args, **kwargs):
        """
        Override init to add in the object serializer field on-the-fly.
        """
        fields = kwargs.pop('fields', None)
        super(pagination.BasePaginationSerializer, self).__init__(*args, **kwargs)
        results_field = self.results_field
        object_serializer = self.opts.object_serializer_class

        if 'context' in kwargs:
            context_kwarg = {'context': kwargs['context']}
        else:
            context_kwarg = {}

        if fields:
            context_kwarg.update({'fields': fields})

        self.fields[results_field] = object_serializer(source='object_list',
                                                       many=True,
                                                       **context_kwarg)


# Set the pagination serializer setting
REST_FRAMEWORK = {
    # [...]
    'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'DynamicFieldsPaginationSerializer',
}

สร้างอนุกรมแบบไดนามิก

from rest_framework import serializers

class DynamicFieldsModelSerializer(serializers.ModelSerializer):
    """
    A ModelSerializer that takes an additional `fields` argument that
    controls which fields should be displayed.

    See:
        http://tomchristie.github.io/rest-framework-2-docs/api-guide/serializers
    """

    def __init__(self, *args, **kwargs):
        # Don't pass the 'fields' arg up to the superclass
        fields = kwargs.pop('fields', None)

        # Instantiate the superclass normally
        super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)

        if fields:
            # Drop any fields that are not specified in the `fields` argument.
            allowed = set(fields)
            existing = set(self.fields.keys())
            for field_name in existing - allowed:
                self.fields.pop(field_name)
# Use it
class MyPonySerializer(DynamicFieldsModelSerializer):
    # [...]

สุดท้ายใช้มิกซ์อินโฮมเมจสำหรับ APIViews ของคุณ

class DynamicFields(object):
    """A mixins that allows the query builder to display certain fields"""

    def get_fields_to_display(self):
        fields = self.request.GET.get('fields', None)
        return fields.split(',') if fields else None

    def get_serializer(self, instance=None, data=None, files=None, many=False,
                       partial=False, allow_add_remove=False):
        """
        Return the serializer instance that should be used for validating and
        deserializing input, and for serializing output.
        """
        serializer_class = self.get_serializer_class()
        context = self.get_serializer_context()
        fields = self.get_fields_to_display()
        return serializer_class(instance, data=data, files=files,
                                many=many, partial=partial,
                                allow_add_remove=allow_add_remove,
                                context=context, fields=fields)

    def get_pagination_serializer(self, page):
        """
        Return a serializer instance to use with paginated data.
        """
        class SerializerClass(self.pagination_serializer_class):
            class Meta:
                object_serializer_class = self.get_serializer_class()

        pagination_serializer_class = SerializerClass
        context = self.get_serializer_context()
        fields = self.get_fields_to_display()
        return pagination_serializer_class(instance=page, context=context, fields=fields)

class MyPonyList(DynamicFields, generics.ListAPIView):
    # [...]

ขอ

ตอนนี้เมื่อคุณขอทรัพยากรคุณสามารถเพิ่มพารามิเตอร์fieldsเพื่อแสดงเฉพาะฟิลด์ที่ระบุใน url /?fields=field1,field2

คุณสามารถดูการแจ้งเตือนได้ที่นี่: https://gist.github.com/Kmaschta/e28cf21fb3f0b90c597a


2

ฟังก์ชั่นดังกล่าวเราได้ให้ไว้ในdrf_tweaks / ควบคุม-over-เขตต่อเนื่อง

หากคุณใช้ซีเรียล?fields=x,y,zไลเซอร์ของเราสิ่งที่คุณต้องมีคือส่งผ่านพารามิเตอร์ในแบบสอบถาม


2

คุณสามารถลองใช้Dynamic RESTซึ่งรองรับฟิลด์ไดนามิก (การรวมการยกเว้น) อ็อบเจ็กต์ฝัง / ไซด์โหลดการกรองการจัดลำดับการแบ่งหน้าและอื่น ๆ


2

สำหรับข้อมูลที่ซ้อนกันฉันใช้ Django Rest Framework กับแพ็คเกจที่แนะนำในdocs , drf-flexfields

สิ่งนี้ช่วยให้คุณสามารถ จำกัด เขตข้อมูลที่ส่งคืนทั้งในวัตถุแม่และลูก คำแนะนำใน readme เป็นสิ่งที่ดีเพียงไม่กี่สิ่งที่ควรระวัง:

ดูเหมือนว่า URL จะต้องมี / like this '/ person /? expand = country & fields = id, name, country' แทนตามที่เขียนใน readme '/ person? expand = country & fields = id, name, country'

การตั้งชื่อของวัตถุที่ซ้อนกันและชื่อที่เกี่ยวข้องจะต้องสอดคล้องกันอย่างสมบูรณ์ซึ่งไม่จำเป็นต้องใช้เป็นอย่างอื่น

หากคุณมี 'หลาย' เช่นประเทศสามารถมีได้หลายรัฐคุณจะต้องตั้งค่า 'หลาย': True ใน Serializer ตามที่อธิบายไว้ในเอกสาร


1

หากคุณต้องการสิ่งที่มีความยืดหยุ่นเช่น GraphQL คุณสามารถใช้Django-restql รองรับข้อมูลที่ซ้อนกัน (ทั้งแบบแบนและซ้ำได้)

ตัวอย่าง

from rest_framework import serializers
from django.contrib.auth.models import User
from django_restql.mixins import DynamicFieldsMixin

class UserSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('id', 'username', 'email', 'groups')

คำขอปกติจะส่งคืนฟิลด์ทั้งหมด

GET /users

    [
      {
        "id": 1,
        "username": "yezyilomo",
        "email": "yezileliilomo@hotmail.com",
        "groups": [1,2]
      },
      ...
    ]

ในทางกลับกันการร้องขอที่มีqueryพารามิเตอร์จะส่งกลับเฉพาะส่วนย่อยของฟิลด์:

GET /users/?query={id, username}

    [
      {
        "id": 1,
        "username": "yezyilomo"
      },
      ...
    ]

ด้วยdjango-restqlคุณสามารถเข้าถึงฟิลด์ที่ซ้อนกันได้ทุกระดับ เช่น

GET /users/?query={id, username, date_joined{year}}

    [
      {
        "id": 1,
        "username": "yezyilomo",
        "date_joined": {
            "year": 2018
        }
      },
      ...
    ]

สำหรับเขตข้อมูลที่ซ้อนกันที่ทำซ้ำได้เช่นกลุ่มผู้ใช้

GET /users/?query={id, username, groups{id, name}}

    [
      {
        "id": 1,
        "username": "yezyilomo",
        "groups": [
            {
                "id": 2,
                "name": "Auth_User"
            }
        ]
      },
      ...
    ]
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.