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 чтобы получить отношение, содержащее несколько связанных объектов, например обратный внешний ключ (один-ко-многим) или отношение многие-ко-многим, которое было определено. Джанго намеренно использует второй запрос, чтобы получить эти объекты по нескольким причинам.

  1. Соединение, которое потребуется в select_related часто не работает, когда вы заставляете его соединяться со второй таблицей. Это связано с тем, что для обеспечения ItemGroup объекты, которые не содержат Item пропущены.
  2. Запрос, используемый prefetch_related является IN в индексированном поле первичного ключа, которое является одним из наиболее производительных запросов.
  3. Запрос запрашивает только идентификаторы Item известные ему объекты существуют, поэтому он может эффективно обрабатывать дубликаты (в случае отношений "многие ко многим") без необходимости выполнять дополнительный подзапрос.

Все это способ сказать: prefetch_related делает именно то, что должно делать, и делает это по определенной причине.

Но я хочу сделать это с select_related тем не мение

Хорошо хорошо. Это то, о чем просили, так что давайте посмотрим, что можно сделать.

Есть несколько способов сделать это, каждый из которых имеет свои плюсы и минусы, и ни один из них не работает без какой-либо ручной "сшивки" в конце. Я предполагаю, что вы не используете встроенный ViewSet или универсальные представления, предоставляемые DRF, но если это так, сшивание должно произойти в filter_queryset метод, позволяющий работать встроенной фильтрации. О, и это, вероятно, нарушает нумерацию страниц или делает его почти бесполезным.

Сохранение оригинальных фильтров

Оригинальный набор фильтров применяется к ItemGroup объект. И поскольку это используется в API, они, вероятно, являются динамическими, и вы не хотите их потерять. Итак, вам нужно применить фильтры одним из двух способов:

  1. Создайте фильтры, а затем добавьте к ним префикс

    Так что вы бы сгенерировали свой foo=bar фильтры, а затем префикс их перед передачей filter() так было бы related__foo=bar, Это может повлиять на производительность, так как теперь вы фильтруете отношения.

  2. Создайте исходный подзапрос и передайте его 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_relatedqueryset, Он переопределит функцию списка в представлении и преобразует из одного сериализатора данные в другие, что даст вам желаемое представление. Он будет использовать только один запрос, и результаты анализа будут в 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"
    ]
}]
Другие вопросы по тегам