13

mam następujące modele:Optymalizacja zapytań do bazy danych w Django REST ramach

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

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

a te modele są używane w poniższym widoku i serializatora:

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 

Więc w zasadzie dla każdego użytkownika zwrócone przez widok GetAllUsers, chcę wydrukować, czy użytkownik jest przyjacielem z requesterem (faktycznie powinienem sprawdzić zarówno od_, jak i do znajomego, ale nie ma to znaczenia dla pytania w punkcie)

co widzę jest to, że dla użytkowników n, w bazie danych, jest 1 zapytanie do uzyskania wszystkich użytkowników n, a następnie 1xn zapytań w serializer na get_is_friend_already

Czy istnieje sposób aby tego uniknąć w drodze reszta-ramowej ? Może coś jak przekazanie zapytania dołączonego do serializera z odpowiednimi wierszami Friendship?

Odpowiedz

19

Django REST Framework nie może automatycznie optymalizować zapytań dla ciebie, tak jak sam Django nie. Są miejsca, na które możesz spojrzeć po wskazówki, including the Django documentation. To has been mentioned, że Django REST Framework powinien automatycznie, choć wiąże się to z pewnymi wyzwaniami.

To pytanie jest bardzo specyficzne dla twojego przypadku, w którym używasz niestandardowego SerializerMethodField, który wysyła żądanie dla każdego zwracanego obiektu. Ponieważ tworzysz nowe żądanie (używając menedżera Friends.objects), bardzo trudno jest zoptymalizować zapytanie.

Możesz jednak sprawić, aby problem był lepszy, nie tworząc nowego zestawu zapytań, a zamiast tego licząc znajomych z innych miejsc. Będzie to wymagało utworzenia relacji wstecznej w modelu Friendship, najprawdopodobniej za pomocą parametru related_name na polu, aby można było wstępnie pobrać wszystkie obiekty Friendship. Jest to jednak przydatne tylko wtedy, gdy potrzebujesz pełnych obiektów, a nie tylko liczby obiektów.

Spowodowałoby myślą i serializatora podobny do następującego:

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 

Jeśli wystarczy zliczania obiektów (podobnie jak przy użyciu queryset.count() lub queryset.exists()), można dołączyć adnotacje wierszy w queryset z liczbą odwrotnych relacji. Można to zrobić za pomocą metody get_queryset, dodając na końcu .annotate(friends_count=Count("friends")) (jeśli related_name był friends), który ustawi atrybut friends_count dla każdego obiektu na liczbę znajomych.

Spowodowałoby myślą i serializatora podobny do następującego:

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 

obu tych rozwiązań pozwoli uniknąć n + 1 kwerend, ale jeden wybrać zależy od tego, co chce osiągnąć.

+0

+1 Świetna odpowiedź Kevin! – Fiver

+0

Świetna odpowiedź Kevin. Wielkie dzięki. Jedyny mały ammend jest to, że zamiast dla przyjaciela w obj.friends, musiałem zadzwonić: dla przyjaciela w obj.friends.all() .. odpowiedni wątek jest tutaj: http://stackoverflow.com/questions/6314841/ typeerror-relatedmanager-object-is-not-iterable – dowjones123

+0

Pierwsze podejście z "prefetch_related" byłoby uciążliwe, gdyby użytkownik miał tysiące przyjaciół. W takim przypadku lepiej byłoby po prostu n zapytań dla każdego użytkownika – xleon

7

Opisane N + 1 problemem jest numerem jeden problem podczas REST Django Framework optymalizacji wydajności, więc z różnymi opiniami, wymaga bardziej solidne podejście następnie skierować prefetch_related() lub select_related() w get_queryset() widzenia metody.

Na podstawie zebranych informacji, oto solidne rozwiązanie, które eliminuje N + 1 (używając kodu OP jako przykładu). Opiera się na dekoratorach i nieco mniej sprzężonych dla większych aplikacji.

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 

Tutaj używamy statycznej metody klasy zbudować specyficzną queryset.

Dekorator:

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 

Funkcja ta zmienia zwrócone queryset aby zwrcania związane rekordy dla modelu określona w setup_eager_loading metody serializacji.

Widok:

class GetAllUsers(generics.ListAPIView): 
    serializer_class = GetAllUsersSerializer 

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

Ten wzór może wyglądać przesadą, ale z pewnością bardziej suchy i ma przewagę nad bezpośrednim modyfikacji queryset wewnątrz poglądów, gdyż umożliwia większą kontrolę nad jednostkami powiązanymi oraz eliminuje niepotrzebne zagnieżdżania powiązane obiekty.

0

Możesz podzielić widok na dwie kwerendy. Najpierw pobierz listę użytkowników (bez pola is_friend_already). To wymaga tylko jednego zapytania.
Po drugie, zdobądź listę znajomych request.user.
Po trzecie, zmodyfikuj wyniki w zależności od tego, czy użytkownik znajduje się na liście znajomych 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)