perf: view_tryout, une requete par lot au lieu d une par ligne
PERF-001, la page la plus consultee de l application. Quatre boucles
posaient une requete par ligne :
User.query.get() par inscription
Evaluation.query par joueur inscrit, pour savoir si ce coach
l avait deja evalue
TeamMember.query par equipe
User.query.get() par membre d equipe
Plus, sur chaque match de type player_vs_player, deux interrogations
supplementaires de la relation dynamique `participants` pour trier par
camp -- alors que la liste complete venait d etre chargee douze lignes plus
haut.
Toutes remplacees par un chargement groupe. Les evaluations de ce coach
sont deduites de la liste `evaluations` deja en memoire, pas redemandees.
Mesure, sur un tryout de 10 inscrits, 2 equipes et 1 match :
34 requetes avant, 12 apres. Le test fixe un budget de 25, volontairement
large -- il ne peut que baisser, et il echoue sur le code d avant.
_users_by_id() est le helper partage par les trois chargements ; une ligne
absente est simplement absente du dictionnaire, ce que faisait deja un
get() renvoyant None.
Un second test verifie que les dix joueurs apparaissent toujours sur la
page : une requete groupee qui perd des lignes est le risque reel ici, pas
l erreur bruyante.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+54
-23
@@ -37,6 +37,25 @@ def can_manage():
|
||||
return isinstance(current_user, (Admin, Manager))
|
||||
|
||||
|
||||
def _users_by_id(user_ids):
|
||||
"""Load these users in one query, keyed by id.
|
||||
|
||||
Replaces the `User.query.get()`-inside-a-loop that view_tryout used in
|
||||
three separate places (PERF-001). Missing ids are simply absent from
|
||||
the result, which is what a per-row get() returning None amounted to.
|
||||
|
||||
Args:
|
||||
user_ids: Iterable of primary keys, may repeat and may be empty.
|
||||
|
||||
Returns:
|
||||
dict[int, User]
|
||||
"""
|
||||
wanted = {user_id for user_id in user_ids if user_id}
|
||||
if not wanted:
|
||||
return {}
|
||||
return {user.id: user for user in User.query.filter(User.id.in_(wanted)).all()}
|
||||
|
||||
|
||||
@tryouts_bp.route('')
|
||||
@login_required
|
||||
def list_tryouts():
|
||||
@@ -289,19 +308,29 @@ def view_tryout(tryout_id):
|
||||
flash(_('You do not have permission to view this tryout.'), 'danger')
|
||||
return redirect(url_for('tryouts.list_tryouts'))
|
||||
|
||||
# Everything below used to run one query per row (PERF-001): one
|
||||
# User.query.get() per registration, one Evaluation lookup per player,
|
||||
# one TeamMember query per team and one more User.query.get() per
|
||||
# member. Thirty registrants and four teams put this page well past a
|
||||
# hundred round trips, on unindexed columns.
|
||||
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all()
|
||||
registered_players = [User.query.get(r.player_id) for r in registrations if r.player_id]
|
||||
registered_player_ids = [r.player_id for r in registrations if r.player_id]
|
||||
players_by_id = _users_by_id(registered_player_ids)
|
||||
registered_players = [
|
||||
players_by_id[player_id]
|
||||
for player_id in registered_player_ids
|
||||
if player_id in players_by_id
|
||||
]
|
||||
evaluations = Evaluation.query.filter_by(tryout_id=tryout_id).all()
|
||||
|
||||
player_eval_status = {}
|
||||
if current_user.can_evaluate():
|
||||
for p in registered_players:
|
||||
existing = Evaluation.query.filter_by(
|
||||
tryout_id=tryout_id,
|
||||
player_id=p.id,
|
||||
evaluator_id=current_user.id,
|
||||
).first()
|
||||
player_eval_status[p.id] = existing is not None
|
||||
evaluated_by_me = {
|
||||
row.player_id
|
||||
for row in evaluations
|
||||
if row.evaluator_id == current_user.id and row.player_id
|
||||
}
|
||||
player_eval_status = {p.id: p.id in evaluated_by_me for p in registered_players}
|
||||
|
||||
is_registered = (
|
||||
TryoutRegistration.query.filter_by(
|
||||
@@ -312,17 +341,16 @@ def view_tryout(tryout_id):
|
||||
)
|
||||
|
||||
teams = Team.query.filter_by(tryout_id=tryout_id).all()
|
||||
team_data = []
|
||||
for team in teams:
|
||||
members = TeamMember.query.filter_by(team_id=team.id).all()
|
||||
team_data.append(
|
||||
{
|
||||
'team': team,
|
||||
'members': [
|
||||
{'player': User.query.get(m.player_id), 'position': m.position} for m in members
|
||||
],
|
||||
}
|
||||
)
|
||||
team_ids = [team.id for team in teams]
|
||||
members_by_team = {}
|
||||
if team_ids:
|
||||
member_rows = TeamMember.query.filter(TeamMember.team_id.in_(team_ids)).all()
|
||||
member_players = _users_by_id([m.player_id for m in member_rows if m.player_id])
|
||||
for row in member_rows:
|
||||
members_by_team.setdefault(row.team_id, []).append(
|
||||
{'player': member_players.get(row.player_id), 'position': row.position}
|
||||
)
|
||||
team_data = [{'team': team, 'members': members_by_team.get(team.id, [])} for team in teams]
|
||||
|
||||
can_edit = current_user.can_manage_this_tryout(tryout)
|
||||
|
||||
@@ -382,15 +410,18 @@ def view_tryout(tryout_id):
|
||||
else [],
|
||||
}
|
||||
elif match.match_type == 'player_vs_player':
|
||||
# Filtered from the list already in hand. Asking the dynamic
|
||||
# relationship again cost two more round trips per match for
|
||||
# rows that were loaded a dozen lines above.
|
||||
team1_players = [
|
||||
{'name': p.player.username, 'position': p.position}
|
||||
for p in match.participants.filter_by(team_side=1).all()
|
||||
if p.player
|
||||
for p in all_participants
|
||||
if p.team_side == 1 and p.player
|
||||
]
|
||||
team2_players = [
|
||||
{'name': p.player.username, 'position': p.position}
|
||||
for p in match.participants.filter_by(team_side=2).all()
|
||||
if p.player
|
||||
for p in all_participants
|
||||
if p.team_side == 2 and p.player
|
||||
]
|
||||
participants = {
|
||||
'team1': 'Team 1',
|
||||
|
||||
Reference in New Issue
Block a user