perf: batch image_url prefetch in list_card_states to eliminate N+1 (#199)
Replace per-row CardModel.get() in _build_card_state_response with a bulk prefetch in list_card_states: collect variant player IDs, issue at most 2 queries (BattingCard + PitchingCard), build a (player_id, variant) -> image_url map, and pass the resolved value directly to the helper. The single-card get_card_state path is unchanged and still resolves image_url inline (one extra query is acceptable for a single-item response). Closes #199 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3852fe1408
commit
b7196c1c56
@ -23,8 +23,12 @@ _NEXT_THRESHOLD_ATTR = {
|
||||
4: None,
|
||||
}
|
||||
|
||||
# Sentinel used by _build_card_state_response to distinguish "caller did not
|
||||
# pass image_url" (do the DB lookup) from "caller passed None" (use None).
|
||||
_UNSET = object()
|
||||
|
||||
def _build_card_state_response(state, player_name=None) -> dict:
|
||||
|
||||
def _build_card_state_response(state, player_name=None, image_url=_UNSET) -> dict:
|
||||
"""Serialise a RefractorCardState into the standard API response shape.
|
||||
|
||||
Produces a flat dict with player_id and team_id as plain integers,
|
||||
@ -67,22 +71,27 @@ def _build_card_state_response(state, player_name=None) -> dict:
|
||||
if player_name is not None:
|
||||
result["player_name"] = player_name
|
||||
|
||||
# Resolve image_url from the variant card row
|
||||
image_url = None
|
||||
if state.variant and state.variant > 0:
|
||||
card_type = (
|
||||
state.track.card_type if hasattr(state, "track") and state.track else None
|
||||
)
|
||||
if card_type:
|
||||
CardModel = BattingCard if card_type == "batter" else PitchingCard
|
||||
try:
|
||||
variant_card = CardModel.get(
|
||||
(CardModel.player_id == state.player_id)
|
||||
& (CardModel.variant == state.variant)
|
||||
)
|
||||
image_url = variant_card.image_url
|
||||
except CardModel.DoesNotExist:
|
||||
pass
|
||||
# Resolve image_url from the variant card row.
|
||||
# When image_url is pre-fetched by the caller (batch list path), it is
|
||||
# passed directly and the per-row DB query is skipped entirely.
|
||||
if image_url is _UNSET:
|
||||
image_url = None
|
||||
if state.variant and state.variant > 0:
|
||||
card_type = (
|
||||
state.track.card_type
|
||||
if hasattr(state, "track") and state.track
|
||||
else None
|
||||
)
|
||||
if card_type:
|
||||
CardModel = BattingCard if card_type == "batter" else PitchingCard
|
||||
try:
|
||||
variant_card = CardModel.get(
|
||||
(CardModel.player_id == state.player_id)
|
||||
& (CardModel.variant == state.variant)
|
||||
)
|
||||
image_url = variant_card.image_url
|
||||
except CardModel.DoesNotExist:
|
||||
pass
|
||||
result["image_url"] = image_url
|
||||
|
||||
return result
|
||||
@ -230,14 +239,43 @@ async def list_card_states(
|
||||
query = query.where(RefractorCardState.last_evaluated_at.is_null(False))
|
||||
|
||||
total = query.count() or 0
|
||||
states_page = list(query.offset(offset).limit(limit))
|
||||
|
||||
# Pre-fetch image_urls in at most 2 bulk queries (one per card table) so
|
||||
# that _build_card_state_response never issues a per-row CardModel.get().
|
||||
batter_pids: set[int] = set()
|
||||
pitcher_pids: set[int] = set()
|
||||
for state in states_page:
|
||||
if state.variant and state.variant > 0:
|
||||
card_type = state.track.card_type if state.track else None
|
||||
if card_type == "batter":
|
||||
batter_pids.add(state.player_id)
|
||||
elif card_type in ("sp", "rp"):
|
||||
pitcher_pids.add(state.player_id)
|
||||
|
||||
image_url_map: dict[tuple[int, int], str | None] = {}
|
||||
if batter_pids:
|
||||
for card in BattingCard.select().where(BattingCard.player_id.in_(batter_pids)):
|
||||
image_url_map[(card.player_id, card.variant)] = card.image_url
|
||||
if pitcher_pids:
|
||||
for card in PitchingCard.select().where(
|
||||
PitchingCard.player_id.in_(pitcher_pids)
|
||||
):
|
||||
image_url_map[(card.player_id, card.variant)] = card.image_url
|
||||
|
||||
items = []
|
||||
for state in query.offset(offset).limit(limit):
|
||||
for state in states_page:
|
||||
player_name = None
|
||||
try:
|
||||
player_name = state.player.p_name
|
||||
except Exception:
|
||||
pass
|
||||
items.append(_build_card_state_response(state, player_name=player_name))
|
||||
img_url = image_url_map.get((state.player_id, state.variant))
|
||||
items.append(
|
||||
_build_card_state_response(
|
||||
state, player_name=player_name, image_url=img_url
|
||||
)
|
||||
)
|
||||
|
||||
return {"count": total, "items": items}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user