23

我有以下型号:

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行的序列化程序?

4

4 回答 4

33

Django REST Framework 无法自动为您优化查询,就像 Django 本身不会那样。您可以在一些地方查看提示,包括 Django 文档。有人提到Django REST 框架应该是自动的,尽管有一些与之相关的挑战。

这个问题对您的情况非常具体,您使用的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()or queryset.exists()),您可以在查询集中使用反向关系的计数来注释行。这将在您的get_queryset方法中通过添加.annotate(friends_count=Count("friends"))到末尾(如果related_namefriends)来完成,这会将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 查询,但您选择的解决方案取决于您要实现的目标。

于 2014-10-28T00:05:22.910 回答
16

描述的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()

这种模式可能看起来有点矫枉过正,但它肯定更干燥,并且比直接在视图中修改查询集更有优势,因为它允许对相关实体进行更多控制并消除相关对象的不必要嵌套。

于 2016-12-22T16:15:27.987 回答
1

使用这个元类DRF 优化 ModelViewSet MetaClass

from django.utils import six

@six.add_metaclass(OptimizeRelatedModelViewSetMetaclass)
class MyModelViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer
于 2018-02-26T20:42:53.527 回答
0

您可以将视图拆分为两个查询。
首先,只获取用户列表(没有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)
于 2017-04-06T09:53:17.063 回答