缺失Greenlet:未调用greenlet_spawn。

11
我正在尝试获取一对多关系中匹配的行数。当我尝试使用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())
2个回答

27

我认为这里的问题是默认情况下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

1
感谢@snakecharmerb的回复,这个方法可行。如果我在我的模型中添加sa_relationship_kwargs={'lazy': 'selectin'},我是否需要运行alembic并创建迁移脚本?当前表格没有配置sa_relationship_kwargs - Jake
3
关系完全存在于Python层,因此不需要迁移。 - snakecharmerb
我有同样的问题。我在我的模型中添加了“sa_relationship_kwargs = {'lazy':'selectin'}”,使用“select(model).where(model.id == id).options(selectinload(model.children)”时出现相同的错误,“greenlet_spawn未被调用!如何解决?这是我的问题:https://stackoverflow.com/questions/74835091/use-asyncsession-sqlalchemy-errorsqlalchemy-exc-missinggreenlet-greenlet-spawn - Michael Mecil

1
主要原因是在同步的sqlalchemy驱动程序中,您可以正确使用session进行延迟加载查询。假设您有两个相互关联的模型,通过查询获取父模型,然后您将通过另一个查询获取子模型(即延迟加载)。然而,当您在sqlalchemy的异步驱动程序(例如asyncpg)上使用时,每次查询后会自动关闭session,导致获取子模型信息时引发错误。
因此,一般来说,为了解决这个问题,您可以在relationship或query中采用不同的加载策略(即急加载):
关系(Relationship):
您应该在relationship中添加lazy="joined"或"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

【加载策略差异】:
【joinedload】是使用的一种策略,它执行SQL JOIN来加载相关的【Parent.children】对象。这意味着所有数据一次性加载完毕。它会减少数据库往返次数,但由于连接操作,初始加载可能会较慢。
【selectinload】策略将加载分为两个独立的查询——一个用于父对象,一个用于子对象。这有时比【joinedload】更快,因为它避免了复杂的连接操作。

PS:我使用了sqlalchemy表单而不是sqlmodel


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接