Django MPTT эффективно сериализует реляционные данные с помощью DRF

У меня есть модель категории, которая является моделью MPTT. Это m2m для группы, и мне нужно сериализовать дерево со связанными счетчиками, представьте, что мое дерево категорий таково:

Root (related to 1 group)
 - Branch (related to 2 groups) 
    - Leaf (related to 3 groups)
...

Таким образом, сериализованный вывод будет выглядеть так:

{ 
    id: 1, 
    name: 'root1', 
    full_name: 'root1',
    group_count: 6,
    children: [
    {
        id: 2,
        name: 'branch1',
        full_name: 'root1 - branch1',
        group_count: 5,
        children: [
        {
            id: 3,
            name: 'leaf1',
            full_name: 'root1 - branch1 - leaf1',
            group_count: 3,
            children: []
        }]
    }]
}

Это моя текущая супер неэффективная реализация:

Модель

class Category(MPTTModel):
    name = ...
    parent = ... (related_name='children')

    def get_full_name(self):
        names = self.get_ancestors(include_self=True).values('name')
        full_name = ' - '.join(map(lambda x: x['name'], names))
        return full_name

    def get_group_count(self):
        cats = self.get_descendants(include_self=True)
        return Group.objects.filter(categories__in=cats).count()

Просмотреть

class CategoryViewSet(ModelViewSet):
    def list(self, request):
        tree = cache_tree_children(Category.objects.filter(level=0))
        serializer = CategorySerializer(tree, many=True)
        return Response(serializer.data)

Сериализатор

class RecursiveField(serializers.Serializer):
    def to_native(self, value):
        return self.parent.to_native(value)


class CategorySerializer(serializers.ModelSerializer):
    children = RecursiveField(many=True, required=False)
    full_name = serializers.Field(source='get_full_name')
    group_count = serializers.Field(source='get_group_count')

    class Meta:
        model = Category
        fields = ('id', 'name', 'children', 'full_name', 'group_count')

Это работает, но также поражает БД безумным количеством запросов, также есть дополнительные отношения, а не только группа. Есть ли способ сделать это эффективным? Как я могу написать свой собственный сериализатор?


person WBC    schedule 22.11.2014    source источник


Ответы (2)


Вы определенно сталкиваетесь с проблемой запроса N+1, о которой я подробно рассказал в другом ответе на переполнение стека. Я бы порекомендовал прочитать об оптимизации запросов в Django, так как это очень распространенная проблема.

Теперь у Django MPTT также есть несколько проблем, которые вам нужно будет обойти, поскольку запросы N + 1. Оба метода self.get_ancestors и self.get_descendants создают новый набор запросов, что в вашем случае происходит для каждого объекта, который вы сериализуете. Возможно, вы захотите найти лучший способ избежать этого, я описал возможные улучшения ниже.

В вашем методе get_full_name вы вызываете self.get_ancestors для создания используемой цепочки. Учитывая, что у вас всегда есть родитель, когда вы генерируете вывод, вы можете извлечь выгоду из перемещения его в SerializerMethodField, который повторно использует родительский объект для генерации имени. Что-то вроде следующего может работать:

class RecursiveField(serializers.Serializer):

    def to_native(self, value):
        return CategorySerializer(value, context={"parent": self.parent.object, "parent_serializer": self.parent})

class CategorySerializer(serializers.ModelSerializer):
    children = RecursiveField(many=True, required=False)
    full_name = SerializerMethodField("get_full_name")
    group_count = serializers.Field(source='get_group_count')

    class Meta:
        model = Category
        fields = ('id', 'name', 'children', 'full_name', 'group_count')

    def get_full_name(self, obj):
        name = obj.name

        if "parent" in self.context:
            parent = self.context["parent"]

            parent_name = self.context["parent_serializer"].get_full_name(parent)

            name = "%s - %s" % (parent_name, name, )

        return name

Возможно, вам придется немного отредактировать этот код, но общая идея заключается в том, что вам не всегда нужно получать предков, потому что у вас уже будет цепочка предков.

Это не решает Group запросов, которые вы, возможно, не сможете оптимизировать, но, по крайней мере, должно уменьшить ваши запросы. Рекурсивные запросы невероятно сложно оптимизировать, и они обычно требуют тщательного планирования, чтобы выяснить, как лучше всего получить необходимые данные, не возвращаясь к ситуациям N+1.

person Kevin Brown    schedule 22.11.2014
comment
Спасибо за подробный метод сериализатора! Я надеялся, что у волшебников, которые создали MPTT, есть решение проблемы N+1 для подсчета :( - person WBC; 24.11.2014

Я нашел решение для подсчетов. Благодаря django-mpttфункции get_cached_trees вы можете сделать следующее:

from django.db.models import Count


class CategorySerializer(serializers.ModelSerializer):
    def get_group_count(self, obj, field=field):
        return obj.group_count

    class Meta:
        model = Category
        fields = [
            'name',
            'slug',
            'children',
            'group_count',
        ]

CategorySerializer._declared_fields['children'] = CategorySerializer(
    many=True,
    source='get_children',
)

class CategoryViewSet(ModelViewSet):
    serializer_class = CategorySerializer

    def get_queryset(self, queryset=None):
        queryset = Category.tree.annotate('group_count': Count('group')})
        queryset = queryset.get_cached_trees()
        return queryset

Где дерево mptts TreeManager, как используется в django-categories, для которого я написал немного более сложный код для этого PR: https://github.com/callowayproject/django-categories/pull/145/files

person Petr Dlouhý    schedule 08.05.2019