perf(feed): Batch load media and tags to fix N+1 query
Per v1.5.0 Phase 3: Fix N+1 query pattern in feed generation. Implementation: - Add get_media_for_notes() to starpunk/media.py for batch media loading - Add get_tags_for_notes() to starpunk/tags.py for batch tag loading - Update _get_cached_notes() in starpunk/routes/public.py to use batch loading - Add comprehensive tests in tests/test_batch_loading.py Performance improvement: - Before: O(n) queries (1 query per note for media + 1 query per note for tags) - After: O(1) queries (2 queries total: 1 for all media, 1 for all tags) - Maintains same API behavior and output format All tests passing: 920 passed in 360.79s 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -725,6 +725,133 @@ def get_note_media(note_id: int) -> List[Dict]:
|
||||
return media_list
|
||||
|
||||
|
||||
def get_media_for_notes(note_ids: List[int]) -> Dict[int, List[Dict]]:
|
||||
"""
|
||||
Batch load media for multiple notes in single query
|
||||
|
||||
Per v1.5.0 Phase 3: Fixes N+1 query pattern in feed generation.
|
||||
Loads media and variants for all notes in 2 queries instead of O(n).
|
||||
|
||||
Args:
|
||||
note_ids: List of note IDs to load media for
|
||||
|
||||
Returns:
|
||||
Dict mapping note_id to list of media dicts (same format as get_note_media)
|
||||
|
||||
Examples:
|
||||
>>> result = get_media_for_notes([1, 2, 3])
|
||||
>>> result[1] # Media for note 1
|
||||
[{'id': 10, 'filename': 'test.jpg', ...}]
|
||||
>>> result[2] # Media for note 2
|
||||
[] # No media
|
||||
"""
|
||||
from starpunk.database import get_db
|
||||
|
||||
if not note_ids:
|
||||
return {}
|
||||
|
||||
db = get_db(current_app)
|
||||
|
||||
# Build placeholders for IN clause
|
||||
placeholders = ','.join('?' * len(note_ids))
|
||||
|
||||
# Query 1: Get all media for all notes
|
||||
media_rows = db.execute(
|
||||
f"""
|
||||
SELECT
|
||||
nm.note_id,
|
||||
m.id,
|
||||
m.filename,
|
||||
m.stored_filename,
|
||||
m.path,
|
||||
m.mime_type,
|
||||
m.size,
|
||||
m.width,
|
||||
m.height,
|
||||
nm.caption,
|
||||
nm.display_order
|
||||
FROM note_media nm
|
||||
JOIN media m ON nm.media_id = m.id
|
||||
WHERE nm.note_id IN ({placeholders})
|
||||
ORDER BY nm.note_id, nm.display_order
|
||||
""",
|
||||
note_ids
|
||||
).fetchall()
|
||||
|
||||
# Extract all media IDs for variant query
|
||||
media_ids = [row[1] for row in media_rows]
|
||||
|
||||
# Query 2: Get all variants for all media (if any media exists)
|
||||
variants_by_media = {}
|
||||
if media_ids:
|
||||
variant_placeholders = ','.join('?' * len(media_ids))
|
||||
variant_rows = db.execute(
|
||||
f"""
|
||||
SELECT media_id, variant_type, path, width, height, size_bytes
|
||||
FROM media_variants
|
||||
WHERE media_id IN ({variant_placeholders})
|
||||
ORDER BY media_id,
|
||||
CASE variant_type
|
||||
WHEN 'thumb' THEN 1
|
||||
WHEN 'small' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'large' THEN 4
|
||||
WHEN 'original' THEN 5
|
||||
END
|
||||
""",
|
||||
media_ids
|
||||
).fetchall()
|
||||
|
||||
# Group variants by media_id
|
||||
for row in variant_rows:
|
||||
media_id = row[0]
|
||||
if media_id not in variants_by_media:
|
||||
variants_by_media[media_id] = []
|
||||
variants_by_media[media_id].append({
|
||||
'variant_type': row[1],
|
||||
'path': row[2],
|
||||
'width': row[3],
|
||||
'height': row[4],
|
||||
'size_bytes': row[5]
|
||||
})
|
||||
|
||||
# Build result dict grouped by note_id
|
||||
result = {note_id: [] for note_id in note_ids}
|
||||
|
||||
for row in media_rows:
|
||||
note_id = row[0]
|
||||
media_id = row[1]
|
||||
|
||||
media_dict = {
|
||||
'id': media_id,
|
||||
'filename': row[2],
|
||||
'stored_filename': row[3],
|
||||
'path': row[4],
|
||||
'mime_type': row[5],
|
||||
'size': row[6],
|
||||
'width': row[7],
|
||||
'height': row[8],
|
||||
'caption': row[9],
|
||||
'display_order': row[10]
|
||||
}
|
||||
|
||||
# Add variants if they exist for this media
|
||||
if media_id in variants_by_media:
|
||||
media_dict['variants'] = {
|
||||
v['variant_type']: {
|
||||
'path': v['path'],
|
||||
'width': v['width'],
|
||||
'height': v['height'],
|
||||
'size_bytes': v['size_bytes']
|
||||
}
|
||||
for v in variants_by_media[media_id]
|
||||
}
|
||||
|
||||
result[note_id].append(media_dict)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def delete_media(media_id: int) -> None:
|
||||
"""
|
||||
Delete media file, variants, and database record
|
||||
|
||||
Reference in New Issue
Block a user