python django-rest-framework django-queryset django-orm django-select-related

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