
在 SQLAlchemy 异步会话中,当需同时指定返回的具体列(如 ModelA.id, ModelA.name)和预加载的关系(如 ModelA.bs)时,直接使用 select(*columns) 会导致 ArgumentError——因其生成纯表达式实体,无法应用 selectinload 等关系加载选项。正确解法是基于完整模型构造查询,再用 with_only_columns() 限定字段。
在 sqlalchemy 异步会话中,当需同时指定返回的具体列(如 `modela.id`, `modela.name`)和预加载的关系(如 `modela.bs`)时,直接使用 `select(*columns)` 会导致 `argumenterror`——因其生成纯表达式实体,无法应用 `selectinload` 等关系加载选项。正确解法是基于完整模型构造查询,再用 `with_only_columns()` 限定字段。
要解决 Query has only expression-based entities 错误,关键在于避免将查询降级为纯列表达式(expression-only),而应始终以 ORM 实体(如 ModelA)为查询主体,再通过 with_only_columns() 精确控制返回字段。这样既保留了 ORM 上下文(支持 options() 加载关系),又实现了字段级灵活性。
✅ 正确实现方式如下:
from sqlalchemy import select
from sqlalchemy.orm import selectinload
async def get(self, fields):
columns, relationships = self.parse_fields(fields, ModelA)
# 始终以 ModelA 为查询主体(确保 ORM 实体上下文)
stmnt = select(ModelA)
# 动态限定返回的列(注意:columns 应为 ModelA 的 mapped column 对象,如 ModelA.id, ModelA.name)
if columns:
stmnt = stmnt.with_only_columns(*columns)
# 动态添加关系预加载(仅对 ModelA 实体有效)
if relationships:
# 注意:selectinload 接收的是 relationship 属性,如 ModelA.bs,而非字符串
stmnt = stmnt.options(*[selectinload(rel) for rel in relationships])
result = await self.session.execute(stmnt)
return result.scalars().all() # 使用 scalars() 获取 ModelA 实例(含已加载关系)
? 重要注意事项:
- with_only_columns() 必须作用于 select(ModelA) 这类实体查询,*不可用于 `select(columns)**;后者返回的是Row元组,无 ORM 语义,options()` 将失效。
- columns 列表应由 MappedColumn 对象构成(如 ModelA.id, ModelA.name),而非字符串或 getattr(ModelA, 'id') 等运行时反射——推荐在 parse_fields() 中严格校验并返回合法列对象。
- relationships 应为 InstrumentedAttribute(如 ModelA.bs),且必须属于 ModelA;若传入 ModelB.a 等反向关系,selectinload 将报错。
- 若只需部分字段 + 关系,但希望结果仍为 ModelA 实例(字段未选则为 None),该方案天然支持;若需纯字典/元组结构,应改用 result.mappings().all() 并手动合并关系数据。
? 进阶提示:
对于更复杂的场景(如混合 ModelA 字段与关联表 ModelB.name),可改用 join() + with_only_columns(),但需注意 selectinload 仅适用于主实体关系预加载;跨表字段需显式 join 和 add_columns()(此时不再适用 scalars())。
综上,核心原则是:“先保实体,再裁字段,后加关系” ——以 select(ModelA) 立住 ORM 上下文,用 with_only_columns() 安全裁剪字段,再通过 options(selectinload(...)) 注入关系加载逻辑,即可在异步环境中稳健实现字段与关系的动态组合查询。










