python - Optimización de consultas de bases de datos en el marco Django REST
django-rest-framework django-queryset (4)
Django REST Framework no puede optimizar automáticamente las consultas por usted, de la misma manera que Django no lo hará. Hay lugares en los que puede buscar consejos, incluida la documentación de Django . Se ha mencionado que Django REST Framework debería automáticamente, aunque hay algunos desafíos asociados con eso.
Esta pregunta es muy específica para su caso, donde está utilizando un
SerializerMethodField
personalizado que realiza una solicitud para cada objeto que se devuelve.
Debido a que está realizando una nueva solicitud (utilizando el administrador
Friends.objects
), es muy difícil optimizar la consulta.
Sin embargo, puede mejorar el problema al no crear un nuevo conjunto de consultas y, en su lugar, hacer que el amigo cuente desde otros lugares.
Esto requerirá que se cree una relación hacia atrás en el modelo de
Friendship
, muy probablemente a través del parámetro
related_name
en el campo, para que pueda
related_name
todos los objetos de
Friendship
.
Pero esto solo es útil si necesita los objetos completos, y no solo un recuento de los objetos.
Esto daría como resultado una vista y un serializador similares a los siguientes:
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
Si solo necesita un recuento de los objetos (similar a usar
queryset.count()
o
queryset.exists()
), puede incluir anotar las filas en el conjunto de consultas con los recuentos de relaciones inversas.
Esto se haría en su método
get_queryset
, agregando
.annotate(friends_count=Count("friends"))
hasta el final (si el
related_name
eran
friends
), lo que establecerá el atributo
friends_count
en cada objeto a la cantidad de amigos.
Esto daría como resultado una vista y un serializador similares a los siguientes:
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
Ambas soluciones evitarán consultas N + 1, pero la que elija dependerá de lo que esté tratando de lograr.
Tengo los siguientes modelos:
class User(models.Model):
name = models.Charfield()
email = models.EmailField()
class Friendship(models.Model):
from_friend = models.ForeignKey(User)
to_friend = models.ForeignKey(User)
Y esos modelos se utilizan en la siguiente vista y serializador:
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
Básicamente, para cada usuario devuelto por la vista
GetAllUsers
, quiero imprimir si el usuario es amigo del solicitante (en realidad, debería verificar tanto from_ como to_friend, pero no importa la pregunta en cuestión)
Lo que veo es que para N usuarios en la base de datos, hay 1 consulta para obtener todos los N usuarios, y luego consultas 1xN en el serializador
get_is_friend_already
¿Hay alguna manera de evitar esto en la forma del marco de descanso?
¿Tal vez algo como pasar una consulta incluida
select_related
al serializador que tiene las filas relevantes de
Friendship
?
El problema descrito de
N + 1
es un problema número uno durante la optimización del rendimiento de
Django REST Framework
, por lo que, desde diversas opiniones, requiere un enfoque más sólido que
prefetch_related()
o
select_related()
en el método de vista
get_queryset()
.
Según la información recopilada, aquí hay una solución sólida que elimina N + 1 (usando el código de OP como ejemplo). Se basa en decoradores y está un poco menos acoplado para aplicaciones más grandes.
Serializador:
class GetAllUsersSerializer(serializers.ModelSerializer):
friends = FriendSerializer(read_only=True, many=True)
# ...
@staticmethod
def setup_eager_loading(queryset):
queryset = queryset.prefetch_related("friends")
return queryset
Aquí usamos el método de clase estática para construir el conjunto de consultas específico.
Decorador:
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
Esta función modifica el conjunto de consultas devuelto para obtener registros relacionados para un modelo como se define en el método de serializador
setup_eager_loading
.
Ver:
class GetAllUsers(generics.ListAPIView):
serializer_class = GetAllUsersSerializer
@setup_eager_loading
def get_queryset(self):
return User.objects.all()
Este patrón puede parecer una exageración, pero ciertamente es más SECO y tiene ventaja sobre la modificación directa del conjunto de consultas dentro de las vistas, ya que permite un mayor control sobre las entidades relacionadas y elimina el anidamiento innecesario de objetos relacionados.
Puede dividir la vista en dos consultas.
Primero, solo obtenga la lista de usuarios (sin el campo
is_friend_already
).
Esto solo requiere una consulta.
En segundo lugar, obtenga la lista de amigos de request.user.
Tercero, modifique los resultados dependiendo de si el usuario está en la lista de amigos de request.user''s.
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)
Usando esta metaclase DRF, optimice ModelViewSet MetaClass
from django.utils import six
@six.add_metaclass(OptimizeRelatedModelViewSetMetaclass)
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer