I have the following models in my DB (Flask-SQLALchemy, declarative approach, simplified):
class Player(db.Model):
id = db.Column(db.Integer, primary_key = True)
...
class Game(db.Model):
id = db.Column(db.Integer, primary_key = True)
creator_id = db.Column(db.Integer, db.ForeignKey('player.id'))
creator = db.relationship(Player, foreign_keys='Game.creator_id')
opponent_id = db.Column(db.Integer, db.ForeignKey('player.id'))
opponent = db.relationship(Player, foreign_keys='Game.opponent_id')
winner = db.Column(db.Enum('creator', 'opponent'))
Each game may be either won, lost or ended in draw. I need to get players sorting them by "win rate" - i.e.:
- if player created a game and that game's winner is
creator, it is considered win; - if the player was invited to game as opponent and game's winner is
opponent, it is also considered win; - other games where this player participated are considered lost games.
So my algorithm is as follows:
@hybrid_property
def winrate(self):
games = Game.query.filter(or_(
Game.creator_id == self.id,
Game.opponent_id == self.id,
))
count = 0
wins = 0
for game in games:
count += 1
if game.creator_id == self.id and game.winner == 'creator':
wins += 1
elif game.opponent_id == self.id and game.winner == 'opponent':
wins += 1
if count == 0:
return 0
return wins / count
This approach works when I want to determine win rate for particular player; but it fails when I want to sort players by win rate. I tried to rewrite it in SQL and got something like this:
SELECT * FROM player
ORDER BY ((SELECT count(g1.id) FROM game g1
WHERE g1.creator_id = player.id AND g1.winner = 'creator'
) + (SELECT count(g2.id) FROM game g2
WHERE g2.opponent_id = player.id AND g2.winner = 'opponent'
)) / (SELECT count(g3.id) FROM game g3
WHERE player.id IN (g3.creator_id, g3.opponent_id)
)
This doesn't handle players without games but should work in general. Players without games can be probably handled with MySQL CASE statement.
But the problem is that I cannot figure how do I encode this SQL using SQLAlchemy. Here is a (simplified) code I try to use:
@winrate.expression
def winrate(cls):
cnt = Game.query.filter(
cls.id.in_(Game.creator_id, Game.opponent_id)
).with_entities(func.count(Game.id))
won = Game.query.filter(
or_(
and_(
Game.creator_id == cls.id,
Game.winner == 'creator',
),
and_(
Game.opponent_id == cls.id,
Game.winner == 'opponent',
),
)
)
return case([
(count == 0, 0),
], else_ = (
won / count
))
This code fails when it comes to won / count line telling me that Query cannot be divided by Query. I tried using subqueries but without any success.
How should I implement it? Or maybe I should use some kind of joins/whatever? (DB scheme cannot be changed.)
won.scalar() / count.scalar()?Query.scalar()produces a scalar subquery, which is a clause elementQuery.as_scalar():)