Оптимизация запросов к базе данных в структуре Django REST
У меня есть следующие модели:
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). Он основан на декораторах и немного меньше связан для больших приложений.
Serializer:
class GetAllUsersSerializer(serializers.ModelSerializer):
friends = FriendSerializer(read_only=True, many=True)
# ...
@staticmethod
def setup_eager_loading(queryset):
queryset = queryset.prefetch_related("friends")
return queryset
Здесь мы используем метод static class для создания конкретного набора запросов.
Decorator:
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
serializer.
Вид:
class GetAllUsers(generics.ListAPIView):
serializer_class = GetAllUsersSerializer
@setup_eager_loading
def get_queryset(self):
return User.objects.all()
Этот шаблон может выглядеть как излишний, но он, безусловно, более DRY и имеет преимущество перед прямой модификацией запросов внутри представлений, поскольку он позволяет больше контролировать связанные сущности и устраняет ненужную вложенность связанных объектов.
Ответ 3
Вы можете разделить представление на два запроса.
Во-первых, получите список Users (без 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)
Ответ 4
Использование этого метакласса DRF оптимизирует ModelViewSet MetaClass
from django.utils import six
@six.add_metaclass(OptimizeRelatedModelViewSetMetaclass)
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer