# Preventing Implicit ORM I/O (Asyncio) !!! info "Primary sources" - [Preventing implicit I/O with AsyncSession](https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html#preventing-implicit-io-when-using-asyncsession) - [SQLAlchemy relationship loading](https://docs.sqlalchemy.org/en/21/orm/queryguide/relationships.html) ??? abstract "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=False` unless 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 ```python 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 ```python user = await session.get(User, user_id) await session.refresh(user, ["roles"]) ``` ### Pattern C: Awaitable attribute access where needed ```python # Requires AsyncAttrs mixin on mapped base or class. roles = await user.awaitable_attrs.roles ``` --- ## Practical Enforcement Model Require explicit I/O behavior on every async ORM path: 1. Define loader options for relationships and deferred columns needed by the operation. 2. Use `refresh()` or awaitable attributes only when the additional query is deliberate and visible. 3. 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.