【问题标题】:Django Rest Framework: Dynamically return subset of fieldsDjango Rest Framework:动态返回字段子集
【发布时间】:2014-07-01 19:59:08
【问题描述】:

问题

按照博文 Best Practices for Designing a Pragmatic RESTful API 中的建议,我想将 fields 查询参数添加到基于 Django Rest Framework 的 API 中,使用户能够仅选择每个资源的字段子集。

示例

序列化器:

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"
  },
  ...
]

包含无效字段的查询应忽略无效字段或引发客户端错误。

目标

这可能以某种方式开箱即用吗?如果没有,实现这一点的最简单方法是什么?是否已经有第三方软件包可以做到这一点?

【问题讨论】:

    标签: django django-rest-framework


    【解决方案1】:

    您可以覆盖序列化程序__init__ 方法并根据查询参数动态设置fields 属性。您可以在整个上下文中访问request 对象,并传递给序列化程序。

    这是Django Rest Framework documentation example关于此事的复制和粘贴:

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

    【讨论】:

    • 我终于来实现这个了,而且效果很好!谢谢。我最终为此编写了一个mixin,组合比子类更灵活:) gist.github.com/dbrgn/4e6fc1fe5922598592d6
    • 您需要在最新版本的 Django 中将 QUERY_PARAMS 更改为 query_params,但除此之外,这就像一个魅力。
    • 您可能应该检查requests 是否作为context 的成员存在。虽然它在生产中这样做,但在运行手动创建对象的单元测试时却没有。
    • 仅供参考:此示例是此处找到的 DRF 文档的逐字副本:django-rest-framework.org/api-guide/serializers/#example 不提供原始作者的链接是一种不好的形式
    • 自发布此答案以来,复制此答案的 DRF documentation 已得到改进。
    【解决方案2】:

    此功能可通过3rd-party package 获得。

    pip install djangorestframework-queryfields
    

    像这样声明你的序列化器:

    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
    

    免责声明:我是作者/维护者。

    【讨论】:

    • 嗨。这和github.com/dbrgn/drf-dynamic-fields(在所选答案的 cmets 中链接)有什么区别?
    • 谢谢,我查看了那个实现,看起来它的基本思想是一样的。但是dbrgn 的实现有一些区别: 1. 不支持用fields!=key1,key2 排除。 2. 还修改了 GET 请求上下文之外的序列化程序,这会破坏一些 PUT/POST 请求。 3. 不累积字段,例如fields=key1&fields=key2,这对于 ajax 应用程序来说是一个不错的选择。它还具有零测试覆盖率,这在 OSS 中有些不寻常。
    • @wim 您的库支持哪些版本的 DRF 和 Django?我在文档中没有找到任何内容。
    • Django 1.7-1.11+,基本上是 DRF 支持的任何配置。此评论可能已过时,请查看test matrix for the CI, here
    • 非常适合我:Django==2.2.7,djangorestframework==3.10.3,djangorestframework-queryfields==1.0.0
    【解决方案3】:

    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
    

    【讨论】:

      【解决方案4】:

      配置一个新的分页序列化器类

      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 使用 homemage mixin

      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

      【讨论】:

        【解决方案5】:

        您可以尝试Dynamic REST,它支持动态字段(包含、排除)、嵌入/侧载对象、过滤、排序、分页等。

        【讨论】:

          【解决方案6】:

          如果你想要像 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"
                      }
                  ]
                },
                ...
              ]
          

          【讨论】:

            【解决方案7】:

            我们在drf_tweaks / control-over-serialized-fields 中提供了此类功能。

            如果您使用我们的序列化程序,您只需在查询中传递?fields=x,y,z 参数即可。

            【讨论】:

              【解决方案8】:

              对于嵌套数据,我使用 Django Rest Framework 和 docsdrf-flexfields 中推荐的包

              这允许您限制在父对象和子对象上返回的字段。自述文件中的说明很好,只是需要注意一些事项:

              URL 似乎需要像这样的 '/person/?expand=country&fields=id,name,country' 而不是自述文件中所写的 / '/person?expand=country&fields=id,name,country'

              嵌套对象的命名及其相关名称需要完全一致,否则不需要。

              如果你有“很多”,例如一个国家可以有多个州,您需要在序列化程序中设置 'many': True,如文档中所述。

              【讨论】:

                【解决方案9】:

                [DRF-Documentation][1] 中建议的解决方案对我有用,但是当我从视图调用序列化程序时:

                class SomeView(ListAPIView):
                    def get(self, request, *args, **kwargs):
                        qry=table.objects.filter(column_value=self.kwargs['urlparameter'])
                        fields=['DBcol1','DBcol2','DBcol3']    
                        serializer=SomeSerializer(qry,many=True,fields=fields)
                

                我必须添加many=True,否则它不起作用。

                  [1]: https://www.django-rest-framework.org/api-guide/serializers/#example
                

                【讨论】:

                  【解决方案10】:

                  另一种选择是使用 GraphWrap:https://github.com/PaulGilmartin/graph_wrap

                  通过将 /graphql 添加到您的 urlpatterns,您可以使用完全兼容的 GraphQL 可查询 API 添加您的 REST API 层。

                  【讨论】:

                    猜你喜欢
                    • 2021-08-20
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2021-09-03
                    • 2013-01-15
                    • 2019-08-19
                    • 1970-01-01
                    • 2021-12-10
                    相关资源
                    最近更新 更多