DRF中如何按Pizza价格总和或首个价格排序查询集?
问题:Django REST Framework多对多关联字段排序异常,如何按价格总和或首个价格排序
在使用DRF的OrderingFilter时,Pizza模型与PizzaPrice通过多对多字段关联,直接指定prices作为排序字段无法得到预期结果。需要实现按价格总和或首个价格对Pizza查询集进行排序。
提供的代码
models.py
class Categorie(models.Model): name = models.CharField(max_length=60, default=[0]) def __str__(self) -> str: return self.name class PizzaPrice(models.Model): price = models.DecimalField(max_digits=8, decimal_places=2) def __str__(self) -> str: return str(self.price) class Meta: ordering = ['price'] class PizzaSize(models.Model): size = models.IntegerField() class Meta: ordering = ['size'] def __str__(self) -> str: return str(self.size) class Pizza(models.Model): name = models.CharField(max_length=60) imageUrl = models.CharField(max_length=150) category = models.ForeignKey(Categorie, default=1, on_delete=models.CASCADE) rating = models.IntegerField(choices=PIZZA_RATING, default=1) sizes = models.ManyToManyField(PizzaSize, blank=True, default=[0]) prices = models.ManyToManyField(PizzaPrice, blank=True, default=[0]) def __str__(self) -> str: return self.name
serializer.py
class PizzaSerializer(serializers.ModelSerializer): category = serializers.CharField(source='category.name') sizes = serializers.SlugRelatedField(many=True, read_only=True, slug_field='size') prices = serializers.SlugRelatedField(many=True, read_only=True, slug_field='price') class Meta: model = Pizza fields = '__all__'
views.py
class PizzasList(generics.ListAPIView): queryset = Pizza.objects.all() serializer_class = PizzaSerializer pagination_class = PageNumberPagination filter_backends = [DjangoFilterBackend, OrderingFilter] filterset_fields = ['category'] ordering_fields = ['rating', 'name', 'prices']
解决方案
问题根源:多对多关联字段直接排序时,Django无法明确使用关联集合中的哪一个值进行排序,因此需要通过聚合函数或子查询生成可用于排序的单值字段。
1. 按首个价格排序(基于PizzaPrice的默认排序)
由于PizzaPrice的Meta类中已设置ordering = ['price'],默认升序排列,我们可以通过Min聚合获取最低价格(即首个价格),或者用子查询严格取关联集合的第一个元素。
方式1:使用聚合函数Min
修改views.py,导入聚合函数并给Pizza查询集添加注解字段:
from django.db.models import Min from rest_framework import generics class PizzasList(generics.ListAPIView): # 给每个Pizza添加first_price注解,对应关联的最低价格 queryset = Pizza.objects.annotate(first_price=Min('prices__price')).all() serializer_class = PizzaSerializer pagination_class = PageNumberPagination filter_backends = [DjangoFilterBackend, OrderingFilter] filterset_fields = ['category'] # 将新添加的first_price加入可排序字段 ordering_fields = ['rating', 'name', 'first_price']
方式2:使用子查询(更精准匹配PizzaPrice的排序)
如果需要严格按照PizzaPrice模型定义的排序取第一个价格,用子查询实现:
from django.db.models import Subquery, OuterRef from rest_framework import generics class PizzasList(generics.ListAPIView): # 子查询获取每个Pizza关联的第一个PizzaPrice的price值 first_price_subquery = PizzaPrice.objects.filter( pizza=OuterRef('pk') ).order_by('price').values('price')[:1] queryset = Pizza.objects.annotate(first_price=Subquery(first_price_subquery)).all() serializer_class = PizzaSerializer pagination_class = PageNumberPagination filter_backends = [DjangoFilterBackend, OrderingFilter] filterset_fields = ['category'] ordering_fields = ['rating', 'name', 'first_price']
2. 按价格总和排序
使用Sum聚合函数计算每个Pizza关联的所有价格总和,用于排序:
from django.db.models import Sum from rest_framework import generics class PizzasList(generics.ListAPIView): # 给每个Pizza添加total_price注解,对应所有关联价格的总和 queryset = Pizza.objects.annotate(total_price=Sum('prices__price')).all() serializer_class = PizzaSerializer pagination_class = PageNumberPagination filter_backends = [DjangoFilterBackend, OrderingFilter] filterset_fields = ['category'] # 将total_price加入可排序字段 ordering_fields = ['rating', 'name', 'first_price', 'total_price']
可选:在序列化器中返回注解字段
如果需要前端能看到这些排序用的字段,修改serializer.py添加对应的字段:
class PizzaSerializer(serializers.ModelSerializer): category = serializers.CharField(source='category.name') sizes = serializers.SlugRelatedField(many=True, read_only=True, slug_field='size') prices = serializers.SlugRelatedField(many=True, read_only=True, slug_field='price') # 添加注解字段,只读 first_price = serializers.DecimalField(max_digits=8, decimal_places=2, read_only=True) total_price = serializers.DecimalField(max_digits=8, decimal_places=2, read_only=True) class Meta: model = Pizza fields = '__all__'
使用方式
前端请求时,通过ordering参数指定排序字段:
- 按首个价格升序:
?ordering=first_price - 按首个价格降序:
?ordering=-first_price - 按价格总和升序:
?ordering=total_price - 按价格总和降序:
?ordering=-total_price
内容的提问来源于stack exchange,提问作者itan mare
相关产品推荐
相关产品推荐

