13

Ich habe folgende Modelle:Optimierung der Datenbankabfragen in Django REST Rahmen

class User(models.Model): 
    name = models.Charfield() 
    email = models.EmailField() 

class Friendship(models.Model): 
    from_friend = models.ForeignKey(User) 
    to_friend = models.ForeignKey(User) 

Und diese Modelle sind in der folgenden Ansicht und Serializer verwendet:

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 

Also im Grunde für jeden Benutzer zurückgegeben von der GetAllUsers Ansicht, ich möchte ausdrucken, ob der Benutzer ein Freund mit dem Anforderer ist (eigentlich sollte ich sowohl from_ und to_friend überprüfen, ist aber nicht wichtig für die Frage in Punkt)

Was ich sehe, ist, dass für N Benutzer in der Datenbank gibt es 1 Abfrage alle N Benutzer für immer, und dann 1 × N-Abfragen in dem Serializer des get_is_friend_already

Gibt es eine Möglichkeit, dies im Rest-Rahmen Art und Weise zu vermeiden ? Vielleicht etwas wie eine select_related enthalten Abfrage an den Serializer übergeben, der die relevanten Friendship Reihen hat?

Antwort

19

Django REST Framework kann Abfragen nicht automatisch für Sie optimieren, genau wie Django selbst nicht. Es gibt Orte, an denen Sie nach Tipps suchen können, including the Django documentation. Es sollte has been mentioned, dass Django REST Framework sollte automatisch, obwohl einige Herausforderungen damit verbunden sind.

Diese Frage ist sehr spezifisch für Ihren Fall, wo Sie eine benutzerdefinierte SerializerMethodField verwenden, die eine Anfrage für jedes Objekt, das zurückgegeben wird. Da Sie eine neue Anfrage (mit dem Manager Friends.objects) erstellen, ist es sehr schwierig, die Abfrage zu optimieren.

Sie können das Problem jedoch verbessern, indem Sie kein neues Abfrage-Set erstellen und stattdessen die Anzahl der Freunde von anderen Orten abrufen. Dies erfordert eine Rückwärtsbeziehung, die auf dem Modell Friendship erstellt wird, höchstwahrscheinlich durch den Parameter related_name auf dem Feld, so dass Sie alle Friendship Objekte vorab abrufen können. Dies ist jedoch nur nützlich, wenn Sie die vollständigen Objekte und nicht nur die Anzahl der Objekte benötigen.

Dies in einer Ansicht und Serializer ähnlich den folgenden führen würde:

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 

Wenn Sie eine Anzahl der Objekte (ähnlich wie mit queryset.count() oder queryset.exists()), können Sie schließen die Reihen in den annotieren müssen nur queryset mit den Zählungen der umgekehrten Beziehungen. Dies würde in Ihrer get_queryset Methode geschehen, indem Sie .annotate(friends_count=Count("friends")) an das Ende hinzufügen (wenn die related_namefriends war), die das friends_count Attribut für jedes Objekt auf die Anzahl der Freunde festlegen.

Dies würde in einer Ansicht und Serializer ähnlich der folgenden:

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 

Beide dieser Lösungen werden N + 1-Abfragen vermeiden, aber die, die Sie hängt holen, was Sie erreichen wollen.

+0

+1 Große Antwort Kevin! – Fiver

+0

Große Antwort Kevin. Danke vielmals. Die einzige kleine Verbesserung ist, dass anstelle von Freund in obj.friends, ich musste anrufen: für einen Freund in obj.friends.all() .. der entsprechende Thread ist hier: http://StackOverflow.com/Questions/6314841/ typeerror-relatedmanager-object-is-not-iterable – dowjones123

+0

Der erste Ansatz mit "prefetch_related" wäre umständlich, wenn der Benutzer Tausende von Freunden hätte. In diesem Fall wäre es besser, nur n Abfragen für jeden Benutzer – xleon

7

Beschrieben N + 1 Problem ist ein Thema Nummer eins bei Django REST-Framework Performance-Optimierung, so von verschiedenen Meinungen, es solidere Ansatz erfordert dann prefetch_related() oder select_related() in get_queryset() Ansicht Verfahren lenken.

Basierend auf gesammelten Informationen, ist hier eine robuste Lösung, die N + 1 (mit OP-Code als Beispiel) eliminiert. Es basiert auf Dekoratoren und etwas weniger gekoppelt für größere Anwendungen.

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 

Hier verwenden wir statische Klassenmethode die spezifische queryset zu bauen.

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 

Diese Funktion queryset um verknüpften Datensätze zu holen für ein Modell in setup_eager_loading Serializer Verfahren wie definiert zurückgegeben modifiziert.

Ausblick:

class GetAllUsers(generics.ListAPIView): 
    serializer_class = GetAllUsersSerializer 

    @setup_eager_loading 
    def get_queryset(self): 
     return User.objects.all() 

Dieses Muster wie übertrieben aussehen, aber es ist sicherlich mehr trocken und hat den Vorteil gegenüber der direkten queryset Modifikation Innenansichten, da sie mehr Kontrolle über verbundene Unternehmen ermöglicht und vermeiden unnötige Verschachtelung verwandte Objekte.

0

Sie können die Ansicht in zwei Abfragen aufteilen.
Zuerst erhalten Sie nur die Benutzerliste (ohne Feld is_friend_already). Dies erfordert nur eine Abfrage.
Zweitens, erhalten Sie die Freundesliste von request.user.
Drittens ändern Sie die Ergebnisse abhängig davon, ob sich der Benutzer in der Freundesliste von request.user befindet.

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) 
0
from rest_framework import serializers 
from rest_framework.utils import model_meta 


class DeclarativeModelViewSetMetaclass(type): 
    """ 
    Metaclass to prefetch and select related objects of the queryset. 
    """ 
    @classmethod 
    def get_many_to_many_rel(cls, info, meta_fields): 
     many_to_many_fields = [] 
     for field_name, relation_info in info.relations.items(): 
      if relation_info.to_many and field_name in meta_fields: 
       many_to_many_fields.append(field_name) 
     return many_to_many_fields 

    @classmethod 
    def get_forward_rel(cls, info, meta_fields): 
     related_fields = [] 
     for field_name, relation_info in info.forward_relations.items(): 
      if field_name in meta_fields: 
       related_fields.append(field_name) 
     return related_fields 

    def __new__(cls, name, bases, attrs): 
     serializer_class = attrs.get('serializer_class', None) 
     many_to_many_fields = [] 
     related_fields = [] 

     for base in reversed(bases): 
      if hasattr(base, '_base_forward_rel'): 
       related_fields.extend(list(base._base_forward_rel)) 
     if serializer_class and issubclass(serializer_class, serializers.ModelSerializer): 
      if hasattr(serializer_class.Meta, 'model'): 
       info = model_meta.get_field_info(serializer_class.Meta.model) 
       meta_fields = tuple(serializer_class.Meta.fields) 
       many_to_many_fields.extend(cls.get_many_to_many_rel(info, meta_fields)) 
       related_fields.extend(cls.get_forward_rel(info, meta_fields)) 

     queryset = attrs.get('queryset', None) 
     if queryset: 
      if many_to_many_fields: 
       queryset = queryset.prefetch_related(*many_to_many_fields) 
      if related_fields: 
       queryset = queryset.select_related(*related_fields) 
      attrs['queryset'] = queryset.all() 
     return super(DeclarativeModelViewSetMetaclass, cls).__new__(cls, name, bases, attrs)