防止 MissingGreenlet 错误
目标:消除"访问未预加载的关系字段时抛 MissingGreenlet"这类运行时错误。
前置知识:理解 SQLAlchemy 的懒加载在异步环境下为什么不能直接用——见 前置知识。
sqlmodel-ext 提供三道防线,从前到后逐渐严格:
第三道防线(默认开启):lazy='raise_on_sql'
0.2.0 起,所有 Relationship 字段的默认 lazy 设置为 'raise_on_sql':访问未加载的关系立刻抛异常,而不是触发隐式同步查询导致 MissingGreenlet。
user = await User.get_exist_one(session, user_id) # 没有 load=
print(user.profile) # ⚠ raise_on_sql: 立刻抛 InvalidRequestError这道防线是自动的,你什么都不用做。它的好处是把"错误信息"从难懂的 MissingGreenlet 变成清晰的 InvalidRequestError: 'User.profile' is not available due to lazy='raise_on_sql'。
第二道防线:load= 显式预加载
最常用的做法。在查询时声明需要的关系:
user = await User.get_exist_one(session, user_id, load=User.profile)
print(user.profile) # 安全嵌套关系直接列出来:
user = await User.get_exist_one(
session,
user_id,
load=[User.profile, Profile.avatar],
)
# 自动构建: selectinload(User.profile).selectinload(Profile.avatar)第二道防线进阶:@requires_relations 装饰器
如果你写的是模型方法(不是端点),而方法内部访问关系字段,那么调用方必须知道"这个方法会访问哪些关系"——这是漏洞百出的契约。
@requires_relations 把"我需要哪些关系"声明在方法本身:
from sqlmodel_ext import RelationPreloadMixin, requires_relations
class Article(
SQLModelBase,
UUIDTableBaseMixin,
RelationPreloadMixin, # ← 必须继承
table=True,
):
author: User = Relationship()
@requires_relations('author') # ← 字符串:本类的关系名
async def render_byline(self, session: AsyncSession) -> str:
return f"by {self.author.name}"调用方什么都不用知道:
article = await Article.get_exist_one(session, article_id)
byline = await article.render_byline(session) # 自动加载 author嵌套关系
用关系属性引用而不是字符串:
@requires_relations('generator', Generator.config)
async def calculate_cost(self, session: AsyncSession) -> int:
return self.generator.config.price * 10增量加载
如果调用方已经预加载了一部分关系,装饰器不会重复加载,只补缺失的那部分。
导入时验证
@requires_relations('typo_name') 中的字符串如果拼错了,模块导入时就会抛 AttributeError——而不是等到运行时才暴露。
@requires_for_update 配套装饰器
如果方法内部要修改字段并 save(),应该让调用方先获取行锁:
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)调用方必须先获取 FOR UPDATE 锁:
account = await Account.get(session, Account.id == uid, with_for_update=True)
await account.adjust_balance(session, amount=-100) # OK
# 没加锁就调用:
account = await Account.get_exist_one(session, uid)
await account.adjust_balance(session, amount=-100) # RuntimeError!运行时通过 session.info[SESSION_FOR_UPDATE_KEY] 检查锁定状态。
第一道防线(实验性,默认关闭):AST 静态分析
0.3 起默认关闭
静态分析器对项目结构有特定假设(FastAPI 端点命名、STI 继承约定),在不同项目上可能产生误报或解析失败。默认关闭。
如果你的项目结构和 sqlmodel-ext 假设一致,可以显式启用:
# 1. 在应用启动最早期:
import sqlmodel_ext.relation_load_checker as rlc
rlc.check_on_startup = True
# 2. 在 models/__init__.py 末尾,configure_mappers() 之后:
from sqlmodel_ext import run_model_checks, SQLModelBase
run_model_checks(SQLModelBase)
# 3. 在 main.py 中:
from sqlmodel_ext import RelationLoadCheckMiddleware
app.add_middleware(RelationLoadCheckMiddleware)启用后,应用启动时会扫描所有模型方法和 FastAPI 路由,发现可疑模式立刻警告。规则代码(如 RLC001 / RLC007)见 静态分析器原理。
决策树
你写的是模型方法吗?
├── 否(端点/普通函数)
│ └── 用 load= 显式预加载
└── 是
└── 是否被多个调用方使用?
├── 否 → 用 load= 也行
└── 是 → 用 @requires_relations(更稳)