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.
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.
CodePudding user response:
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
CodePudding user response:
the minimum optimization you can do is:
from django.db.models import Sum
@property
def score(self):
PostVote.objects.filter(post=self).annotate(total=Sum('vote'))['total']
details you can find here: Django: aggregation