Prevent MissingGreenlet errors
Goal: eliminate the runtime errors caused by "accessing a relation field that wasn't preloaded".
Background: understand why SQLAlchemy lazy loading doesn't fly in async — see Prerequisites.
sqlmodel-ext provides three layers of defense, increasingly strict:
Third defense (always on): lazy='raise_on_sql'
Since 0.2.0, every Relationship field defaults to lazy='raise_on_sql': accessing an unloaded relation raises immediately rather than triggering an implicit synchronous query that would cause MissingGreenlet.
user = await User.get_exist_one(session, user_id) # No load=
print(user.profile) # ⚠ raise_on_sql: raises InvalidRequestError immediatelyThis defense is automatic — you don't have to do anything. The benefit is converting a confusing MissingGreenlet into a clear InvalidRequestError: 'User.profile' is not available due to lazy='raise_on_sql'.
Second defense: explicit load=
The most common approach. Declare needed relations at query time:
user = await User.get_exist_one(session, user_id, load=User.profile)
print(user.profile) # safeFor nested relations, just list them:
user = await User.get_exist_one(
session,
user_id,
load=[User.profile, Profile.avatar],
)
# Auto-builds: selectinload(User.profile).selectinload(Profile.avatar)Second defense, advanced: @requires_relations
If you're writing a model method (not an endpoint) that accesses relation fields internally, the caller would otherwise need to know "which relations does this method touch" — a fragile contract.
@requires_relations declares "I need these relations" on the method itself:
from sqlmodel_ext import RelationPreloadMixin, requires_relations
class Article(
SQLModelBase,
UUIDTableBaseMixin,
RelationPreloadMixin, # ← required
table=True,
):
author: User = Relationship()
@requires_relations('author') # ← string: this class's relation name
async def render_byline(self, session: AsyncSession) -> str:
return f"by {self.author.name}"The caller doesn't need to know anything:
article = await Article.get_exist_one(session, article_id)
byline = await article.render_byline(session) # author is auto-loadedNested relations
Use the relation attribute reference instead of a string:
@requires_relations('generator', Generator.config)
async def calculate_cost(self, session: AsyncSession) -> int:
return self.generator.config.price * 10Incremental loading
If the caller has already preloaded part of the relations, the decorator won't reload them — it only fills in the missing pieces.
Import-time validation
If you mistype @requires_relations('typo_name'), an AttributeError is raised at module import time, not at runtime.
@requires_for_update companion decorator
If a method modifies fields and save()s, callers should acquire a row lock first:
from sqlmodel_ext import requires_for_update
class Account(SQLModelBase, UUIDTableBaseMixin, RelationPreloadMixin, table=True):
balance: int
@requires_for_update # ←
async def adjust_balance(self, session: AsyncSession, *, amount: int) -> None:
self.balance += amount
await self.save(session)Callers must first acquire a FOR UPDATE lock:
account = await Account.get(session, Account.id == uid, with_for_update=True)
await account.adjust_balance(session, amount=-100) # OK
# Without the lock:
account = await Account.get_exist_one(session, uid)
await account.adjust_balance(session, amount=-100) # RuntimeError!The runtime check uses session.info[SESSION_FOR_UPDATE_KEY].
First defense (experimental, off by default): AST static analysis
Off by default since 0.3
The static analyzer makes assumptions about project layout (FastAPI endpoint conventions, STI inheritance, etc.). On other projects it may produce false positives or fail to parse. Off by default.
If your project layout matches sqlmodel-ext's assumptions, opt in explicitly:
# 1. As early as possible during application startup:
import sqlmodel_ext.relation_load_checker as rlc
rlc.check_on_startup = True
# 2. At the end of models/__init__.py, after configure_mappers():
from sqlmodel_ext import run_model_checks, SQLModelBase
run_model_checks(SQLModelBase)
# 3. In main.py:
from sqlmodel_ext import RelationLoadCheckMiddleware
app.add_middleware(RelationLoadCheckMiddleware)Once enabled, the application scans every model method and FastAPI route at startup and warns about suspicious patterns. Rule codes (RLC001 / RLC007 etc.) are documented in Static analyzer internals.
Decision tree
Are you writing a model method?
├── No (endpoint/regular function)
│ └── Use load= to preload explicitly
└── Yes
└── Is it called from multiple places?
├── No → load= is fine too
└── Yes → use @requires_relations (more robust)