Оптимизация запросов к базе данных в Django REST framework

У меня есть следующие модели:

class User(models.Model):
    name = models.Charfield()
    email = models.EmailField()

class Friendship(models.Model):
    from_friend = models.ForeignKey(User)
    to_friend = models.ForeignKey(User)

И эти модели используются в следующем представлении и сериализаторе:

class GetAllUsers(generics.ListAPIView):
    authentication_classes = (SessionAuthentication, TokenAuthentication)
    permission_classes = (permissions.IsAuthenticated,)
    serializer_class = GetAllUsersSerializer
    model = User

    def get_queryset(self):
        return User.objects.all()

class GetAllUsersSerializer(serializers.ModelSerializer):

    is_friend_already = serializers.SerializerMethodField('get_is_friend_already')

    class Meta:
        model = User
        fields = ('id', 'name', 'email', 'is_friend_already',)

    def get_is_friend_already(self, obj):
        request = self.context.get('request', None)

        if request.user != obj and Friendship.objects.filter(from_friend = user):
            return True
        else:
            return False

Итак, в основном, для каждого пользователя, возвращенного представлением GetAllUsers, я хочу распечатать, является ли пользователь другом запрашивающего (на самом деле я должен проверить и from_, и to_friend, но это не имеет значения для рассматриваемого вопроса)

Я вижу, что для N пользователей в базе данных есть 1 запрос для получения всех N пользователей, а затем 1xN запросов в get_is_friend_already сериализатора

Есть ли способ избежать этого с помощью остальной части фреймворка? Может быть, что-то вроде передачи select_related включенного запроса в сериализатор с соответствующими Friendship строками?


person dowjones123    schedule 27.10.2014    source источник


Ответы (4)


Django REST Framework не может автоматически оптимизировать запросы для вас так же, как сам Django. Есть места, где вы можете найти советы, включая документацию Django . упоминалось, что Django REST Framework должен автоматически проблемы, связанные с этим.

Этот вопрос очень специфичен для вашего случая, когда вы используете пользовательский SerializerMethodField, который запрашивает каждый возвращаемый объект. Поскольку вы делаете новый запрос (используя Friends.objects менеджер), очень сложно оптимизировать запрос.

Однако вы можете решить проблему, не создавая новый набор запросов, а вместо этого получая счетчик друзей из других мест. Это потребует создания обратной связи в модели Friendship, скорее всего, с помощью параметра related_name в поле, чтобы вы могли предварительно выбрать все объекты Friendship. Но это полезно только в том случае, если вам нужны полные объекты, а не только их количество.

Это приведет к представлению и сериализатору, подобным следующему:

class Friendship(models.Model):
    from_friend = models.ForeignKey(User, related_name="friends")
    to_friend = models.ForeignKey(User)

class GetAllUsers(generics.ListAPIView):
    ...

    def get_queryset(self):
        return User.objects.all().prefetch_related("friends")

class GetAllUsersSerializer(serializers.ModelSerializer):
    ...

    def get_is_friend_already(self, obj):
        request = self.context.get('request', None)

        friends = set(friend.from_friend_id for friend in obj.friends)

        if request.user != obj and request.user.id in friends:
            return True
        else:
            return False

Если вам просто нужно подсчитать количество объектов (аналогично использованию queryset.count() или queryset.exists()), вы можете включить аннотирование строк в наборе запросов с помощью количества обратных отношений. Это можно сделать в вашем get_queryset методе, добавив .annotate(friends_count=Count("friends")) в конец (если related_name был friends), который установит атрибут friends_count для каждого объекта на количество друзей.

Это приведет к представлению и сериализатору, подобным следующему:

class Friendship(models.Model):
    from_friend = models.ForeignKey(User, related_name="friends")
    to_friend = models.ForeignKey(User)

class GetAllUsers(generics.ListAPIView):
    ...

    def get_queryset(self):
        from django.db.models import Count

        return User.objects.all().annotate(friends_count=Count("friends"))

