Оптимизация запросов к базе данных в 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 view, я хочу распечатать, является ли пользователь другом запрашивающего (на самом деле я должен проверить как from_, так и to_friend, но не имеет значения для вопроса в точке)

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

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

4 ответов


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

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

вы можете сделать проблему лучше, хотя, не создавая новый queryset и вместо этого получать подсчет друзей из других мест. Для этого потребуется создать обратное отношение на Friendship модель, скорее всего, через related_name параметр на поле, так что вы можете prefetch все 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()), вы можете включить аннотирование строк в queryset с подсчетами обратных отношений. Это будет сделано в вашем get_queryset метод, добавив .annotate(friends_count=Count("friends")) до конца (если related_name был friends), которое на каждом объекте по количеству друзей.

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

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


описал N+1 проблема является проблемой номер один во время Django Rest Framework оптимизация производительности, поэтому из разных мнений, это требует более твердого подхода, чем direct 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

здесь мы используем метод статического класса для построения конкретного 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

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

вид:

class GetAllUsers(generics.ListAPIView):
    serializer_class = GetAllUsersSerializer

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

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


Вы можете разделить окно на два запроса.
Во-первых, только получить список пользователей (без


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

from django.utils import six

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