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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.23 05:06:08