class GetAllUsersSerializer(serializers.ModelSerializer):
    ...

    def get_is_friend_already(self, obj):
        request = self.context.get('request', None)

        if request.user != obj and obj.friends_count > 0:
            return True
        else:
            return False

Оба этих решения позволят избежать запросов N + 1, но выбор зависит от того, чего вы пытаетесь достичь.

person Kevin Brown    schedule 28.10.2014
comment
Отличный ответ, Кевин. Большое спасибо. Единственное небольшое изменение в том, что вместо друга в obj.friends мне нужно было позвонить: другу в obj.friends.all () .. соответствующий поток находится здесь: stackoverflow.com/questions/6314841/ - person dowjones123; 03.11.2014
comment
Первый подход с prefetch_related был бы громоздким, если бы у пользователя были тысячи друзей. В этом случае было бы лучше просто сделать n запросов для каждого пользователя. - person xleon; 17.07.2015
comment
Кевин, я использую метод prefetch_related для своих представлений, но когда я вызываю .all() для объекта внутри сериализатора, он по-прежнему обращается к БД. на основе моих журналов sql. - person Dominooch; 05.02.2016

Описанная проблема N + 1 является проблемой номер один при оптимизации производительности Django REST Framework, поэтому, по разным оценкам, она требует более основательного подхода, чем прямой prefetch_related() или select_related() в get_queryset() метод просмотра. .

Основываясь на собранной информации, вот надежное решение, которое устраняет N + 1 (на примере кода OP). Он основан на декораторах и немного менее связан с более крупными приложениями.

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

class GetAllUsersSerializer(serializers.ModelSerializer):
    friends = FriendSerializer(read_only=True, many=True)

    # ...

    @staticmethod
    def setup_eager_loading(queryset):
        queryset = queryset.prefetch_related("friends")

        return queryset

Здесь мы используем метод статического класса для создания определенного набора запросов.

Декоратор:

def setup_eager_loading(get_queryset):
    def decorator(self):
        queryset = get_queryset(self)
        queryset = self.get_serializer_class().setup_eager_loading(queryset)
        return queryset

    return decorator

Эта функция изменяет возвращаемый набор запросов, чтобы получить связанные записи для модели, как определено в методе сериализатора setup_eager_loading.

Просмотр:

class GetAllUsers(generics.ListAPIView):
    serializer_class = GetAllUsersSerializer

    @setup_eager_loading
    def get_queryset(self):
        return User.objects.all()

Этот шаблон может показаться излишним, но он, безусловно, более СУХИЙ и имеет преимущество перед прямым изменением набора запросов внутри представлений, поскольку он позволяет лучше контролировать связанные сущности и устраняет ненужное вложение связанных объектов.

person Damaged Organic    schedule 22.12.2016
comment
этот метод также работает для результатов POST? Я получил setup_eager_loading для работы с GET, но когда клиент отправляет POST и полученный экземпляр возвращается в качестве ответа на POST, кажется, что ни одно из предложений prefetch_related не применяется. - person phoenix; 30.03.2020

Используя этот метакласс, DRF оптимизирует метакласс ModelViewSet

from django.utils import six

@six.add_metaclass(OptimizeRelatedModelViewSetMetaclass)
class MyModelViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer
person jackotonye    schedule 26.02.2018
comment
Это отличное решение! Я использовал его, и он работал «из коробки». - person Pawel; 15.07.2020

Вы можете разделить представление на два запроса.
Во-первых, получить только список пользователей (без поля is_friend_already). Для этого нужен только один запрос.
Во-вторых, получите список друзей request.user.
В-третьих, измените результаты в зависимости от того, находится ли пользователь в списке друзей request.user.

class GetAllUsersSerializer(serializers.ModelSerializer):
    ... 


class UserListView(ListView):
    def get(self, request):
        friends = request.user.friends
        data = []
        for user in self.get_queryset():
            user_data = GetAllUsersSerializer(user).data
            if user in friends:
                user_data['is_friend_already'] = True
            else:
                user_data['is_friend_already'] = False
            data.append(user_data)
        return Response(status=200, data=data)
person ramwin    schedule 06.04.2017