Как избежать пересчета при сортировке по свойству модели в Django Rest Framework?

У меня есть простое приложение для социальных сетей, с моделями User, Post и PostVote.

class Post(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    
    ...

    @property
    def score(self):
        total = 0
        for vote in PostVote.objects.all():
            if vote.post == self:
                total += vote.vote
        return total
   
    def get_num_upvotes(self):
        return PostVote.objects.filter(post=self, vote=1).count()

    def get_num_downvotes(self):
        return PostVote.objects.filter(post=self, vote=-1).count()


class PostVote(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    post = models.ForeignKey(Post, on_delete=models.SET_NULL, null=True)
    vote = models.IntegerField()

В моем файле views.py я пытаюсь вычислить "лучшие" посты, отсортированные по количеству баллов. Вот код представления.

class ListCreatePostAPIView(ListCreateAPIView):
    serializer_class = PostSerializer
    permission_classes = (IsGoodUser | IsAdminUser,)

    def get_queryset(self):
        try:
            queryset = Post.objects.all().exclude(user=None)
            queryset = list(queryset)
            queryset.sort(key=operator.attrgetter("score"), reverse=True)
            return queryset
        except:
            return Post.objects.none()

В сериализаторах я также еще раз пересчитываю и возвращаю оценку, так как хочу передать ее в мой front-end.

class PostSerializer(serializers.ModelSerializer):
    score = serializers.SerializerMethodField()
    num_upvotes = serializers.SerializerMethodField()
    num_downvotes = serializers.SerializerMethodField()

    def get_score(self, obj):
        return obj.get_score()

    def get_num_upvotes(self, obj):
        return obj.get_num_upvotes()

    def get_num_downvotes(self, obj):
        return obj.get_num_downvotes()

    class Meta:
        model = Post
        fields = (
            "score",
            "num_upvotes",
            "num_downvotes",
        )

Я знаю, что пересчитываю результат слишком много раз, так как на возврат всего 50 фальшивых постов / 1250 фальшивых PostVotes уходит 3-4 секунды, но как сделать это более эффективным? Я пробовал prefetch_related, но не могу понять, как мне использовать его, если я сортирую с помощью свойства. Я действительно запутался и буду благодарен за любую помощь.

Вы можете обнаружить некоторое немедленное улучшение, используя агрегацию Django, а не питонический вариант, который перебирает все объекты вручную. Вот как будет выглядеть ваша функция оценки:

@property
def score(self):
    return self.postvote_set.aggregate(Sum('vote')).get('vote__sum')

Вы можете улучшить это, кэшируя результат или храня его в модели.

Редактировать - изменено Count на Sum

Вернуться на верх