Django REST框架按日期分组订单并定制序列化响应问题
嘿,我来帮你搞定这个按日期分组订单的需求!先理清楚你的问题:你需要把Order模型的数据按ordered_at日期分组,返回嵌套格式的响应,还要用序列化器实现,同时结合DRF的分页功能对吧?
你的现状
首先看你的Order模型:
class Order(models.Model): drink = models.TextField(blank=True, null=True) total = models.IntegerField(null=True, blank=True) ordered_at = models.DateField(default=timezone.now)
你期望的响应是按日期分组,每个日期条目包含该日的所有订单,格式如下:
[ { "id": 1, "ordered_at": "2020-09-19", "orders": [ { "id": 64, "drink": "", "total": 200000, "ordered_at": "2020-09-19" }, { "id": 65, "drink": "", "total": 200000, "ordered_at": "2020-09-19" } ] }, { "id": 2, "ordered_at": "2020-09-18", "orders": [ { "id": 63, "drink": "", "total": 200000, "ordered_at": "2020-09-18" } ] } ]
你提到想使用distinct()和group_by,但不确定怎么在序列化器里落地,而且希望用序列化器而非纯APIView实现,还要加分页。下面一步步来解决:
步骤1:先搞定数据分组(避免N+1查询)
首先我们需要从数据库中把订单按日期分组,这里要注意性能,尽量减少查询次数。可以先一次性拉取所有订单,再在内存中分组:
from django.utils import timezone def get_grouped_orders(): # 一次性获取所有订单,按日期倒序排列 all_orders = Order.objects.order_by('-ordered_at') # 用字典按日期分组 date_order_map = {} for order in all_orders: # 提取日期部分(因为你的ordered_at是DateField,直接取就行) order_date = order.ordered_at if order_date not in date_order_map: date_order_map[order_date] = [] date_order_map[order_date].append(order) # 把字典转换为有序的列表,按日期倒序,同时添加分组id grouped_data = [] for idx, (date, orders) in enumerate(sorted(date_order_map.items(), key=lambda x: x[0], reverse=True), start=1): grouped_data.append({ 'id': idx, 'date': date, 'orders': orders }) return grouped_data
这样只需要一次数据库查询,比循环查每个日期的订单高效多了。
步骤2:自定义序列化器适配分组结构
因为我们需要嵌套的响应格式,所以要写两个序列化器:一个处理单个订单,一个处理分组后的日期条目:
from rest_framework import serializers from .models import Order # 单个订单的序列化器 class OrderItemSerializer(serializers.ModelSerializer): class Meta: model = Order fields = ['id', 'drink', 'total', 'ordered_at'] # 分组后的日期条目序列化器 class DateGroupedOrderSerializer(serializers.Serializer): id = serializers.IntegerField() # 把date字段映射为ordered_at,匹配你要的响应字段名 ordered_at = serializers.DateField(source='date') # 嵌套订单序列化器 orders = OrderItemSerializer(many=True)
这里用source='date'把我们分组时的date字段映射成响应里的ordered_at,完美匹配你要的格式。
步骤3:结合DRF的分页功能
DRF的默认分页是针对QuerySet的,但我们的分组数据是列表,所以需要在视图中手动处理分页。我们用APIView来实现,同时集成DRF的分页组件:
首先定义一个自定义分页类(可以根据需求调整参数):
from rest_framework.pagination import PageNumberPagination class GroupedOrderPagination(PageNumberPagination): page_size = 5 # 默认每页显示5个分组 page_size_query_param = 'page_size' # 允许前端指定每页数量 max_page_size = 20 # 最大每页数量限制
然后写视图类:
from rest_framework.views import APIView from rest_framework.response import Response class GroupedOrderListView(APIView): pagination_class = GroupedOrderPagination def get(self, request): # 获取分组好的数据 grouped_data = get_grouped_orders() # 初始化分页器 paginator = self.pagination_class() # 对分组数据进行分页 paginated_groups = paginator.paginate_queryset(grouped_data, request) # 序列化分页后的分组数据 serializer = DateGroupedOrderSerializer(paginated_groups, many=True) # 返回带分页信息的响应(包含next/previous链接等) return paginator.get_paginated_response(serializer.data)
最后别忘了在urls.py中配置这个视图的路由:
from django.urls import path from .views import GroupedOrderListView urlpatterns = [ path('grouped-orders/', GroupedOrderListView.as_view(), name='grouped-orders'), ]
补充优化方案(数据库层面分组)
如果你的订单量特别大,还可以考虑用Django的TruncDate注解来做数据库层面的分组,进一步优化性能:
from django.db.models.functions import TruncDate from django.db.models import Count date_groups = Order.objects.annotate( date=TruncDate('ordered_at') ).values('date').annotate(order_count=Count('id')).order_by('-date')
然后再根据每个date查询对应的订单,不过这种方式如果日期很多还是会有N+1问题,所以内存分组在大部分场景下更实用。
内容的提问来源于stack exchange,提问作者Hà Ngọc Khuê

