13

Ho i seguenti modelli:Ottimizzazione delle query di database in quadro 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) 

E quei modelli sono utilizzati nella seguente vista e serializzatore:

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 

Quindi, in pratica, per ogni utente restituito dalla vista GetAllUsers, voglio stampare se l'utente è un amico con il richiedente (in realtà dovrei controllare entrambi da_ e to_friend, ma non ha importanza per la domanda in questione)

Quello che vedo è che per N utenti nel database, v'è 1 query per ottenere tutti gli utenti N, e le query poi 1XN nel del serializzatore get_is_friend_already

C'è un modo per evitare questo nel modo di riposo-quadro ? Forse qualcosa come passare una query inclusa a select_related al serializzatore che ha le righe rilevanti Friendship?

risposta

19

Django REST Framework non è in grado di ottimizzare automaticamente le query per voi, nello stesso modo in cui Django non lo farà. Ci sono posti che puoi consultare per suggerimenti, including the Django documentation. È has been mentioned che Django REST Framework dovrebbe automaticamente, anche se ci sono alcune sfide associate a questo.

Questa domanda è molto specifica per il tuo caso, in cui si utilizza un SerializerMethodField personalizzato che effettua una richiesta per ciascun oggetto restituito. Poiché stai facendo una nuova richiesta (utilizzando il gestore Friends.objects), è molto difficile ottimizzare la query.

Tuttavia, è possibile migliorare il problema non creando un nuovo set di query e ottenendo il conteggio degli amici da altri luoghi. Ciò richiederà la creazione di una relazione all'indietro sul modello Friendship, molto probabilmente attraverso il parametro related_name sul campo, in modo da poter precaricare tutti gli oggetti Friendship. Ma questo è utile solo se hai bisogno degli oggetti completi e non solo del conteggio degli oggetti.

Ciò si tradurrebbe in una vista e serializzatore simile al seguente:

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 

Se avete solo bisogno di un conteggio degli oggetti (simile a quello di queryset.count() o queryset.exists()), è possibile includere annotare le righe della queryset con i conteggi delle relazioni inverse. Questo sarebbe stato fatto nel tuo metodo get_queryset, aggiungendo .annotate(friends_count=Count("friends")) alla fine (se lo related_name era friends), che imposterà l'attributo friends_count su ogni oggetto al numero di amici.

Ciò si tradurrebbe in una vista e serializzatore simile al seguente:

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 

Entrambe queste soluzioni eviterà N + 1 query, ma quello che si sceglie dipende da ciò che si sta cercando di ottenere.

+0

+1 Grande risposta di Kevin! – Fiver

+0

Ottima risposta Kevin. Molte grazie. L'unica piccola ammenda è che invece di un amico in obj.friends, avevo bisogno di chiamare: per amico in obj.friends.all() .. il thread corrispondente è qui: http://stackoverflow.com/questions/6314841/ typeerror-relatedmanager-object-is-iterable – dowjones123

+0

Il primo approccio con "prefetch_related" sarebbe ingombrante se l'utente avesse migliaia di amici. In tal caso sarebbe meglio fare solo n query per ogni utente. – xleon

7

Descritto N + 1 problema è un problema numero uno durante Django RIPOSO quadro ottimizzazione delle prestazioni, in modo da varie opinioni, richiede approccio più solido quindi indirizzare prefetch_related() o select_related() in get_queryset() metodo di visualizzazione.

Sulla base delle informazioni raccolte, ecco una soluzione robusta che elimina N + 1 (utilizzando il codice OP come esempio). È basato su decoratori e leggermente meno accoppiato per applicazioni più grandi.

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 

Qui usiamo il metodo classe statica per costruire il set di query specifica.

decoratore:

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 

Questa funzione modifica restituito queryset per recuperare i record correlati per un modello come definito setup_eager_loading metodo serializzatore.

Vista:

class GetAllUsers(generics.ListAPIView): 
    serializer_class = GetAllUsersSerializer 

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

Questo modello può sembrare eccessivo, ma è certamente più asciutto e presenta vantaggio rispetto modifica set di query direttamente all'interno di vista, in quanto consente un maggiore controllo sulla entità correlate ed elimina la nidificazione inutile di oggetti correlati.

0

È possibile dividere la vista in due query.
Per prima cosa, ottenere solo l'elenco Utenti (senza il campo is_friend_already). Questo richiede solo una query.
In secondo luogo, ottenere l'elenco di amici di request.user.
In terzo luogo, modificare i risultati a seconda se l'utente è nella lista degli amici di 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) 
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) 
Problemi correlati