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

use*_*803 13 python django django-models django-related-manager django-rest-framework

我有两个型号,Item并且ItemGroup:

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..
Run Code Online (Sandbox Code Playgroud)

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

所以我想要这个输出:

[ {group_name: "item group name", "items": [... list of items ..] }, ... ]
Run Code Online (Sandbox Code Playgroud)

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

class ItemGroupSerializer(serializers.ModelSerializer):
   class Meta:
      model = ItemGroup
      fields = ('item_set', 'group_name') 
Run Code Online (Sandbox Code Playgroud)

意思是,我必须为ItemGroup(不是Item)编写序列化器.为了避免很多查询,我传递了这个查询集:

ItemGroup.objects.filter(**filters).prefetch_related('item_set')
Run Code Online (Sandbox Code Playgroud)

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

Item.objects.filter(**filters).select_related('item_group')
Run Code Online (Sandbox Code Playgroud)

这导致JOIN更好.

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

edi*_*lio 6

使用prefetch_related你将有两个查询+大的IN子句问题,虽然它已被证明和可移植.

我会根据您的字段名称给出一个更多示例的解决方案.它将创建一个函数,从序列化器转换为Item使用您的select_related queryset.它将覆盖视图的列表函数,并从一个序列化数据转换为另一个序列化数据,从而为您提供所需的表示.它将只使用一个查询并解析结果,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))
Run Code Online (Sandbox Code Playgroud)

在视图中:

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
Run Code Online (Sandbox Code Playgroud)

这是我的假数据的结果:

[{
    "group_name": "group #2",
    "items": [
        "first item",
        "2 item",
        "3 item"
    ]
},
{
    "group_name": "group #1",
    "items": [
        "g1 #1",
        "g1 #2",
        "g1 #3"
    ]
}]
Run Code Online (Sandbox Code Playgroud)


Kev*_*own 1

让我们从基础开始

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

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

prefetch_related用于获取多个相关对象

这究竟意味着什么?当您查询单个对象(例如单个 )时ItemGroup,您可以用来prefetch_related获取包含多个相关对象的关系,例如反向外键(一对多)或多对多关系。出于几个原因,Django 有意使用第二个查询来获取这些对象

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

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

select_related但无论如何我想用

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

有几种方法可以实现这一点,所有这些方法都有其优点和缺点,并且最终如果没有一些手动“缝合”工作,这些方法都不起作用。我假设您没有使用 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)
Run Code Online (Sandbox Code Playgroud)

我故意使用 ofid作为ItemGroup字典的键,因为大多数 Django 模型都不是不可变的,有时人们会覆盖哈希方法以使用主键以外的方法。

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()
Run Code Online (Sandbox Code Playgroud)

这将为您提供所请求的所有对象ItemGroup并将它们存储在变量listitem_groups。每个对象都将具有在属性中设置的相关对象ItemGroup的列表。您可能需要重命名它,这样它就不会与自动生成的同名反向外键冲突。Itemitem_set

从这里,您可以像平常一样使用它,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()
Run Code Online (Sandbox Code Playgroud)

你只需将其称为

item_groups = flip_and_stitch(items, 'item_group', 'item_set')
Run Code Online (Sandbox Code Playgroud)

在哪里:

  • items是您最初请求的项目的查询集,并且select_related已经应用​​了调用。
  • item_group是存储Item相关对象的属性。ItemGroup
  • item_set是应存储ItemGroup相关对象列表的对象的属性。Item