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,
|
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.
|
"""Serialise a RefractorCardState into the standard API response shape.
|
||||||
|
|
||||||
Produces a flat dict with player_id and team_id as plain integers,
|
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:
|
if player_name is not None:
|
||||||
result["player_name"] = player_name
|
result["player_name"] = player_name
|
||||||
|
|
||||||
# Resolve image_url from the variant card row
|
# Resolve image_url from the variant card row.
|
||||||
image_url = None
|
# When image_url is pre-fetched by the caller (batch list path), it is
|
||||||
if state.variant and state.variant > 0:
|
# passed directly and the per-row DB query is skipped entirely.
|
||||||
card_type = (
|
if image_url is _UNSET:
|
||||||
state.track.card_type if hasattr(state, "track") and state.track else None
|
image_url = None
|
||||||
)
|
if state.variant and state.variant > 0:
|
||||||
if card_type:
|
card_type = (
|
||||||
CardModel = BattingCard if card_type == "batter" else PitchingCard
|
state.track.card_type
|
||||||
try:
|
if hasattr(state, "track") and state.track
|
||||||
variant_card = CardModel.get(
|
else None
|
||||||
(CardModel.player_id == state.player_id)
|
)
|
||||||
& (CardModel.variant == state.variant)
|
if card_type:
|
||||||
)
|
CardModel = BattingCard if card_type == "batter" else PitchingCard
|
||||||
image_url = variant_card.image_url
|
try:
|
||||||
except CardModel.DoesNotExist:
|
variant_card = CardModel.get(
|
||||||
pass
|
(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
|
result["image_url"] = image_url
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@ -230,14 +239,43 @@ async def list_card_states(
|
|||||||
query = query.where(RefractorCardState.last_evaluated_at.is_null(False))
|
query = query.where(RefractorCardState.last_evaluated_at.is_null(False))
|
||||||
|
|
||||||
total = query.count() or 0
|
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 = []
|
items = []
|
||||||
for state in query.offset(offset).limit(limit):
|
for state in states_page:
|
||||||
player_name = None
|
player_name = None
|
||||||
try:
|
try:
|
||||||
player_name = state.player.p_name
|
player_name = state.player.p_name
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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}
|
return {"count": total, "items": items}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user