Reputation: 12585
I have a Django Model:
from django.db import models
class Player(models.Model):
name = models.CharField(max_length=254, null=True, blank=True,)
score = models.IntegerField()
A = Player.create(name="A", score=99)
B = Player.create(name="B", score=66)
C = Player.create(name="C", score=66)
D = Player.create(name="D", score=55)
E = Player.create(name="E", score=44)
I want to maintain a ranking of all my players by score. If players have the same score, they will have the same rank. So in this case, the ranking would look like this:
Name Score Rank
A 99 1
B 66 2
C 66 2
D 55 4
E 44 5
I want to store this ranking in the Database so that I don't constantly have to perform expensive sorting queries. I need to efficiently perform all the following operations:
I will need to insert and delete players from this list. Whenever I do #3, #4 or #5, I will need to update the ranks of other players accordingly to maintain the integrity of the ranking.
What is the most efficient way to do so in Django? How do I build my models so this will work efficiently and my database operations will be minimal? Please show me what any new models should look like.
Upvotes: 3
Views: 3207
Reputation: 21779
Technically, every object in the queryset is a Python object. Which means you can assign attributes to them on-the-fly. In our case we will assign each player a rank
attribute.
So I wrote a small algorithm to calculate ranks on-the-fly. This algorithm works on the assumption that the queryset is sorted in decreasing order.
players = Player.objects.order_by('-score')
current_rank = 1
counter = 0
for player in players:
if counter < 1: # for first player
player.rank = current_rank
else: # for other players
if player.score == players[counter - 1].score:
# if player and previous player have same score,
# give them the same rank
player.rank = current_rank
else:
# first update the rank
current_rank += 1
# then assign new rank to player
player.rank = current_rank
counter += 1
Now in your templates, you can access the rank by using {{ player.rank }}
.
Limitations:
Upvotes: 1
Reputation: 51675
You can materialize ranks
and reorder on save Player, notice than not all times you need to resort ranks when a new score is saved:
The new model:
class ScoreRank(models.Model):
score = models.IntegerField(primary_key=True)
rank = models.IntegerField()
player_count = models.IntegerField()
Keeping rank sorted:
from django.db.models.signals import pre_save
@receiver(pre_save, sender=Player)
def update_score_rank(sender, instance, **kwargs):
#delete from previous rank
if instance.pk:
previous_score = ( Player.objects
.filter( id=instance.id )
.values_list( 'score', flat=True ).first() )
sc = ScoreRank.objects.get( score = previous_score )
if sc.player_count == 1:
#new hole in ranks, add -1 to other ranks to remove it:
_ = (ScoreRank.objects
.filter( score__gt = previous_score )
.update( rank=F('rank') - 1 ) )
sc.delete()
#insert in new rank
sc, is_new = ( ScoreRank.objects
.get_or_create(score=instance.score,
defaults={'rank': -1,'player_count': 1,}) )
if not is_new:
#this score is not new: add one to player_count
_ = ( ScoreRank.objects
.filter( score = instance.score )
.update( rank=F('player_count') + 1 ) )
else:
#this score is not new: make hole for it
rank = ( ScoreRank.objects
.filter( score__gt = instance.score )
.annotate( m=Min("score" ) ) )
new_rank = rank["m"] if rank["m"] else 1
_ = ( ScoreRank.objects
.filter( score__lte = new_rank )
.update( rank=F('rank') + 1 ) )
_ = ( ScoreRank.objects
.filter( sc = sc.score )
.update( rank=new_rank ) )
Don't forget to enclose database operations in a single transaction ( a serializable transaction )
Disclainer: not tested.
Upvotes: 1