我正在尝试获取一对多关系中匹配的行数。当我尝试
parent.children_count
时,我得到:
sqlalchemy.exc.MissingGreenlet:greenlet_spawn尚未被调用; 不能在这里调用await_only()。是否在意想不到的地方尝试了 IO? (此错误的背景位于:https://sqlalche.me/e/14/xd2s)
我添加了
expire_on_commit=False
但仍然遇到相同的错误。我该如何解决这个问题?
import asyncio
from uuid import UUID, uuid4
from sqlmodel import SQLModel, Relationship, Field
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
class Parent(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
children: list["Child"] = Relationship(back_populates="parent")
@property
def children_count(self):
return len(self.children)
class Child(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
parent_id: UUID = Field(default=None, foreign_key=Parent.id)
parent: "Parent" = Relationship(back_populates="children")
async def main():
engine = create_async_engine("sqlite+aiosqlite://")
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
async with AsyncSession(engine) as session:
parent = Parent()
session.add(parent)
await session.commit()
await session.refresh(parent)
print(parent.children_count) # I expect 0 here, as of now this parent has no children
asyncio.run(main())
我认为这里的问题是默认情况下 SQLAlchemy 延迟加载关系,因此访问
parent.children_count
会隐式触发数据库查询,从而导致报告的错误。
解决此问题的一种方法是在关系定义中指定除“惰性”之外的加载策略。使用 SQLModel,这看起来像:
children: list['Child'] = Relationship(
back_populates='parent', sa_relationship_kwargs={'lazy': 'selectin'}
)
这将导致 SQLAlchemy 发出额外的查询来获取关系,同时仍处于“异步模式”。另一种选择是传递
{'lazy': 'joined'}
,这将导致 SQLAlchemy 在单个 JOIN
查询中获取所有结果。
如果不需要配置关系,您可以发出指定选项的查询:
from sqlalchemy.orm import selectinload
from sqlmodel import select
...
async with AsyncSession(engine) as session:
parent = Parent()
session.add(parent)
await session.commit()
result = await session.scalars(
select(Parent).options(selectinload(Parent.children))
)
parent = result.first()
print(
parent.children_count
) # I need 0 here, as of now this parent has no children
此错误的主要原因是,在同步
sqlalchemy
驱动程序中,您可以正确使用 session
进行延迟加载查询。假设您有两个相互关系模型,通过查询获取父级,然后您将到达导致另一个查询的子级(即延迟加载)。但是,当您在 sqlalchemy
之上使用异步驱动程序(例如 asyncpg
)时,每次查询后都会自动关闭会话,导致获取子信息时出现错误。
因此,一般来说,为了解决这个问题,无论是在关系中还是在查询中,都可以采用不同的加载策略(渴望加载):
您应该在关系中添加
lazy= "joined" or "selectin"
class Parent(Base):
...
children = relationship("Child", back_populates="parent", lazy="selectin")
这样,您现在可以在 crud 方法中执行查询,如下所示:
from sqlalchemy.ext.asyncio import AsyncSession
async def create_parent(db: AsyncSession) -> Parent:
parent = Parent()
db.add(parent)
await db.commit()
await db.refresh(parent) # you need this
print(parent.children_count) # works properly now
return parent
注意:异步数据库会话已作为参数注入到 crud 方法中。
现在,假设我们没有更改关系惰性值,因此我们需要对查询进行相同的更改,如下所示:
使用
joinedload
:
from sqlalchemy.orm import joinedload, selectinload
async def create_parent(db: AsyncSession) -> Parent:
parent = Parent()
db.add(parent)
await db.commit()
result = await db.execute(
select(Parent)
.options(joinedload(Parent.children))
.where(Parent.id == parent.id)
)
parent = result.scalars().unique().one()
print(parent.children_count) # works properly now
return parent
或使用
selectin
:
from sqlalchemy.orm import joinedload, selectinload
async def create_parent(db: AsyncSession) -> Parent:
parent = Parent()
db.add(parent)
await db.commit()
result = await db.scalars(
select(Parent)
.options(selectinload(Parent.children))
.where(Parent.id == parent.id)
)
parent = result.first()
print(parent.children_count) # works properly now
return parent
[加载策略差异]:
用于执行 SQL JOIN 来加载相关的joinedload
对象。这意味着所有数据都会一次性加载。它会减少数据库往返次数,但由于连接操作,初始加载可能会更慢。Parent.children
策略将加载分解为两个单独的查询 - 一个用于父对象,一个用于子对象。这有时比 joinload 更快,因为它避免了复杂的连接。selectinload
PS:我使用了
sqlalchemy
形式而不是sqlmodel