django rest framework - обратная сериализация, чтобы избежать prefetch_related - PullRequest
0 голосов
/ 20 декабря 2018

У меня есть две модели, 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 ]

0 голосов
/ 29 декабря 2018

Давайте начнем с основ

Сериализатор может работать только с данными, которые ему даны

Так что это означает, что для получения сериализатора, который может сериализовать список ItemGroup и Item объекты во вложенном представлении, этот список должен быть задан в первую очередь.Вы уже достигли этого, используя запрос к модели ItemGroup, который вызывает prefetch_related, чтобы получить связанные Item объекты.Вы также определили, что prefetch_related запускает второй запрос для получения этих связанных объектов, и это не является удовлетворительным.

prefetch_related используется для получения нескольких связанных объектов

Что делаетэто значит точно?Когда вы запрашиваете один объект, например, один ItemGroup, вы используете prefetch_related, чтобы получить отношение, содержащее несколько связанных объектов, например обратный внешний ключ (один-ко-многим) или отношение многие-ко-многимэто было определено.Django намеренно использует второй запрос для получения этих объектов по нескольким причинам

  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 объектов должен быть сохранен.
0 голосов
/ 24 декабря 2018

Используя 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"
    ]
}]
...