Django DRF多对多关联SerializerMethodField引发重复查询问题
首先,咱们先定位问题根源:
你的MyModel和MyModelRelation是多对一关系(MyModelRelation的obj是ForeignKey),所以obj.relation返回的是一个QuerySet而不是单个实例。你在序列化器里直接调用obj.relation.tag_list(),相当于对每个MyModel都要查询它的所有关联MyModelRelation,再对每个MyModelRelation查询关联的Tag——哪怕你加了prefetch_related,这种写法也没利用到预取的结果,自然会触发大量重复查询。
另外,tag_list()方法里的values_list('name', flat=True).distinct(),如果没有基于预取的内存数据处理,也会额外触发数据库查询。
下面分两种场景给你优化方案:
场景1:每个MyModel对应一个MyModelRelation(一对一关系)
如果你的业务逻辑是一个MyModel只会关联一个MyModelRelation,那首先把外键改成OneToOneField,这样反向关联会直接返回单个实例,避免QuerySet的困扰:
1. 修改模型关系
class MyModelRelation(models.Model): # 把ForeignKey改成OneToOneField,确保一对一关联 obj = models.OneToOneField(MyModel, related_name='relation', on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE) tags = models.ManyToManyField(Tag)
2. 优化序列化器,利用预取数据
直接从预取的tags集合中提取名称,避免调用实例方法触发新查询:
class MyModelSerializer(serializers.ModelSerializer): tags_list = serializers.SerializerMethodField(read_only=True) def get_tags_list(self, obj): # 从预取的tags中提取名称并去重,全程在内存处理,不触发新查询 return list({tag.name for tag in obj.relation.tags.all()}) class Meta: model = MyModel fields = ['name', 'tags_list']
3. 视图保持预取配置
class ObjListView(ListAPIView): serializer_class = MyModelSerializer queryset = MyModel.objects.all() def get_queryset(self): # select_related关联查询MyModelRelation,prefetch_related预取关联的tags return super().get_queryset().select_related('relation').prefetch_related('relation__tags')
场景2:每个MyModel对应多个MyModelRelation(一对多关系)
如果一个MyModel会关联多个MyModelRelation,咱们直接在数据库层面聚合所有关联的标签,一次查询搞定所有数据:
1. 使用数据库聚合(以PostgreSQL为例)
借助ArrayAgg聚合函数,直接在MyModel的查询中把所有关联标签的名称聚合为一个列表:
from django.db.models import OuterRef from django.contrib.postgres.aggregates import ArrayAgg from rest_framework import serializers class MyModelSerializer(serializers.ModelSerializer): # 直接用ListField接收聚合后的标签列表 tags_list = serializers.ListField(child=serializers.CharField(), read_only=True) class Meta: model = MyModel fields = ['name', 'tags_list'] class ObjListView(ListAPIView): serializer_class = MyModelSerializer def get_queryset(self): # 用annotate在数据库层面聚合所有关联的标签名称,自动去重 return MyModel.objects.annotate( tags_list=ArrayAgg('relation__tags__name', distinct=True) )
2. 非PostgreSQL环境的替代方案
如果用的是MySQL等不支持ArrayAgg的数据库,可以用StringAgg把标签名拼接成字符串,再在序列化器里拆分:
from django.db.models import StringAgg class MyModelSerializer(serializers.ModelSerializer): tags_list = serializers.SerializerMethodField(read_only=True) def get_tags_list(self, obj): return obj.tags_list.split(',') if obj.tags_list else [] class Meta: model = MyModel fields = ['name', 'tags_list'] class ObjListView(ListAPIView): serializer_class = MyModelSerializer def get_queryset(self): return MyModel.objects.annotate( tags_list=StringAgg('relation__tags__name', separator=',', distinct=True) )
额外提示
你模型里的ChartField应该是笔误,记得改成CharField哦!
内容的提问来源于stack exchange,提问作者Anuj TBE

