django rest framework - 向后序列化以避免 prefetch_related

django rest framework - backward serialization to avoid prefetch_related

我有两个模型,ItemItemGroup:

class ItemGroup(models.Model):
   group_name = models.CharField(max_length=50)
   # fields..

class Item(models.Model):
   item_name = models.CharField(max_length=50)
   item_group = models.ForeignKey(ItemGroup, on_delete=models.CASCADE)
   # other fields..

我想编写一个序列化程序,它将获取所有项目组及其项目列表作为嵌套数组。

所以我想要这个输出:

[ {group_name: "item group name", "items": [... list of items ..] }, ... ]

如我所见,我应该用 django rest 框架来写这个:

class ItemGroupSerializer(serializers.ModelSerializer):
   class Meta:
      model = ItemGroup
      fields = ('item_set', 'group_name') 

意味着,我必须为 ItemGroup(而不是 Item)编写一个序列化程序。 为了避免很多查询,我传递了这个查询集:

ItemGroup.objects.filter(**filters).prefetch_related('item_set')

我看到的问题是,对于大型数据集,prefetch_related 会导致带有非常大的 sql IN 子句的额外查询,我可以通过查询避免这种情况在 Item 对象上:

Item.objects.filter(**filters).select_related('item_group')

这会导致更好的 JOIN。

是否可以查询 Item 而不是 ItemGroup,并且还具有相同的序列化输出?

使用 prefetch_related 你将有两个查询 + 大的 IN 子句问题,尽管它已被证明是可移植的。

我会根据您的字段名称给出一个更多示例的解决方案。它将创建一个函数,该函数使用您的 select_related querysetItem 的序列化程序进行转换。它将覆盖视图的列表函数,并将一个序列化程序数据转换为另一个序列化程序数据,从而为您提供所需的表示形式。它将只使用一个查询,解析结果将在 O(n) 中,因此应该很快。

您可能需要重构 get_data 以便在结果中添加更多字段。

class ItemSerializer(serializers.ModelSerializer):
    group_name = serializers.CharField(source='item_group.group_name')

    class Meta:
        model = Item
        fields = ('item_name', 'group_name')

class ItemGSerializer(serializers.Serializer):
    group_name = serializers.CharField(max_length=50)
    items = serializers.ListField(child=serializers.CharField(max_length=50))

在视图中:

class ItemGroupViewSet(viewsets.ModelViewSet):
    model = models.Item
    serializer_class = serializers.ItemSerializer
    queryset = models.Item.objects.select_related('item_group').all()

    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())

        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            data = self.get_data(serializer.data)
            s = serializers.ItemGSerializer(data, many=True)
            return self.get_paginated_response(s.data)

        serializer = self.get_serializer(queryset, many=True)
        data = self.get_data(serializer.data)
        s = serializers.ItemGSerializer(data, many=True)
        return Response(s.data)

    @staticmethod
    def get_data(data):
        result, current_group = [], None
        for elem in data:
            if current_group is None:
                current_group = {'group_name': elem['group_name'], 'items': [elem['item_name']]}
            else:
                if elem['group_name'] == current_group['group_name']:
                    current_group['items'].append(elem['item_name'])
                else:
                    result.append(current_group)
                    current_group = {'group_name': elem['group_name'], 'items': [elem['item_name']]}

        if current_group is not None:
            result.append(current_group)
        return result

这是我的假数据结果:

[{
    "group_name": "group #2",
    "items": [
        "first item",
        "2 item",
        "3 item"
    ]
},
{
    "group_name": "group #1",
    "items": [
        "g1 #1",
        "g1 #2",
        "g1 #3"
    ]
}]

让我们从基础开始

序列化程序只能处理给定的数据

所以这意味着为了获得一个可以序列化嵌套表示中的 ItemGroupItem 对象列表的序列化程序,必须首先给出该列表。到目前为止,您已经使用对 ItemGroup 模型的查询调用 prefetch_related 来获取相关的 Item 对象。您还发现 prefetch_related 触发了第二个查询以获取那些相关对象,这并不令人满意。

prefetch_related用于获取多个相关对象

这到底是什么意思?当您查询单个对象时,例如单个 ItemGroup,您可以使用 prefetch_related 来获取包含多个相关对象的关系,例如反向外键(一对多)或多-已定义的对多关系。出于某些原因,Django 有意使用第二个查询来获取这些对象

  1. select_related 中所需的连接在您强制它对第二个 table 进行连接时通常性能不佳。这是因为需要右外连接以确保不会遗漏任何 ItemGroup 个不包含 Item 的对象。
  2. prefetch_related 使用的查询是 索引主键字段 上的 IN,这是目前性能最高的查询之一。
  3. 查询只请求它知道存在的 Item 个对象的 ID,因此它可以有效地处理重复项(在多对多关系的情况下),而无需执行额外的子查询。

