django rest framework - 向后序列化以避免 prefetch_related
django rest framework - backward serialization to avoid prefetch_related
我有两个模型,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
会导致带有非常大的 sql IN
子句的额外查询,我可以通过查询避免这种情况在 Item 对象上:
Item.objects.filter(**filters).select_related('item_group')
这会导致更好的 JOIN。
是否可以查询 Item
而不是 ItemGroup
,并且还具有相同的序列化输出?
使用 prefetch_related
你将有两个查询 + 大的 IN 子句问题,尽管它已被证明是可移植的。
我会根据您的字段名称给出一个更多示例的解决方案。它将创建一个函数,该函数使用您的 select_related
queryset
从 Item
的序列化程序进行转换。它将覆盖视图的列表函数,并将一个序列化程序数据转换为另一个序列化程序数据,从而为您提供所需的表示形式。它将只使用一个查询,解析结果将在 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"
]
}]
让我们从基础开始
序列化程序只能处理给定的数据
所以这意味着为了获得一个可以序列化嵌套表示中的 ItemGroup
和 Item
对象列表的序列化程序,必须首先给出该列表。到目前为止,您已经使用对 ItemGroup
模型的查询调用 prefetch_related
来获取相关的 Item
对象。您还发现 prefetch_related
触发了第二个查询以获取那些相关对象,这并不令人满意。
prefetch_related
用于获取多个相关对象
这到底是什么意思?当您查询单个对象时,例如单个 ItemGroup
,您可以使用 prefetch_related
来获取包含多个相关对象的关系,例如反向外键(一对多)或多-已定义的对多关系。出于某些原因,Django 有意使用第二个查询来获取这些对象
select_related
中所需的连接在您强制它对第二个 table 进行连接时通常性能不佳。这是因为需要右外连接以确保不会遗漏任何 ItemGroup
个不包含 Item
的对象。
prefetch_related
使用的查询是 索引主键字段 上的 IN
,这是目前性能最高的查询之一。
- 查询只请求它知道存在的
Item
个对象的 ID,因此它可以有效地处理重复项(在多对多关系的情况下),而无需执行额外的子查询。
所有这些都是在表达:prefetch_related
正在做它应该做的事情,而且这样做是有原因的。
但无论如何我都想用 select_related
来做到这一点
好的,好的。这就是要求的,所以让我们看看可以做什么。
有几种方法可以实现这一点,所有方法都各有利弊,none 其中一些方法最终无需一些手动“拼接”工作即可工作。我假设您没有使用 DRF 提供的内置 ViewSet 或通用视图,但如果您使用,则必须在 filter_queryset
方法中进行拼接,以允许内置过滤工作。哦,它可能会破坏分页或使其几乎无用。
保留原始过滤器
原始过滤器集正在应用于 ItemGroup
对象。由于这是在 API 中使用的,因此它们可能是动态的,您不想丢失它们。因此,您将需要通过以下两种方式之一应用过滤器:
生成过滤器,然后用相关名称作为前缀
因此您将生成正常的 foo=bar
过滤器,然后在将其传递给 filter()
之前为它们添加前缀,因此它是 related__foo=bar
。这可能会对性能产生一些影响,因为您现在正在跨关系进行过滤。
生成原始子查询,然后直接传给Item
查询
这可能是“最干净”的解决方案,除了您生成的 IN
查询与 prefetch_related
查询的性能相当。除了性能更差,因为这被视为不可缓存的子查询。
实现这两个实际上超出了这个问题的范围,因为我们希望能够“翻转和拼接”Item
和 ItemGroup
对象,以便序列化器工作。
翻转 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)
我有意使用 ItemGroup
的 id
作为字典的键,因为大多数 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
是存储相关 ItemGroup
的 Item
对象的属性。
item_set
是 ItemGroup
对象的属性,相关 Item
对象的列表应该存储在该对象中。
我有两个模型,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
会导致带有非常大的 sql IN
子句的额外查询,我可以通过查询避免这种情况在 Item 对象上:
Item.objects.filter(**filters).select_related('item_group')
这会导致更好的 JOIN。
是否可以查询 Item
而不是 ItemGroup
,并且还具有相同的序列化输出?
使用 prefetch_related
你将有两个查询 + 大的 IN 子句问题,尽管它已被证明是可移植的。
我会根据您的字段名称给出一个更多示例的解决方案。它将创建一个函数,该函数使用您的 select_related
queryset
从 Item
的序列化程序进行转换。它将覆盖视图的列表函数,并将一个序列化程序数据转换为另一个序列化程序数据,从而为您提供所需的表示形式。它将只使用一个查询,解析结果将在 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"
]
}]
让我们从基础开始
序列化程序只能处理给定的数据
所以这意味着为了获得一个可以序列化嵌套表示中的 ItemGroup
和 Item
对象列表的序列化程序,必须首先给出该列表。到目前为止,您已经使用对 ItemGroup
模型的查询调用 prefetch_related
来获取相关的 Item
对象。您还发现 prefetch_related
触发了第二个查询以获取那些相关对象,这并不令人满意。
prefetch_related
用于获取多个相关对象
这到底是什么意思?当您查询单个对象时,例如单个 ItemGroup
,您可以使用 prefetch_related
来获取包含多个相关对象的关系,例如反向外键(一对多)或多-已定义的对多关系。出于某些原因,Django 有意使用第二个查询来获取这些对象
select_related
中所需的连接在您强制它对第二个 table 进行连接时通常性能不佳。这是因为需要右外连接以确保不会遗漏任何ItemGroup
个不包含Item
的对象。prefetch_related
使用的查询是 索引主键字段 上的IN
,这是目前性能最高的查询之一。- 查询只请求它知道存在的
Item
个对象的 ID,因此它可以有效地处理重复项(在多对多关系的情况下),而无需执行额外的子查询。
所有这些都是在表达:prefetch_related
正在做它应该做的事情,而且这样做是有原因的。
但无论如何我都想用 select_related
来做到这一点
好的,好的。这就是要求的,所以让我们看看可以做什么。
有几种方法可以实现这一点,所有方法都各有利弊,none 其中一些方法最终无需一些手动“拼接”工作即可工作。我假设您没有使用 DRF 提供的内置 ViewSet 或通用视图,但如果您使用,则必须在 filter_queryset
方法中进行拼接,以允许内置过滤工作。哦,它可能会破坏分页或使其几乎无用。
保留原始过滤器
原始过滤器集正在应用于 ItemGroup
对象。由于这是在 API 中使用的,因此它们可能是动态的,您不想丢失它们。因此,您将需要通过以下两种方式之一应用过滤器:
生成过滤器,然后用相关名称作为前缀
因此您将生成正常的
foo=bar
过滤器,然后在将其传递给filter()
之前为它们添加前缀,因此它是related__foo=bar
。这可能会对性能产生一些影响,因为您现在正在跨关系进行过滤。生成原始子查询,然后直接传给
Item
查询这可能是“最干净”的解决方案,除了您生成的
IN
查询与prefetch_related
查询的性能相当。除了性能更差,因为这被视为不可缓存的子查询。
实现这两个实际上超出了这个问题的范围,因为我们希望能够“翻转和拼接”Item
和 ItemGroup
对象,以便序列化器工作。
翻转 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)
我有意使用 ItemGroup
的 id
作为字典的键,因为大多数 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
是存储相关ItemGroup
的Item
对象的属性。item_set
是ItemGroup
对象的属性,相关Item
对象的列表应该存储在该对象中。