次のモデルがあります。
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
に含まれるクエリを渡すようなものですか?
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_name
がfriends
の場合)、各オブジェクトの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クエリを回避しますが、どちらを選択するかは、達成しようとしていることに依存します。
説明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であり、ビュー内のクエリセットを直接変更するよりも優れています。関連するエンティティをより詳細に制御し、関連するオブジェクトの不要なネストを排除できるためです。
このメタクラスの使用 DRF optimize ModelViewSet MetaClass
from Django.utils import six
@six.add_metaclass(OptimizeRelatedModelViewSetMetaclass)
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
ビューを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)