MissingGreenlet:greenlet_spawn尚未被调用

问题描述 投票:0回答:2

我正在尝试获取一对多关系中匹配的行数。当我尝试

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())
python sqlite sqlalchemy python-asyncio sqlmodel
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
投票

此错误的主要原因是,在同步

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

[加载策略差异]:

joinedload
用于执行 SQL JOIN 来加载相关的
Parent.children
对象。这意味着所有数据都会一次性加载。它会减少数据库往返次数,但由于连接操作,初始加载可能会更慢。

selectinload
策略将加载分解为两个单独的查询 - 一个用于父对象,一个用于子对象。这有时比 joinload 更快,因为它避免了复杂的连接。


PS:我使用了

sqlalchemy
形式而不是
sqlmodel

© www.soinside.com 2019 - 2024. All rights reserved.