web-dev-qa-db-ja.com

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)

そして、それらのモデルは次のビューとシリアライザーで使用されます:

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

したがって、基本的には、GetAllUsersビューによって返された各ユーザーについて、ユーザーがリクエスタと友達であるかどうかを出力したいと思います(実際には、from_とto_friendの両方を確認する必要がありますが、質問は問題ではありません)

私が見るのは、データベース内のN人のユーザーの場合、N人のすべてのユーザーを取得するクエリが1つあり、シリアライザのget_is_friend_alreadyに1xNクエリがあるということです。

残りのフレームワークの方法でこれを回避する方法はありますか?おそらく、関連するFriendship行を持つシリアライザにselect_relatedに含まれるクエリを渡すようなものですか?

19
dowjones123

Django RESTフレームワークは、Django自体とは異なり、クエリを自動的に最適化できません。ヒントを見ることができる場所があります Djangoのドキュメントを含む 。それは 言及されています Django RESTフレームワークは自動的に実行されますが、それに関連するいくつかの課題があります。

この質問は、返される各オブジェクトに対して要求を行うカスタムSerializerMethodFieldを使用している場合に非常に固有です。 (Friends.objectsマネージャーを使用して)新しいリクエストを作成しているため、クエリを最適化することは非常に困難です。

ただし、新しいクエリセットを作成せず、代わりに他の場所から友達の数を取得することで、問題を改善できます。これには、Friendshipモデルで、おそらくフィールドのrelated_nameパラメーターを使用して後方関係を作成する必要があるため、すべてのFriendshipオブジェクトをプリフェッチできます。ただし、これは、オブジェクトの数だけではなく、完全なオブジェクトが必要な場合にのみ役立ちます。

この結果、ビューとシリアライザは次のようになります。

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

オブジェクトの数だけが必要な場合(queryset.count()またはqueryset.exists()を使用する場合と同様)、逆関係の数でクエリセットの行に注釈を付けることができます。これは、get_querysetメソッドの最後に.annotate(friends_count=Count("friends"))を追加することによって行われ(related_namefriendsの場合)、各オブジェクトのfriends_count属性を友達の数に設定します。

この結果、ビューとシリアライザは次のようになります。

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

これらのソリューションはどちらもN + 1クエリを回避しますが、どちらを選択するかは、達成しようとしていることに依存します。

25
Kevin Brown

説明N + 1問題はDjango REST Frameworkパフォーマンスの最適化中の最大の問題なので、さまざまな意見から、より確実なアプローチが必要です次に、prefetch_related() viewメソッドでselect_related()またはget_queryset()を指示します。

収集された情報に基づいて、N + 1を排除する堅牢なソリューションを次に示します(例としてOPのコードを使用)。それはデコレーターに基づいており、大規模なアプリケーションの場合は少し結合されています。

シリアライザ:

class GetAllUsersSerializer(serializers.ModelSerializer):
    friends = FriendSerializer(read_only=True, many=True)

    # ...

    @staticmethod
    def setup_eager_loading(queryset):
        queryset = queryset.prefetch_related("friends")

        return queryset

ここでは、静的クラスメソッドを使用して、特定のクエリセットを作成します。

デコレーター:

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

この関数は、setup_eager_loadingシリアライザメソッドで定義されているモデルの関連レコードをフェッチするために、返されたクエリセットを変更します。

表示:

class GetAllUsers(generics.ListAPIView):
    serializer_class = GetAllUsersSerializer

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

このパターンは過剰に見えるかもしれませんが、確かにDRYであり、ビュー内のクエリセットを直接変更するよりも優れています。関連するエンティティをより詳細に制御し、関連するオブジェクトの不要なネストを排除できるためです。

14
Damaged Organic

このメタクラスの使用 DRF optimize ModelViewSet MetaClass

from Django.utils import six

@six.add_metaclass(OptimizeRelatedModelViewSetMetaclass)
class MyModelViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer
0
jackotonye

ビューを2つのクエリに分割できます。
最初に、ユーザーリストのみを取得します(is_friend_alreadyフィールドなし)。これには1つのクエリのみが必要です。
次に、request.userのフレンドリストを取得します。
3番目に、ユーザーが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
ramwin