Preventing Implicit ORM I/O (Asyncio)¶
Decision metadata
- Status: adopted
- Decision level: advisory
- Applies to: api-runtime, workers, tests
- Last reviewed: 2026-06-17
Purpose¶
Minimize unexpected database round-trips caused by attribute access in async ORM code.
In asyncio applications, hidden lazy loads are easy to miss and can produce runtime surprises. This guide defines explicit-loading defaults and progressive enforcement practices.
Scope and Non-Goals¶
- In scope: relationship loading strategy, post-commit attribute access, explicit refresh/awaitable access patterns.
- Out of scope: full ORM performance tuning and domain-specific query architecture.
Rules¶
- Prefer explicit eager loading for data required by endpoint/service outputs.
- Avoid relying on implicit lazy-load behavior in request critical paths.
- Keep
expire_on_commit=Falseunless strict expiration behavior is intentionally required. - Use explicit refresh or awaitable-attribute access when loading deferred state is necessary.
Recommended Patterns¶
Pattern A: Eager-load what you need¶
from sqlalchemy import select
from sqlalchemy.orm import selectinload
stmt = select(User).options(selectinload(User.roles))
users = (await session.scalars(stmt)).all()
Pattern B: Explicit refresh of named attributes¶
Pattern C: Awaitable attribute access where needed¶
Practical Enforcement Model¶
Require explicit I/O behavior on every async ORM path:
- Define loader options for relationships and deferred columns needed by the operation.
- Use
refresh()or awaitable attributes only when the additional query is deliberate and visible. - Add review checks that reject unplanned lazy-load paths.
This keeps event-loop behavior predictable and makes query boundaries reviewable from the code.
Anti-Patterns¶
- Returning ORM objects from handlers and triggering lazy loads during serialization.
- Assuming post-commit attribute access will always be loaded without explicit strategy.
- Relying on broad expiration + implicit reload behavior in async request flows.
- Enabling relationship patterns that hide SQL behavior in critical code paths.
Operational Checks¶
- Endpoint query blocks define loader options for returned related data.
- Critical handlers do not depend on incidental lazy loads.
- Known exceptions are documented with rationale and follow-up items.
Testing Checks¶
- Integration tests cover endpoints that return related objects.
- Tests verify expected data is present without hidden secondary query surprises.
- Regression tests exist for routes previously affected by implicit-load failures.