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

Django REST Framework含额外字段的多对多字段实现求助

Django多对多关联中间模型问题修复

你当前的核心问题是Purchase模型的ManyToManyField未关联自定义中间模型PurchaseProduct,导致Django自动生成默认中间表,和你定义的PurchaseProduct完全独立,进而序列化器和视图无法正确读写关联数据。以下是完整的修复方案:

一、修正模型定义

必须在Purchase的products字段中指定through参数关联自定义中间模型,同时添加联合唯一约束避免重复关联:

from django.db import models
import uuid
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

STATUS = [
    ("Pending", "Pending"),
    # 补充你的其他状态选项
]

class Product(models.Model):
    id = models.UUIDField(primary_key=True, unique=True, default=uuid.uuid4, editable=False)
    code = models.CharField(_("Code"), max_length=30)
    un = models.CharField(_("Measurement Unit"), max_length=30)
    description = models.TextField(_("Description"))

class Purchase(models.Model):
    id = models.UUIDField(primary_key=True, unique=True, default=uuid.uuid4, editable=False)
    created_at = models.DateTimeField(_("Created At"), default=timezone.now)
    request_date = models.DateField(_("Request Date"), default=timezone.now().date())
    obs = models.TextField(_("Observation"))
    status = models.CharField(_("Status"), choices=STATUS, default="Pending", max_length=8)
    # 关键修改:指定through关联自定义中间模型,through_fields明确关联顺序
    products = models.ManyToManyField(
        Product,
        verbose_name=_("Products"),
        through="PurchaseProduct",
        through_fields=("purchase", "product")
    )
    approval_date = models.DateField(_("Approval Date"), blank=True, null=True)

class PurchaseProduct(models.Model):
    purchase = models.ForeignKey(Purchase, on_delete=models.CASCADE)   
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    quantity = models.DecimalField(_("Quantity"), max_digits=10, decimal_places=2)
    price = models.DecimalField(_("Price"), max_digits=10, decimal_places=2)
    status = models.CharField(_("Status"), choices=STATUS, default="Pending", max_length=8)

    # 添加联合唯一约束,避免同一采购重复关联同一产品
    class Meta:
        unique_together = ("purchase", "product")

二、修正序列化器

原序列化器的update方法仅处理现有关联的更新,未支持添加新关联和移除旧关联,同时优化字段映射逻辑:

from rest_framework import serializers
from .models import Purchase, PurchaseProduct, Product
from django.utils import timezone

def retroactive_date(date_val):
    # 保留你的日期校验逻辑
    return date_val <= timezone.now().date()

class PurchaseProductSerializer(serializers.ModelSerializer):
    # 优化字段:product_id用于写入,product用于读取产品详情(可选)
    product_id = serializers.PrimaryKeyRelatedField(
        queryset=Product.objects.all(), source="product", write_only=True
    )
    product = serializers.SerializerMethodField(read_only=True)

    def get_product(self, obj):
        return {
            "id": obj.product.id,
            "code": obj.product.code,
            "description": obj.product.description
        }

    class Meta:
        model = PurchaseProduct
        fields = ("product_id", "product", "quantity", "price", "status")

class PurchaseSerializer(serializers.ModelSerializer):
    products = PurchaseProductSerializer(many=True, source="purchaseproduct_set")

    class Meta:
        model = Purchase
        fields = "__all__"

    def validate(self, data):
        if data.get("request_date") and not retroactive_date(data["request_date"]):
            raise serializers.ValidationError(
                {"request_date": "申请日期不能为过去的日期。"}
            )
        if data.get("approval_date") and not retroactive_date(data["approval_date"]):
            raise serializers.ValidationError(
                {"approval_date": "批准日期不能为过去的日期。"}
            )
        # 校验产品列表是否有重复
        products_data = data.get("purchaseproduct_set", [])
        product_ids = [item["product"].id for item in products_data]
        if len(set(product_ids)) != len(product_ids):
            raise serializers.ValidationError({"products": "不允许重复添加同一产品。"})
        return data

    def create(self, validated_data):
        products_data = validated_data.pop("purchaseproduct_set")
        purchase = Purchase.objects.create(**validated_data)
        # 批量创建中间模型实例
        PurchaseProduct.objects.bulk_create([
            PurchaseProduct(
                purchase=purchase,
                product=item["product"],
                quantity=item["quantity"],
                price=item["price"],
                status=item["status"]
            ) for item in products_data
        ])
        return purchase

    def update(self, instance, validated_data):
        products_data = validated_data.pop("purchaseproduct_set", None)

        # 更新Purchase自身字段
        for key, value in validated_data.items():
            setattr(instance, key, value)
        instance.save()

        if products_data:
            # 获取当前关联的产品ID集合
            existing_product_ids = set(
                instance.purchaseproduct_set.values_list("product__id", flat=True)
            )
            # 获取传入的产品ID集合
            incoming_product_ids = set(
                item["product"].id for item in products_data
            )

            # 移除不在传入列表中的关联
            instance.purchaseproduct_set.filter(
                product__id__in=existing_product_ids - incoming_product_ids
            ).delete()

            # 更新或创建关联
            for item in products_data:
                product = item["product"]
                purchase_product, created = PurchaseProduct.objects.get_or_create(
                    purchase=instance, product=product
                )
                purchase_product.quantity = item["quantity"]
                purchase_product.price = item["price"]
                purchase_product.status = item["status"]
                purchase_product.save()

        return instance

三、修正视图

添加单个采购的更新/删除视图,同时完善采购产品的CRUD操作:

from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
from .models import Purchase, PurchaseProduct
from .serializers import PurchaseSerializer, PurchaseProductSerializer

class PurchaseListCreateView(generics.ListCreateAPIView):
    queryset = Purchase.objects.all().prefetch_related("purchaseproduct_set__product")
    serializer_class = PurchaseSerializer
    filter_backends = [DjangoFilterBackend, filters.OrderingFilter, filters.SearchFilter]
    search_fields = ["obs", "status"]
    ordering_fields = ["created_at", "request_date"]
    filterset_fields = ["status", "request_date"]
    permission_classes = [IsAuthenticated]

class PurchaseRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Purchase.objects.all().prefetch_related("purchaseproduct_set__product")
    serializer_class = PurchaseSerializer
    permission_classes = [IsAuthenticated]

class PurchaseProductListCreateView(generics.ListCreateAPIView):
    serializer_class = PurchaseProductSerializer
    filter_backends = [filters.OrderingFilter, filters.SearchFilter]
    search_fields = ["product__code", "product__description"]
    ordering_fields = ["quantity", "price"]
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        purchase_pk = self.kwargs["pk"]
        return PurchaseProduct.objects.filter(purchase=purchase_pk).select_related("product")

class PurchaseProductRetrieveUpdateDestroyView(generics.RetrieveUpdateDestroyAPIView):
    serializer_class = PurchaseProductSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        purchase_pk = self.kwargs["pk"]
        return PurchaseProduct.objects.filter(purchase=purchase_pk)

关键修改说明

  1. 模型层:通过through参数将多对多字段与自定义中间模型绑定,添加联合唯一约束避免重复关联。
  2. 序列化器层:
    • 优化PurchaseProductSerializer,区分读写字段,同时返回产品详情提升API可用性。
    • 重写update方法,实现移除旧关联、更新现有关联、添加新关联的完整逻辑。
    • 提前校验产品重复,避免数据库层面的约束报错。
  3. 视图层:
    • 添加prefetch_related和select_related优化查询性能,避免N+1问题。
    • 补充单个采购和单个采购产品的更新/删除视图,满足完整的CRUD需求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 09:44:52