所有这些都是在表达:prefetch_related 正在做它应该做的事情,而且这样做是有原因的。

但无论如何我都想用 select_related 来做到这一点

好的,好的。这就是要求的,所以让我们看看可以做什么。

有几种方法可以实现这一点,所有方法都各有利弊,none 其中一些方法最终无需一些手动“拼接”工作即可工作。我假设您没有使用 DRF 提供的内置 ViewSet 或通用视图,但如果您使用,则必须在 filter_queryset 方法中进行拼接,以允许内置过滤工作。哦,它可能会破坏分页或使其几乎无用。

保留原始过滤器

原始过滤器集正在应用于 ItemGroup 对象。由于这是在 API 中使用的,因此它们可能是动态的,您不想丢失它们。因此,您将需要通过以下两种方式之一应用过滤器:

  1. 生成过滤器,然后用相关名称作为前缀

    因此您将生成正常的 foo=bar 过滤器,然后在将其传递给 filter() 之前为它们添加前缀,因此它是 related__foo=bar。这可能会对性能产生一些影响,因为您现在正在跨关系进行过滤。

  2. 生成原始子查询,然后直接传给Item查询

    这可能是“最干净”的解决方案,除了您生成的 IN 查询与 prefetch_related 查询的性能相当。除了性能更差,因为这被视为不可缓存的子查询。

实现这两个实际上超出了这个问题的范围,因为我们希望能够“翻转和拼接”ItemItemGroup 对象,以便序列化器工作。

翻转 Item 查询以获得 ItemGroup 个对象的列表

采用原始问题中给出的查询,其中 select_related 用于获取所有 ItemGroup 对象以及 Item 对象,您将返回一个充满的查询集Item 个对象。我们实际上想要一个 ItemGroup 对象的列表,因为我们正在使用 ItemGroupSerializer,所以我们将不得不“翻转”它。

from collections import defaultdict

items = Item.objects.filter(**filters).select_related('item_group')

item_groups_to_items = defaultdict(list)
item_groups_by_id = {}

for item in items:
    item_group = item.item_group

    item_groups_by_id[item_group.id] = item_group
    item_group_to_items[item_group.id].append(item)

我有意使用 ItemGroupid 作为字典的键,因为大多数 Django 模型不是 immutable,有时人们会重写哈希方法是主键以外的东西。

这将为您提供 ItemGroup 对象与其相关 Item 对象的映射,这最终是您再次将它们“缝合”在一起所需要的。

ItemGroup 对象与其相关的 Item 对象拼接起来

这部分实际上并不难,因为您已经拥有所有相关对象。

for item_group_id, item_group_items in item_group_to_items.items():
    item_group = item_groups_by_id[item_group_id]

    item_group.item_set = item_group_items

item_groups = item_groups_by_id.values()

这将为您提供请求的所有 ItemGroup 对象,并将它们作为 list 存储在 item_groups 变量中。每个 ItemGroup 对象都将在 item_set 属性中设置相关 Item 对象的列表。您可能需要重命名它,以免与自动生成的同名反向外键冲突。

从这里开始,您可以像往常一样在 ItemGroupSerializer 中使用它,它应该可以用于序列化。

奖励:“翻转和拼接”的通用方法

您可以非常快速地使此通用(且不可读),以用于其他类似场景:

def flip_and_stitch(itmes, group_from_item, store_in):
    from collections import defaultdict

    item_groups_to_items = defaultdict(list)
    item_groups_by_id = {}

    for item in items:
        item_group = getattr(item, group_from_item)

        item_groups_by_id[item_group.id] = item_group
        item_group_to_items[item_group.id].append(item)

    for item_group_id, item_group_items in item_group_to_items.items():
        item_group = item_groups_by_id[item_group_id]

        setattr(item_group, store_in, item_group_items)

    return item_groups_by_id.values()

你只需将其称为

item_groups = flip_and_stitch(items, 'item_group', 'item_set')

其中:

  • items 是您最初请求的项目的查询集,已经应用了 select_related 调用。
  • item_group 是存储相关 ItemGroupItem 对象的属性。
  • item_setItemGroup 对象的属性,相关 Item 对象的列表应该存储在该对象中。