django rest framework - обратная сериализация, чтобы избежать 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 framework:
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')
Что приводит к присоединению, что намного лучше.
Можно ли запросить Item
вместо ItemGroup
и все же иметь такой же выход сериализации?
2 ответа
Давайте начнем с основ
Сериализатор может работать только с данными, которые ему даны
Это означает, что для получения сериализатора, который может сериализовать список ItemGroup
а также Item
объекты во вложенном представлении, это должен быть дан этот список в первую очередь. Вы достигли этого до сих пор с помощью запроса на ItemGroup
модель, которая вызывает prefetch_related
чтобы получить связанный Item
объекты. Вы также определили, что prefetch_related
запускает второй запрос, чтобы получить эти связанные объекты, и это не является удовлетворительным.
prefetch_related
используется для получения нескольких связанных объектов
Что это значит точно? Когда вы запрашиваете один объект, например, один ItemGroup
, ты используешь prefetch_related
чтобы получить отношение, содержащее несколько связанных объектов, например обратный внешний ключ (один-ко-многим) или отношение многие-ко-многим, которое было определено. Джанго намеренно использует второй запрос, чтобы получить эти объекты по нескольким причинам.
- Соединение, которое потребуется в
select_related
часто не работает, когда вы заставляете его соединяться со второй таблицей. Это связано с тем, что для обеспеченияItemGroup
объекты, которые не содержатItem
пропущены. - Запрос, используемый
prefetch_related
являетсяIN
в индексированном поле первичного ключа, которое является одним из наиболее производительных запросов. - Запрос запрашивает только идентификаторы
Item
известные ему объекты существуют, поэтому он может эффективно обрабатывать дубликаты (в случае отношений "многие ко многим") без необходимости выполнять дополнительный подзапрос.
Все это способ сказать: prefetch_related
делает именно то, что должно делать, и делает это по определенной причине.
Но я хочу сделать это с select_related
тем не мение
Хорошо хорошо. Это то, о чем просили, так что давайте посмотрим, что можно сделать.
Есть несколько способов сделать это, каждый из которых имеет свои плюсы и минусы, и ни один из них не работает без какой-либо ручной "сшивки" в конце. Я предполагаю, что вы не используете встроенный ViewSet или универсальные представления, предоставляемые DRF, но если это так, сшивание должно произойти в 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)
Я намеренно использую id
из 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()
Это даст вам все ItemGroup
объекты, которые были запрошены и хранятся как list
в item_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
объекты должны быть сохранены.
С помощью 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"
]
}]