Оптимизация запросов к базе данных в 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