いつも動画・ブログでお世話になっております。 <br />
DRFで多対多の参照先がさらにForeignKeyのフィールドを読み込む場合<br />
(下記の例だとBlog ←→ sponsor → user, price)にviewsでprefetchを指定してもN+1回読み込まれてしまいます。 <br />
※該当する箇所だけ簡略化して書いています。
# models.py
class User(TimeStampedModel):
name = models.CharField()
class Price(TimeStampedModel):
price = models.PositiveIntegerField()
## 記事を応援してくれた人という設定
class Sponsor(TimeStampedModel):
user = models.ForeignKey(User)
price = models.ForeignKey(Price)
class Blog(TimeStampedModel):
title = models.CharField()
...
sponsor = models.ManyToManyField(Sponsor, blank=True)
## 記事のいいね機能
class Likes(TimeStampedModel):
blog = models.ForeignKey(Blog)
user = models.ForeignKey(User)
# serializer.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = '__all__'
class PriceSerializer(serializers.ModelSerializer):
class Meta:
model = Price
fields = '__all__'
class SponsorSerializer(serializers.ModelSerializer):
user = UserSerializer()
price = PriceSerializer()
class Meta:
model = Sponsor
fields = '__all__'
class BlogSerializer(serializers.ModelSerializer):
sponsor = SponsorSerializer(many=True)
likes = serializers.SerializerMethodField()
def get_likes(self, obj):
likes = LikeData.objects.filter(blog=obj).count()
user = self.context['request'].user
is_liked = LikeData.objects.filter(user=user, blog=obj).exists()
return {'likes': likes, 'is_liked': is_liked}
class Meta:
model = Blog
fields = '__all__'
# views.py
class BlogViewSet(viewsets.ModelViewSet):
queryset = Blog.objects.all()
serializer_classes = BlogSerializer
1 . querysetに対してPrefetch
class BlogViewSet(viewsets.ModelViewSet):
queryset = Blog.objects.all().prefetch_related(Prefetch('sponsor', Sponsor.objects.select_related('user', 'price')))
としましたが、UserとPriceが何回も重複して呼ばれてしまいました。
2 . django-auto-prefetchingの導入
SerializerMethodFieldがあると自動で認識してくれず、少し複雑になり、エラーが起こったため諦めました
実際のものとは少し変えて書いてあるので、テーブル構造を変えて対応するのではなく、クエリ側での改善を試みたいです。
Prefetchの理解に誤りがあるのでしょうか?
それともこのケースだと生SQLで対応しないと無理なのでしょうか。
知見をいただきたいです!よろしくお願いいたします。
ご回答ありがとうございます!
related_nameを設定していない場合は、_setが自動的につくので、下記のようにしてみてはいかがでしょうか。
不勉強で知らなかったです。
ただ質問の際には省略してしまいましたが、実際のモデルだとrelated_nameつけていました・・
## PriceとUserはrelated_nameなし
class Sponsor(TimeStampedModel):
user = models.ForeignKey(User, related_name='user', on_delete=models.CASCADE)
price = models.ForeignKey(Price, related_name='price', on_delete=models.CASCADE)
class Blog(TimeStampedModel):
title = models.CharField()
...
sponsor = models.ManyToManyField(Sponsor, related_name='sponsor', blank=True)
またrelated_nameついている状態で_setを付きのものも試しましたが効果でず(Invalid Parameter)でした。
ListAPIViewはpermission_classesをAllowAnyにして、誰でも見れる状態にして、
開発段階で権限周りをすべてAllowAnyにしていましたが、たしかにactionによって分けるべきですね!
分けて実装し直してみます。ご回答がありがとうございました!!
ベストアンサーは質問が2転してしまったため、最初の質問タイトルに最も合致してるものを選ばさせていただきました。
私であればModelViewSetとListAPIViewクラスを分けて実装します。
ブログを想定している場合は、
ListAPIViewはpermission_classes
をAllowAny
にして、誰でも見れる状態にして、
ModelViewSetは、新規登録や編集、削除なので、トークンが必要とする実装をよくします。
ご回答ありがとうございます!!
先生のコメントに返信ができなかったのでこちらに失礼します。
sponsorのrelated_nameを外した状態で_setをつけても変わらないでしょうか?
こちらも試してみましたが結果変わらずでした。
ただ、色々と試したところどうやら原因はここではないような気がしてきました。
度々後出しですみませんが、実は上記のviews内でgetとcreateでserializerを変える処理をしていました。
getの際はfieldを展開して表示させ、createの際はidだけでの登録を意図していたためです。
# serializer.py
## Create用
class BlogSerializer(serializers.ModelSerializer):
class Meta:
model = Blog
fields = '__all__'
## list, retrieve用
class BlogRefSerializer(serializers.ModelSerializer):
sponsor = SponsorSerializer(many=True)
likes = serializers.SerializerMethodField()
class Meta:
model = Blog
fields = '__all__'
# views.py
class BlogViewSet(viewsets.ModelViewSet):
queryset = Blog.objects.all()
serializer_classes = {
"create": BlogSerializer
}
default_serializer_class = BlogRefSerializer
def get_serializer_class(self):
serializer = self.serializer_classes.get(self.action, self.default_serializer_class)
print(serializer)
return serializer
django-debug-toolbarでブラウザからアクセスするDRFのページ(urls.pyに登録したパスのもの)を確認していたのですが、どうやらこのページはcreate用のserializerも呼ばるようで、Ref1回, BlogSerializer2回と合計3回も処理が走っているようでした。
prefetchを適用したquerysetはすでにBlogRefSerializerを最適化してくれていたのですが、BlogSerializerは最適化されおらず、裏でBlogSerializerを呼んでいたため見かけのSQLが重くなっているようでした。
別の質問となってしまいすいません。なるべくurlを増やしたくなかったのでこの形式にしていたのですが、generic.ListAPIViewなどでクラスを分けたほうが良いのでしょうか。
それともserializerが複数回呼ばれるのはdrfのページに限ったものなのでviewsはこのままn+1の探し方を変えるだけで問題ないでしょうか。
sponsorのrelated_nameを外した状態で_setをつけても変わらないでしょうか?
related_nameを設定していない場合は、_set
が自動的につくので、下記のようにしてみてはいかがでしょうか。
queryset = Blog.objects.all().prefetch_related(Prefetch('sponsor_set', queryset=Sponsor.objects.select_related('user', 'price')))