如何在Django ORM中通过DRF序列化器实现多对多关联数据的嵌套聚合输出
核心问题说明
你现在写的序列化器是直接绑定AuthorBooks中间表的单行记录的,序列化出来的粒度和数据库里存的每一行关联记录完全对齐,本来就会输出平铺的单条关联数据,不可能靠调整fields = '__all__'的配置实现按作者聚合的效果。要实现目标,得从数据库层做分组聚合、匹配对应结构的序列化器两个方向改。
具体实现步骤
1. 调整查询集逻辑,在数据库层面完成分组聚合
不要再直接把全量的中间表白记录传给序列化器,要先按Author字段分组,把同一作者下关联的所有Book ID聚合为数组:
- 如果你的数据库是PostgreSQL,可以直接用内置的
ArrayAgg聚合方法,自动返回数组结构:from django.contrib.postgres.aggregates import ArrayAgg from .models import AuthorBooks # 替换原有的 AuthorBooks.objects.all() 查询逻辑 aggregated_queryset = AuthorBooks.objects.values("Author").annotate( books=ArrayAgg("Book", distinct=True) ) - 如果是MySQL/SQLite这类不支持原生数组返回的数据库,可以自定义
GROUP_CONCAT聚合类,拿到逗号分隔的图书ID字符串,后续在序列化层转成整数列表:from django.db.models import Aggregate, CharField class GroupConcat(Aggregate): function = "GROUP_CONCAT" template = "%(function)s(%(distinct)s%(expressions)s)" def __init__(self, expression, distinct=False, **extra): super().__init__( expression, distinct="DISTINCT" if distinct else "", output_field=CharField(), **extra ) # 聚合查询 aggregated_queryset = AuthorBooks.objects.values("Author").annotate( books=GroupConcat("Book", distinct=True) )
2. 替换原有的单行序列化器,匹配聚合后的数据结构
原来绑定单条中间表记录的AuthorBooksSerializer无法承接聚合后的分组数据,需要重写序列化器,对应目标输出的author、books两个字段:
from rest_framework import serializers class AuthorAggregatedBooksSerializer(serializers.Serializer): # 将Author字段重命名为目标输出的author,自动做类型转换 author = serializers.IntegerField(source="Author") # 用方法字段兼容不同聚合方式的返回格式 books = serializers.SerializerMethodField() def get_books(self, obj): # 兼容PostgreSQL ArrayAgg直接返回数组的场景 if isinstance(obj["books"], list): return [int(book_id) for book_id in obj["books"]] # 兼容MySQL/SQLite返回逗号分隔字符串的场景 if isinstance(obj["books"], str): return [int(book_id) for book_id in obj["books"].split(",")] return []
3. 调整视图返回逻辑,输出纯数组结构
之前拿到的“以记录主键为键的字典格式”,是因为原视图对查询集做了字典映射转换,要替换成传many=True给序列化器的逻辑,输出数组格式:
from rest_framework.views import APIView from rest_framework.response import Response class AuthorBooksAggregateView(APIView): def get(self, request): # 传入前面写好的聚合查询集 serializer = AuthorAggregatedBooksSerializer(aggregated_queryset, many=True) return Response(serializer.data)
可选优化方案
如果你在模型层面已经给Author配置了和Book的多对多关联,可以跳过自定义聚合查询,直接基于Author模型写序列化器,配合预加载机制避免N+1查询:
class AuthorAggregateSerializer(serializers.ModelSerializer): books = serializers.SerializerMethodField() class Meta: model = Author fields = ["id", "books"] def get_books(self, obj): return list(obj.books.values_list("id", flat=True)) # 视图层查询集搭配预加载优化 queryset = Author.objects.prefetch_related("books").all()
注意事项
- 别图省事在序列化器里挨个作者查关联图书,这种写法数据库查询次数会随作者数线性增长,性能很差,必须在查询阶段用聚合或者预加载把数据准备好。
- 如果原逻辑中用字典转换查询集(比如
{str(obj.pk): obj for obj in queryset})来生成输出,要删掉这部分转字典的逻辑,才能得到期望的纯数组返回结构。
内容的提问来源于stack exchange,提问作者sofetuere
相关产品推荐
相关产品推荐

