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)
关键修改说明
- 模型层:通过
through参数将多对多字段与自定义中间模型绑定,添加联合唯一约束避免重复关联。 - 序列化器层:
- 优化
PurchaseProductSerializer,区分读写字段,同时返回产品详情提升API可用性。 - 重写
update方法,实现移除旧关联、更新现有关联、添加新关联的完整逻辑。 - 提前校验产品重复,避免数据库层面的约束报错。
- 优化
- 视图层:
- 添加
prefetch_related和select_related优化查询性能,避免N+1问题。 - 补充单个采购和单个采购产品的更新/删除视图,满足完整的CRUD需求。
- 添加
内容的提问来源于stack exchange,提问作者Felipe Rodrigues
相关产品推荐
相关产品推荐

