Kerangka Kerja Django Rest: Secara dinamis mengembalikan subset bidang

100

Masalah

Seperti yang direkomendasikan dalam Best Practices for Designing a Pragmatic RESTful API , saya ingin menambahkan fieldsparameter kueri ke API berbasis Django Rest Framework yang memungkinkan pengguna untuk memilih hanya subset bidang per sumber daya.

Contoh

Serializer:

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

Kueri biasa akan mengembalikan semua bidang.

GET /identities/

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

Kueri dengan fieldsparameter seharusnya hanya mengembalikan subset bidang:

GET /identities/?fields=id,data

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

Kueri dengan bidang tidak valid harus mengabaikan bidang yang tidak valid atau menimbulkan kesalahan klien.

Tujuan

Apakah ini mungkin di luar kotak entah bagaimana? Jika tidak, apa cara termudah untuk menerapkan ini? Apakah ada paket pihak ke-3 yang sudah melakukan ini?

Danilo Bargen
sumber

Jawaban:

121

Anda dapat mengganti __init__metode serializer dan menyetel fieldsatribut secara dinamis, berdasarkan parameter kueri. Anda dapat mengakses requestobjek di seluruh konteks, diteruskan ke serializer.

Berikut adalah salinan & tempel dari contoh dokumentasi Django Rest Framework tentang masalah tersebut:

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')
YAtOff
sumber
4
Saya akhirnya datang untuk menerapkan ini, dan itu bekerja dengan sempurna! Terima kasih. Saya akhirnya menulis mixin untuk ini, komposisinya sedikit lebih fleksibel daripada subclassing :) gist.github.com/dbrgn/4e6fc1fe5922598592d6
Danilo Bargen
8
Anda perlu mengganti QUERY_PARAMSke query_paramsdalam versi terbaru Django, tapi selain itu ini bekerja seperti pesona.
Myk Willis
3
Anda mungkin harus memeriksa bahwa requestsada sebagai anggota context. Meskipun dalam produksi, ia tidak menjalankan pengujian unit yang membuat objek secara manual.
smitec
21
FYI: Contoh ini adalah salinan verbatim dari dokumentasi DRF yang ditemukan di sini: django-rest-framework.org/api-guide/serializers/#example Adalah bentuk yang buruk untuk tidak memberikan tautan ke penulis asli
Alex Bausk
3
The dokumentasi DRF , dari mana jawaban ini disalin, telah membaik sejak jawaban ini telah diposting.
Chris
51

Fungsionalitas ini tersedia dari paket pihak ketiga .

pip install djangorestframework-queryfields

Deklarasikan pembuat serial Anda seperti ini:

from rest_framework.serializers import ModelSerializer
from drf_queryfields import QueryFieldsMixin

class MyModelSerializer(QueryFieldsMixin, ModelSerializer):
    ...

Kemudian bidang sekarang dapat ditentukan (sisi klien) dengan menggunakan argumen kueri:

GET /identities/?fields=id,data

Pemfilteran pengecualian juga dimungkinkan, misalnya mengembalikan setiap bidang kecuali id:

GET /identities/?fields!=id

disclaimer: Saya adalah penulis / pengelola.

wim
sumber
1
Hai. Apa perbedaan antara this dan github.com/dbrgn/drf-dynamic-fields (seperti yang ditautkan di komentar jawaban yang dipilih)?
Danilo Bargen
5
Terima kasih, saya telah melihat implementasinya, dan sepertinya itu adalah ide dasar yang sama. Namun dbrgnimplementasinya memiliki beberapa perbedaan: 1. tidak mendukung exclude with fields!=key1,key2. 2. juga memodifikasi serializers di luar konteks permintaan GET, yang dapat dan akan merusak beberapa permintaan PUT / POST. 3. tidak mengakumulasi bidang misalnya fields=key1&fields=key2, yang bagus untuk dimiliki untuk aplikasi ajax. Ini juga memiliki cakupan tes nol, yang agak tidak biasa di OSS.
wim
1
@wim Versi DRF dan Django manakah yang didukung perpustakaan Anda? Saya tidak menemukan apa pun di dokumen.
pawelswiecki
1
Django 1.7-1.11 +, pada dasarnya semua konfigurasi yang didukung DRF. Komentar ini mungkin kedaluwarsa, jadi periksa matriks uji untuk CI, di sini .
wim
1
Berfungsi bagus untuk saya: 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
Austin Malerba
sumber
3

Konfigurasikan kelas serializer pagination baru

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',
}

Buat serializer dinamis

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):
    # [...]

Terakhir, gunakan campuran homemage untuk APIViews Anda

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):
    # [...]

Permintaan

Sekarang, saat Anda meminta sumber daya, Anda dapat menambahkan parameter fieldsuntuk hanya menampilkan bidang tertentu di url. /?fields=field1,field2

Anda dapat menemukan pengingat di sini: https://gist.github.com/Kmaschta/e28cf21fb3f0b90c597a

Kmaschta
sumber
2

Anda dapat mencoba Dynamic REST , yang memiliki dukungan untuk bidang dinamis (penyertaan, pengecualian), objek yang disematkan / dipindahkan, pemfilteran, pengurutan, penomoran halaman, dan banyak lagi.

blueFast
sumber
1

Fungsionalitas yang kami sediakan di drf_tweaks / control-over-serialized-fields .

Jika Anda menggunakan serializers kami, yang Anda butuhkan hanyalah meneruskan ?fields=x,y,zparameter dalam kueri.

Paweł Krzyżaniak
sumber
1

Untuk data bersarang, saya menggunakan Django Rest Framework dengan paket yang direkomendasikan di dokumen , drf-flexfields

Ini memungkinkan Anda untuk membatasi bidang yang dikembalikan pada objek induk dan anak. Instruksi di readme bagus, hanya beberapa hal yang harus diperhatikan:

URL tampaknya membutuhkan / seperti ini '/ person /? Expand = country & field = id, name, country' alih-alih seperti yang tertulis di readme '/ person? Expand = country & fields = id, name, country'

Penamaan objek bertingkat dan nama terkaitnya harus benar-benar konsisten, yang tidak diperlukan sebaliknya.

Jika Anda memiliki 'many' misalnya suatu negara dapat memiliki banyak negara bagian, Anda harus menyetel 'many': True di Serializer seperti yang dijelaskan di dokumen.

Otak Kecil
sumber
1

Jika Anda menginginkan sesuatu yang fleksibel seperti GraphQL, Anda dapat menggunakan django-restql . Ini mendukung data bersarang (flat dan iterable).

Contoh

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

Permintaan reguler mengembalikan semua bidang.

GET /users

    [
      {
        "id": 1,
        "username": "yezyilomo",
        "email": "[email protected]",
        "groups": [1,2]
      },
      ...
    ]

Permintaan dengan queryparameter di sisi lain hanya mengembalikan subset bidang:

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

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

Dengan django-restql Anda bisa mengakses bidang bersarang di tingkat manapun. Misalnya

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

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

Untuk bidang bersarang yang dapat diulang, misalnya grup pada pengguna.

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

    [
      {
        "id": 1,
        "username": "yezyilomo",
        "groups": [
            {
                "id": 2,
                "name": "Auth_User"
            }
        ]
      },
      ...
    ]
Yezy Ilomo
sumber