You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DRF中无法关联多个订单项至订单,无法获取嵌套数据

问题

开发餐厅API时,用户购物车下单需将商品复制至OrderItem模型,同时创建Order模型存储总价、订单状态等信息。目前添加商品到购物车功能正常,但调用订单接口时返回结果缺失items字段,无法获取嵌套的订单项数据。

相关代码

models.py

class Category(models.Model):
    slug = models.SlugField()
    title = models.CharField(max_length=255,db_index=True)
    
    def __str__(self) -> str:
        return self.title
    
class MenuItem(models.Model):
    title = models.CharField(max_length=255,db_index=True)
    price = models.DecimalField(max_digits=4,decimal_places=2)
    featured = models.BooleanField(db_index=True,null=True)
    category = models.ForeignKey(Category,on_delete=models.PROTECT)

    def __str__(self) -> str:
        return self.title

class Cart(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    menuitem = models.ForeignKey(MenuItem, on_delete=models.CASCADE)
    quantity = models.IntegerField()
    unit_price = models.DecimalField(max_digits=8,decimal_places=1)
    price = models.DecimalField(max_digits=6,decimal_places=1)

    class Meta:
        unique_together = ['menuitem','user']

class OrderList(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    menuitem = models.ForeignKey(MenuItem, on_delete=models.CASCADE)
    quantity = models.SmallIntegerField()
    unit_price = models.DecimalField(max_digits=6,decimal_places=1)
    price = models.DecimalField(max_digits=6,decimal_places=1)

class Test(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)

class Order(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    delivery_crew = models.ForeignKey(User, related_name='delivery_crew', on_delete=models.CASCADE,null=True)
    total = models.DecimalField(max_digits=6,decimal_places=1)
    status = models.BooleanField(default=0)
    date = models.DateField(db_index=True,auto_now_add=True)

class OrderItem(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE)
    menuitem = models.ForeignKey(MenuItem, on_delete=models.CASCADE)
    quantity = models.SmallIntegerField()
    unit_price = models.DecimalField(max_digits=6,decimal_places=1)
    price = models.DecimalField(max_digits=6,decimal_places=1)

    class Meta:
        unique_together = ['menuitem','order']

views.py

class OrderView(views.APIView):
    def post(self,request):
        endpoint = request.POST.get('endpoint', False)
        if endpoint == 'http://127.0.0.1:8000/api/cart/menu-items':
            total = 0
            user = self.request.user
            cart_items = Cart.objects.filter(user=user)
            for item in cart_items:
                total += item.price
            
            if Order.objects.filter(user=user):
                o = Order.objects.get(user=user)
                o.total = total
                o.save()
            else:
                Order.objects.create(user=user,total=total)

            order = Order.objects.filter(user=user).first()

            for item in cart_items:
                menuitem = item.menuitem
                price_per_unit = float(item.unit_price)
                price = float(item.price)
                quantity = item.quantity
                OrderItem.objects.create(order=order,menuitem=menuitem,unit_price=price_per_unit,price=price,quantity=quantity)

            cart_items.delete()
   
            return Response({'message':'order placed successfully'})
        
        return Response({'message':'invalid endpoint'})
        
    def get(self, request):
        user = self.request.user
        queryset = Order.objects.filter(user=user)
        serialized_item = OrderSerializer(queryset,many=True)
        return Response(serialized_item.data)

serializer.py

class OrderItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = OrderItem
        fields = '__all__'
    
class OrderSerializer(serializers.ModelSerializer):
    items = OrderItemSerializer(many=True,read_only=True)
    class Meta:
        model = Order
        fields = '__all__'

urls.py

urlpatterns = [
    path('cart', views.CartView.as_view()),
    path('orders',views.OrderView.as_view()),
    path('orders/<int:id>',views.single_order_item),
]

当前返回结果

[
    {
        "delivery_crew": null,
        "total": "16500.0",
        "status": false,
        "date": "2023-07-19"
    }
]

期望返回格式

{
  "user": "A",
  "delivery_crew": "B",
  "items": [
    {
      "id": 5,
      "name": "Item 5"
    },
    {
      "id": 6,
      "name": "Item 6"
    }
  ],
  "total" : 123.00
}

解决方案

1. 修复Order与OrderItem的反向关联映射

Django中外键的反向关联默认使用模型名_set格式(如orderitem_set),序列化器中items字段未指定来源导致无法识别,可通过两种方式修复:

方式一:修改OrderItem模型的外键

给OrderItem的order外键添加related_name='items',明确反向关联名称:

class OrderItem(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items')
    # 其他字段保持不变

方式二:在序列化器中指定source

若不想修改模型,在OrderSerializer的items字段指定source为默认反向关联名:

class OrderSerializer(serializers.ModelSerializer):
    items = OrderItemSerializer(many=True, read_only=True, source='orderitem_set')
    # 其他代码不变

2. 优化查询,避免N+1数据库问题

获取Order查询集时,使用prefetch_related预加载关联的OrderItem数据,提升接口性能:

def get(self, request):
    user = self.request.user
    # 预加载关联的订单项
    queryset = Order.objects.filter(user=user).prefetch_related('items')
    serialized_item = OrderSerializer(queryset, many=True)
    return Response(serialized_item.data)

3. 调整序列化器字段,匹配期望格式

  • 让OrderSerializer显示user字段,可选择显示用户名或ID
  • 修改OrderItemSerializer,将menuitem的title映射为name字段,精简返回内容:
class OrderItemSerializer(serializers.ModelSerializer):
    name = serializers.CharField(source='menuitem.title')
    class Meta:
        model = OrderItem
        fields = ['id', 'name', 'quantity', 'unit_price', 'price']

class OrderSerializer(serializers.ModelSerializer):
    items = OrderItemSerializer(many=True, read_only=True)
    # 显示用户名而非ID,若需ID可移除该字段
    user = serializers.CharField(source='user.username')
    delivery_crew = serializers.CharField(source='delivery_crew.username', allow_null=True)
    
    class Meta:
        model = Order
        fields = ['user', 'delivery_crew', 'items', 'total', 'status', 'date']

4. 修复下单逻辑:每次下单创建新订单

当前代码会更新用户已有订单,不符合“一个用户可拥有多个订单”的业务逻辑,修改post方法:

def post(self,request):
    endpoint = request.POST.get('endpoint', False)
    if endpoint == 'http://127.0.0.1:8000/api/cart/menu-items':
        user = self.request.user
        cart_items = Cart.objects.filter(user=user)
        if not cart_items.exists():
            return Response({'message': 'cart is empty'}, status=status.HTTP_400_BAD_REQUEST)
            
        total = sum(item.price for item in cart_items)
        # 每次下单创建新订单
        order = Order.objects.create(user=user, total=total)

        for item in cart_items:
            # 保留Decimal类型,避免精度丢失
            OrderItem.objects.create(
                order=order,
                menuitem=item.menuitem,
                unit_price=item.unit_price,
                price=item.price,
                quantity=item.quantity
            )

        cart_items.delete()
        return Response({'message':'order placed successfully'})
    
    return Response({'message':'invalid endpoint'}, status=status.HTTP_400_BAD_REQUEST)

最终效果

调整后调用订单接口,会返回包含嵌套订单项的完整数据,格式如下:

[
    {
        "user": "user1",
        "delivery_crew": null,
        "items": [
            {
                "id": 1,
                "name": "Burger",
                "quantity": 2,
                "unit_price": "10.0",
                "price": "20.0"
            }
        ],
        "total": "20.0",
        "status": false,
        "date": "2023-07-19"
    }
]

内容的提问来源于stack exchange,提问作者user693150

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 12:17:06