14

我有两个模型,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..

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

所以我想要这个输出:

[ {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会导致带有非常大的 sqlIN子句的额外查询,而我可以通过对 Item 对象的查询来避免这种情况:

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

这导致 JOIN 更好。

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

4

2 回答 2

7

使用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))

在视图中:

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"
    ]
}]
于 2018-12-23T22:15:58.380 回答
1

让我们从基础开始

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

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

prefetch_related用于获取多个相关对象

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

  1. 当您强制它对第二个表进行连接时,a 中所需的连接select_related通常是非性能的。这是因为需要右外连接以确保不会遗漏ItemGroup不包含 an 的对象。Item
  2. 使用的查询prefetch_related索引主键字段IN上的,这是目前性能最高的查询之一。
  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。除了性能更差,因为这被视为不可缓存的子查询。

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

翻转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)

我故意使用idofItemGroup作为字典的键,因为大多数 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()

这将为您获取所有ItemGroup请求的对象并将它们存储listitem_groups变量中。每个对象都将在属性中设置ItemGroup相关Item对象的列表。item_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()

你可以把它称为

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

在哪里:

  • items是您最初请求的项目的查询集,select_related已应用调用。
  • item_group是存储Item相关的对象的属性。ItemGroup
  • item_set是应该存储ItemGroup相关对象列表的对象的属性。Item
于 2018-12-29T20:43:04.337 回答