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

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

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

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

Ответ 1

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 запросов, но тот, который вы выбираете, зависит от того, чего вы пытаетесь достичь.

Ответ 2

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

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

Serializer:

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

    # ...

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

        return queryset

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

Decorator:

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 serializer.

Вид:

class GetAllUsers(generics.ListAPIView):
    serializer_class = GetAllUsersSerializer

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

Этот шаблон может выглядеть как излишний, но он, безусловно, более DRY и имеет преимущество перед прямой модификацией запросов внутри представлений, поскольку он позволяет больше контролировать связанные сущности и устраняет ненужную вложенность связанных объектов.

Ответ 3

Вы можете разделить представление на два запроса.
Во-первых, получите список Users (без 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)

Ответ 4

Использование этого метакласса DRF оптимизирует ModelViewSet MetaClass

from django.utils import six

@six.add_metaclass(OptimizeRelatedModelViewSetMetaclass)
class MyModelViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer