-
-
Notifications
You must be signed in to change notification settings - Fork 664
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
async - can't access parent.child item (relationship models) using async #74
Comments
I think what you're seeing is a result of SQLAlchemy trying to perform implicit IO when accessing heroes from teams without performing eager loading on heroes. If you eager load heroes, the error message should go away per the SQLAlchemy documentation. |
Just to flesh this out a bit, because I struggled to get it sorted and went round in circles for too long. Import selectinload from the sqlalachemy package and then after the selects, add the selectinload option
|
thanks, @rscottweekly that was the solution. I have created an async sample that can help others implement async sqlmodel https://github.com/jonra1993/fastapi-alembic-sqlmodel-async |
Thanks to @rscottweekly for the solution and @jonra1993 for a sample implementation - it is much appreciated! |
@rscottweekly you're an angel |
Hi, I am also facing this issue and getting the same exception. Adding I've been wrapping my head for days trying to figure out what was the cause. Please let me know where I did wrong. I expect the results to be something like this. I'm aware that this may yield infinite data/loop (e.g. Parent->Child->Parent), but please ignore that for now. {
"name":"Parent",
"children":[
{
"name":"Child",
"children":[]
}
],
} Here's an example I modified from the one given by OP import asyncio
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
from typing import List, Optional
from sqlmodel import Field, Relationship, SQLModel, select
class LinkNodes(SQLModel, table=True):
parent_id: Optional[int] = Field(default=None, nullable=False, foreign_key="node.id", primary_key=True)
child_id: Optional[int] = Field(default=None, nullable=False, foreign_key="node.id", primary_key=True)
class Node(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
children: List['Node'] = Relationship(
link_model=LinkNodes,
sa_relationship_kwargs={
"lazy": "selectin",
"primaryjoin":"Node.id==LinkNodes.parent_id",
"secondaryjoin":"Node.id==LinkNodes.child_id",
})
sqlite_file_name = "database.db"
sqlite_url = f"sqlite+aiosqlite:///{sqlite_file_name}"
engine = create_async_engine(sqlite_url, echo=True)
async def create_db_and_tables():
# SQLModel.metadata.create_all(engine)
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.drop_all)
await conn.run_sync(SQLModel.metadata.create_all)
async def create_nodes():
async with AsyncSession(engine) as session:
parent_node = Node(name="Parent")
child_node = Node(name="Child")
parent_node.children.append(child_node)
session.add(parent_node)
await session.commit()
await session.refresh(parent_node)
print(parent_node)
async def select_nodes():
async with AsyncSession(engine) as session:
statement = select(Node).where(Node.name == "Parent")
result = await session.execute(statement)
node = result.scalar()
print(f"Parent: {node}")
print(f"Children: {node.children}")
async def main():
await create_db_and_tables()
await create_nodes()
await select_nodes()
if __name__ == "__main__":
asyncio.run(main()) |
In my case - I only want to enable eager loading where I know I'm going to ask for the relationship attribute. I'm using joinedload eager loading at the query level rather than enable eager loading everywhere by defining it at the model level: import logging
from typing import List, Optional
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from app import app, engine, Heroes, Team
@app.get("/team/{team_id}/heroes", response_model=List[Heroes])
async def get_team_heroes(
team_id: int,
) -> List[Heroes]:
"""
List Heroes per Team
"""
async with AsyncSession(engine) as session:
team: Optional[Team] = await session.get(
entity=Team,
ident=team_id,
options=[
joinedload(Team.heroes) # explicit load of relationship supports async session
],
)
if team is None:
raise HTTPException(status_code=404, detail="Team does not exist")
heroes: List[Heroes] = team.heroes
return heroes |
for future reference:
EDIT: CAUTION! don't use joinedload, it breaks everything, took me a while to figure out where the issue is but the query hangs and eats up whole ram, reminds me of serverless horror stories. the following works for now (probably could be optimized)
|
hello, any clue for the lazy loading solution ? |
I've made an extension module that adds an awaitable field for accessing relationship models using async. from typing import Optional
from collections.abc import Awaitable
from sqlmodel import Field, select
from async_sqlmodel import AsyncSQLModel, AwaitableField
class Team(AsyncSQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
heroes: List["Hero"] = Relationship()
awt_heroes: Awaitable[List["Hero"]] = AwaitableField(field="heroes")
class Hero(AsyncSQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
team_id: Optional[int] = Field(default=None, foreign_key="team.id")
team: Optional[Team] = Relationship(back_populates="heroes")
awt_team: Awaitable[Optional[Team]] = AwaitableField(field="team")
hero = (
await session.exec(select(Hero).where(Hero.id == 1))
).one()
# loading lazy loading attribute will raise MissingGreenlet error
team = hero.team
# E sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called;
# can't call await_only() here. Was IO attempted in an unexpected place?
# (Background on this error at: https://sqlalche.me/e/20/xd2s)
# it works!
team = await hero.awt_team |
works like a charm 💯 , thanks a lot for this feature ! |
@2jun0 say: class Team(AsyncSQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
heroes: List["Hero"] = Relationship()
tags: List["Tag"] = Relationship()
awt_heroes: Awaitable[List["Hero"]] = AwaitableField(field="heroes")
awt_tags: Awaitable[List["Tag"]] = AwaitableField(field="tags") # get awaitable relationship values:
heros = await team.awt_heros
tags = await team.awt_tags Both heros and tags got tags values, after tests, it seems that it's always the last declared Awaitable in the Model definition (here |
@copdips |
I confirm the bug is fixed, and what you did is really awesome to me, take your time to improve it if you want. |
Using selectin related methods affects the original lazy loading design. Asynchronous access to Relationship attributes can be achieved by Example: ...
from sqlalchemy.ext.asyncio import AsyncAttrs
class Team(SQLModel, AsyncAttrs, table=True): # <-- AsyncAttrs
...
heroes: List["Hero"] = Relationship(back_populates="team")
async def select_heroes():
async with AsyncSession(engine) as session:
...
heroes = await team_preventers.awaitable_attrs.heroes # <-- awaitable_attrs
print(f"Preventers heroes: {heroes}") |
First Check
Commit to Help
Dependencies:
aiosqlite
https://pypi.org/project/aiosqlite/
Example Code
Operating System
macOS
Operating System Details
Using a MacBook with macOS running FastAPI with docker.
SQLModel Version
0.0.4
Python Version
3.8.6
Additional Context
We have two models with a one to many relationship (Team -> Hero).
Using async / await AsyncSession , we are trying to access the parent's child ( {team_preventers.heroes} ) but this is causing the following exception :
sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called; can't call await_() here. Was IO attempted in an unexpected place? (Background on this error at: https://sqlalche.me/e/14/xd2s)
sys:1: RuntimeWarning: coroutine 'Connection.cursor' was never awaited
Full stacktrace
The text was updated successfully, but these errors were encountered: