在Django REST框架中优化数据库查询

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个用户,然后在序列化器的get_is_friend_already中进行1xN次查询。

是否有一种避免这种情况的rest-framework方式?也许像传递一个包含相关Friendship行的select_related查询给序列化器这样的东西?

4个回答

34

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_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查询,但你选择哪个取决于你想要实现什么目标。


很好的回答,Kevin。非常感谢。唯一需要修改的是,我需要调用 obj.friends.all() 而不是 for friend in obj.friends,对应的线程在这里:https://dev59.com/um015IYBdhLWcg3w6QLA。 - dowjones123
如果用户有数千个好友,使用“prefetch_related”第一种方法会很麻烦。在这种情况下,最好为每个用户进行n次查询。 - xleon
Kevin,我在视图中使用了prefetch_related方法,但是当我在序列化器内部调用对象的.all()方法时,根据我的SQL日志,它仍然会向数据库发出调用。 - Dominooch

17

所描述的N+1问题是在Django REST Framework性能优化期间的头号问题,因此根据各种意见,它需要比直接在get_queryset()视图方法中使用prefetch_related()select_related()更坚实的方法。

基于收集到的信息,这里提供了一个强大的解决方案来消除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()

这种模式可能看起来有点过度设计,但是它比直接在视图中修改查询集更符合DRY原则,并具有优势,因为它允许更多地控制相关实体并消除相关对象的不必要嵌套。


这个方法对POST结果也适用吗?我已经成功地将setup_eager_loading应用于GET请求,但是当客户端进行POST请求并且返回的实例作为POST响应时,似乎没有应用任何prefetch_related子句。 - phoenix

1

使用这个元类 DRF优化ModelViewSet元类

from django.utils import six

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

在django-cookiter drf 3.13.1中,它会抛出一个TypeError:metaclass conflict异常。 - nsbm

0

你可以将视图分为两个查询。
首先,仅获取用户列表(不包括is_friend_already字段)。这只需要一个查询。
其次,获取请求用户的朋友列表。
第三,根据用户是否在请求用户的朋友列表中修改结果。

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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接