Django Rest Framework 在 SerializerMethodField 上排序

Django Rest Framework Ordering on a SerializerMethodField

我有一个论坛主题模型,我想在计算的 SerializerMethodField 上排序,例如 vote_count。这里有一个非常简化的模型、序列化器和视图集来显示问题:

# models.py
class Topic(models.Model):
    """
    An individual discussion post in the forum
    """
    title = models.CharField(max_length=60)

    def vote_count(self):
        """
        count the votes for the object
        """
        return TopicVote.objects.filter(topic=self).count()


# serializers.py
class TopicSerializer(serializers.ModelSerializer):
    vote_count = serializers.SerializerMethodField()

    def get_vote_count(self, obj):
        return obj.vote_count()

    class Meta:
        model = Topic


# views.py
class TopicViewSet(TopicMixin, viewsets.ModelViewSet):
    queryset = Topic.objects.all()
    serializer_class = TopicSerializer

这是有效的方法:

  1. OrderingFilter默认开启,我可以成功下单/topics?ordering=title
  2. vote_count 函数完美运行

我正在尝试通过 TopicSerializer 上的 MethodField 进行排序,vote_count 类似于 /topics?ordering=-vote_count,但似乎不受支持。有什么方法可以按那个字段排序吗?

我的简化 JSON 回复如下所示:

{
    "id": 1,
    "title": "first post",
    "voteCount": 1
},
{ 
    "id": 2,
    "title": "second post",
    "voteCount": 8
},
{ 
    "id": 3,
    "title": "third post",
    "voteCount": 4
}

我正在使用 Ember 来使用我的 API 并且解析器正在将其转换为驼峰式。我也尝试过 ordering=voteCount,但这不起作用(也不应该)

这不可能使用 the default OrderingFilter,因为排序是在数据库端 实现的。这是出于效率原因,因为手动对结果进行排序可能 难以置信地 缓慢并且意味着打破标准 QuerySet。通过将所有内容都保留为 QuerySet,您可以受益于 Django REST 框架(通常需要 QuerySet)提供的内置过滤和内置分页(没有分页可能会很慢)。

现在,在这些情况下您有两个选择:弄清楚如何在数据库端检索您的值,或者尽量减少您将不得不承受的性能损失。由于后一个选项是非常特定于实现的,所以我现在将跳过它。

在这种情况下,您可以使用the Count function provided by Django to do the count on the database side. This is provided as part of the aggregation API and works like the SQL COUNT function。您可以通过将视图上的 queryset 修改为

来执行等效的 Count 调用
queryset = Topic.objects.annotate(vote_count=Count('topicvote_set'))

your related_name for the field替换topicvote_set(你有一套,对吧?)。这将允许您根据投票数对结果进行排序,甚至可以进行过滤(如果您愿意),因为它在查询本身中可用。

这需要对您的序列化程序稍作更改,因此它从对象上可用的新 vote_count 属性 中提取。

class TopicSerializer(serializers.ModelSerializer):
    vote_count = serializers.IntegerField(read_only=True)

    class Meta:
        model = Topic

这将覆盖您现有的 vote_count 方法,因此您可能希望重命名注释时使用的变量(如果您不能替换旧方法)。


此外,您可以将方法名称作为 Django REST 框架字段的 source 传递,它会自动调用它。所以从技术上讲,您当前的序列化程序可能只是

class TopicSerializer(serializers.ModelSerializer):
    vote_count = serializers.IntegerField(read_only=True)

    class Meta:
        model = Topic

它会完全像现在一样工作。注意这里需要read_only,因为方法和属性不一样,所以不能设置值。

我会把它放在这里,因为描述的案例不是唯一的。 这个想法是重写你的 Viewset 的列表方法,以通过你的任何 SerializerMethodField(s) 进行排序,而不会将你的逻辑从 Serializer 移动到 ModelManager(特别是当你使用几个复杂的方法 and/or 个相关模型)

def list(self, request, *args, **kwargs):
    response = super(YourModelList, self).list(request, args, kwargs)
    ordering = request.query_params.get('ordering')
    response.data['results'] = sorted(response.data['results'], key=operator.itemgetter(ordering.replace('-',''),))

    if "-" in ordering:      
        response.data['results'] = sorted(response.data['results'], key=lambda k: (k[ordering.replace('-','')], ), reverse=True)
    else:
        response.data['results'] = sorted(response.data['results'], key=lambda k: (k[ordering], ))

    return response

感谢@Kevin Brown的精彩解释和解答!

在我的例子中,我需要对一个名为 total_donation 的 serializerMethodField 进行排序,它是来自 UserPayments table.

sum 捐款

UserPayments 有:

  • User 作为外键
  • sum 这是一个 IntegerField
  • related_name='payments'

我需要获得每个用户的总捐款,但只有状态为 'donated' 的捐款,而不是 'pending'。还需要过滤掉 payment_type coupon,它通过另外两个外键关联。

我很惊讶如何加入和过滤这些捐赠,然后能够通过 ordering_fields 对其进行排序。

感谢您的 post 我明白了! 我意识到它需要成为原始 queryset 的一部分才能使用 ordering.

进行排序

我需要做的就是在我的视图中注释查询集,使用 Sum() 和内部过滤器,如下所示:

class DashboardUserListView(generics.ListAPIView):
    donation_filter =  Q(payments__status='donated') & ~Q(payments__payment_type__payment_type='coupon')
    queryset = User.objects.annotate(total_donated=Sum('payments__sum', filter=donation_filter ))
    serializer_class = DashboardUserListSerializer
    pagination_class = DashboardUsersPagination
    filter_backends = [filters.OrderingFilter]
    ordering_fields = ['created', 'last_login', 'total_donated' ]
    ordering = ['-created',]