Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

How to avoid recalculation when sorting by a model's property in Django Rest Framework?

I have a simple social media app, with User, Post, and PostVote models.

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()

In my views.py, I attempt to calculate the ‘top’ posts, sorted by score. Here’s the view code.

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()

In serializers, I also once again recalculate and return the score, as I’d like to pass it to my front-end.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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",
        )

I know I’m re-calculating the score way too many times, as it’s taking 3-4 seconds to return just 50 fake posts / 1250 fake PostVotes, but how do I actually make this more efficient? I’ve tried prefetch_related, and I can’t figure out how I’d use it if I’m sorting with a property. I’m really lost and would appreciate any help.

>Solution :

You might find some immediate improvement with using a Django aggregation, rather than a pythonic option that loops through all the objects manually. This is what your score function would become:

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

You can further improve this by caching the result or storing it on the model.

Edit – changed Count to Sum

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading