mirror of
https://github.com/rommapp/romm.git
synced 2026-06-28 06:46:00 +00:00
perf(roms): avoid hydrating full Rom rows for siblings on list endpoint
The paginated ROM list eager-loaded sibling_roms via selectinload, which hydrated full Rom ORM instances (including heavy JSON metadata columns) for every sibling even though only an existence/count check was needed on the frontend. On large collections this dominated request latency. Split sibling handling by response shape: - SimpleRomSchema (list): siblings is now list[int]; populated per page by a single SELECT against the sibling_roms view projecting only (rom_id, sibling_rom_id) — no Rom row hydration. - DetailedRomSchema (detail): keeps full SiblingRomSchema objects, with load_only on (id, name, fs_name_no_tags, fs_name_no_ext) so sibling rows stop dragging in JSON metadata. Frontend usage already only consumes siblings.length on list views; the detail-page VersionSwitcher continues to receive the richer schema.
This commit is contained in:
@@ -296,7 +296,6 @@ class RomSchema(BaseModel):
|
||||
missing_from_fs: bool
|
||||
has_notes: bool
|
||||
|
||||
siblings: list[SiblingRomSchema]
|
||||
rom_user: RomUserSchema
|
||||
merged_screenshots: list[str]
|
||||
merged_ra_metadata: RomRAMetadata | None
|
||||
@@ -304,15 +303,6 @@ class RomSchema(BaseModel):
|
||||
@classmethod
|
||||
def populate_properties(cls, db_rom: Rom, request: Request) -> Rom:
|
||||
db_rom.rom_user = RomUserSchema.for_user(request.user.id, db_rom) # type: ignore
|
||||
db_rom.siblings = [ # type: ignore
|
||||
SiblingRomSchema(
|
||||
id=s.id,
|
||||
name=s.name,
|
||||
fs_name_no_tags=s.fs_name_no_tags,
|
||||
fs_name_no_ext=s.fs_name_no_ext,
|
||||
)
|
||||
for s in db_rom.sibling_roms
|
||||
]
|
||||
db_rom.has_notes = any( # type: ignore
|
||||
note.is_public or note.user_id == request.user.id for note in db_rom.notes
|
||||
)
|
||||
@@ -330,10 +320,6 @@ class RomSchema(BaseModel):
|
||||
def sort_files(cls, v: list[RomFileSchema]) -> list[RomFileSchema]:
|
||||
return sorted(v, key=lambda x: x.file_name)
|
||||
|
||||
@field_validator("siblings")
|
||||
def sort_siblings(cls, v: list[SiblingRomSchema]) -> list[SiblingRomSchema]:
|
||||
return sorted(v, key=lambda x: x.sort_comparator)
|
||||
|
||||
|
||||
class SiblingRomSchema(BaseModel):
|
||||
id: int
|
||||
@@ -355,9 +341,13 @@ class SiblingRomSchema(BaseModel):
|
||||
|
||||
|
||||
class SimpleRomSchema(RomSchema):
|
||||
siblings: list[int] = []
|
||||
|
||||
@classmethod
|
||||
def from_orm_with_request(cls, db_rom: Rom, request: Request) -> SimpleRomSchema:
|
||||
db_rom = cls.populate_properties(db_rom, request)
|
||||
# `sibling_ids` is set on each Rom by the list endpoint before serialization.
|
||||
db_rom.siblings = getattr(db_rom, "sibling_ids", []) # type: ignore
|
||||
return cls.model_validate(db_rom)
|
||||
|
||||
@classmethod
|
||||
@@ -387,16 +377,30 @@ class UserCollectionSchema(BaseModel):
|
||||
|
||||
|
||||
class DetailedRomSchema(RomSchema):
|
||||
siblings: list[SiblingRomSchema] = []
|
||||
user_saves: list[SaveSchema]
|
||||
user_states: list[StateSchema]
|
||||
user_screenshots: list[ScreenshotSchema]
|
||||
user_collections: list[UserCollectionSchema]
|
||||
all_user_notes: list[UserNoteSchema]
|
||||
|
||||
@field_validator("siblings")
|
||||
def sort_siblings(cls, v: list[SiblingRomSchema]) -> list[SiblingRomSchema]:
|
||||
return sorted(v, key=lambda x: x.sort_comparator)
|
||||
|
||||
@classmethod
|
||||
def from_orm_with_request(cls, db_rom: Rom, request: Request) -> DetailedRomSchema:
|
||||
user_id = request.user.id
|
||||
db_rom = cls.populate_properties(db_rom, request)
|
||||
db_rom.siblings = [ # type: ignore
|
||||
SiblingRomSchema(
|
||||
id=s.id,
|
||||
name=s.name,
|
||||
fs_name_no_tags=s.fs_name_no_tags,
|
||||
fs_name_no_ext=s.fs_name_no_ext,
|
||||
)
|
||||
for s in db_rom.sibling_roms
|
||||
]
|
||||
|
||||
db_rom.user_saves = [ # type: ignore
|
||||
SaveSchema.model_validate(s) for s in db_rom.saves if s.user_id == user_id
|
||||
|
||||
@@ -569,12 +569,20 @@ def get_roms(
|
||||
with sync_session.begin() as session:
|
||||
rom_id_index = session.scalars(query.with_only_columns(Rom.id)).all() # type: ignore
|
||||
|
||||
def _transform(items):
|
||||
sibling_ids_by_rom = db_rom_handler.get_sibling_ids_for_roms(
|
||||
[i.id for i in items]
|
||||
)
|
||||
for rom in items:
|
||||
rom.sibling_ids = sibling_ids_by_rom.get(rom.id, [])
|
||||
return [
|
||||
SimpleRomSchema.from_orm_with_request(i, request) for i in items
|
||||
]
|
||||
|
||||
return paginate(
|
||||
session,
|
||||
query,
|
||||
transformer=lambda items: [
|
||||
SimpleRomSchema.from_orm_with_request(i, request) for i in items
|
||||
],
|
||||
transformer=_transform,
|
||||
additional_data={
|
||||
"char_index": char_index_dict,
|
||||
"rom_id_index": rom_id_index,
|
||||
|
||||
@@ -38,7 +38,7 @@ from decorators.database import begin_session
|
||||
from handler.metadata.base_handler import UniversalPlatformSlug as UPS
|
||||
from models.assets import Save, Screenshot, State
|
||||
from models.platform import Platform
|
||||
from models.rom import Rom, RomFile, RomMetadata, RomNote, RomUser
|
||||
from models.rom import Rom, RomFile, RomMetadata, RomNote, RomUser, SiblingRom
|
||||
from utils.database import (
|
||||
json_array_contains_all,
|
||||
json_array_contains_any,
|
||||
@@ -146,7 +146,14 @@ def with_details(func):
|
||||
joinedload(RomFile.rom).load_only(Rom.fs_path, Rom.fs_name)
|
||||
),
|
||||
selectinload(Rom.sibling_roms).options(
|
||||
noload(Rom.platform), noload(Rom.metadatum)
|
||||
load_only(
|
||||
Rom.id,
|
||||
Rom.name,
|
||||
Rom.fs_name_no_tags,
|
||||
Rom.fs_name_no_ext,
|
||||
),
|
||||
noload(Rom.platform),
|
||||
noload(Rom.metadatum),
|
||||
),
|
||||
selectinload(Rom.collections),
|
||||
selectinload(Rom.notes),
|
||||
@@ -195,6 +202,33 @@ class DBRomsHandler(DBBaseHandler):
|
||||
return []
|
||||
return session.scalars(query.filter(Rom.id.in_(ids))).all()
|
||||
|
||||
@begin_session
|
||||
def get_sibling_ids_for_roms(
|
||||
self,
|
||||
rom_ids: Iterable[int],
|
||||
*,
|
||||
session: Session = None, # type: ignore
|
||||
) -> dict[int, list[int]]:
|
||||
"""Return {rom_id: [sibling_rom_id, ...]} for the given rom IDs.
|
||||
|
||||
Single query against the sibling_roms view, projecting only the two
|
||||
id columns — no Rom row hydration.
|
||||
"""
|
||||
ids = list(rom_ids)
|
||||
if not ids:
|
||||
return {}
|
||||
|
||||
rows = session.execute(
|
||||
select(SiblingRom.rom_id, SiblingRom.sibling_rom_id).where(
|
||||
SiblingRom.rom_id.in_(ids)
|
||||
)
|
||||
).all()
|
||||
|
||||
result: dict[int, list[int]] = {rom_id: [] for rom_id in ids}
|
||||
for rom_id, sibling_rom_id in rows:
|
||||
result[rom_id].append(sibling_rom_id)
|
||||
return result
|
||||
|
||||
def filter_by_platform_id(self, query: Query, platform_id: int):
|
||||
return query.filter(Rom.platform_id == platform_id)
|
||||
|
||||
@@ -564,10 +598,8 @@ class DBRomsHandler(DBBaseHandler):
|
||||
selectinload(Rom.files).options(
|
||||
joinedload(RomFile.rom).load_only(Rom.fs_path, Rom.fs_name)
|
||||
),
|
||||
# Show sibling rom badges on cards
|
||||
selectinload(Rom.sibling_roms).options(
|
||||
noload(Rom.platform), noload(Rom.metadatum)
|
||||
),
|
||||
# Sibling badges are populated separately via get_sibling_ids_for_roms
|
||||
# to avoid hydrating full Rom rows (including JSON metadata) per sibling.
|
||||
# Show notes indicator on cards
|
||||
selectinload(Rom.notes),
|
||||
)
|
||||
|
||||
@@ -15,7 +15,6 @@ import type { RomMobyMetadata } from './RomMobyMetadata';
|
||||
import type { RomRAMetadata } from './RomRAMetadata';
|
||||
import type { RomSSMetadata } from './RomSSMetadata';
|
||||
import type { RomUserSchema } from './RomUserSchema';
|
||||
import type { SiblingRomSchema } from './SiblingRomSchema';
|
||||
export type SimpleRomSchema = {
|
||||
id: number;
|
||||
igdb_id: (number | null);
|
||||
@@ -83,7 +82,7 @@ export type SimpleRomSchema = {
|
||||
updated_at: string;
|
||||
missing_from_fs: boolean;
|
||||
has_notes: boolean;
|
||||
siblings: Array<SiblingRomSchema>;
|
||||
siblings: Array<number>;
|
||||
rom_user: RomUserSchema;
|
||||
merged_screenshots: Array<string>;
|
||||
merged_ra_metadata: (RomRAMetadata | null);
|
||||
|
||||
Reference in New Issue
Block a user