在 Django REST 框架中优化数据库查询
- 2025-04-10 09:46:00
- admin 原创
- 13
问题描述:
我有以下模型:
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 的代码为例)。它基于装饰器,对于较大的应用程序来说耦合度略低。
序列化器:
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,并且比在视图内部直接修改查询集更具优势,因为它允许对相关实体进行更好的控制,并消除了相关对象的不必要嵌套。
解决方案 3:
使用此元类DRF优化ModelViewSet MetaClass
from django.utils import six
@six.add_metaclass(OptimizeRelatedModelViewSetMetaclass)
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
解决方案 4:
您可以将视图拆分为两个查询。
首先,仅获取用户列表(不带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)
扫码咨询,免费领取项目管理大礼包!