Skip to content

Add an AwaitableField for accessing sqlmodel field asynchronously#872

Closed
2jun0 wants to merge 6 commits intofastapi:mainfrom
2jun0:awaitable-field
Closed

Add an AwaitableField for accessing sqlmodel field asynchronously#872
2jun0 wants to merge 6 commits intofastapi:mainfrom
2jun0:awaitable-field

Conversation

@2jun0
Copy link
Copy Markdown

@2jun0 2jun0 commented Apr 1, 2024

I'm not that good at English. So, if there's anything you don't understand, please feel free to reply.

Currently, accessing some fields asynchronously is hard. I often encounter the MissingGreenlet error while programming asynchronously, which happens because attempting IO without using await keyword when accessing lazy loading or expired fields. (others seem to have this problem too #868 #74)

While SQLAlchemy provides the AsyncAttr Mixin, I found it not suitable for sqlmodel because the completion wasn't good enough.

So, I propose an AwaitableField, making access to other fields awaitable like below:

Usage

Create a AsyncSQLModel and add an awaitable field

You can easliy create a AsyncSQLModel Using the same interface as a sqlmodel. and an AwaitableField yields an awaitable field for the field specified in the argument.

from typing import Optional, Awaitable

from sqlmodel import Field
from sqlmodel.ext.asyncio.async_model import AsyncSQLModel, AwaitableField


class Hero(AsyncSQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str
    awt_name: Awaitable[str] = AwaitableField(field="name")

This allows fields which may be subject to lazy loading or deferred / unexpiry loading to be accessed like this:

hero = Hero(name="Rusty-Man")
async_session.add(hero)
await async_session.commit()

# the fields of "hero" have expired.
# Therefore, accessing them will raise MissingGreenlet error
print(hero.name)
# 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!
print(await hero.awt_name) # Rusty-Man

Access a Relationship Field using an AwaitableField

Using an AwaitableField with Relationship fields can resolve the problem encountered during lazy loading

from typing import Optional, Awaitable

from sqlmodel import Field, select
from sqlmodel.ext.asyncio.async_model 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 == hero_rusty_man.id))
).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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2024

📝 Docs preview for commit 2d9af2f at: https://bfab4bb5.sqlmodel.pages.dev

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2024

📝 Docs preview for commit 548f005 at: https://d4117a75.sqlmodel.pages.dev

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2024

📝 Docs preview for commit 1d7f73e at: https://b18eab40.sqlmodel.pages.dev

@marrocksd
Copy link
Copy Markdown

@tiangolo this is very useful. My workaround now is a ton of lazy loads

@alejsdev alejsdev added the feature New feature or request label Jul 12, 2024
@yfranasiuk
Copy link
Copy Markdown

any news on this issue?

@github-actions github-actions bot added the conflicts Automatically generated when a PR has a merge conflict label Sep 5, 2025
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Sep 5, 2025

This pull request has a merge conflict that needs to be resolved.

Copy link
Copy Markdown
Member

@YuriiMotov YuriiMotov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@2jun0, thank you for your interest and efforts!

My personal opinions is that we don't need this.
This gives an alternative to lazy load instructions\declarations, but the syntax is not nice:

  • we have to declare all types as Awaitable[str]
  • we have to add await every time we access field

Working with async SQLAlchemy (and SQLModel) is more difficult, but even with suggested changes people will still need to learn it to avoid issues with accessing fields when DB session has already been closed, and other situations.
Also, lazy loading instructions\declarations will be more efficient in terms of performance.

Thanks again, but I think we should close this PR.

@2jun0
Copy link
Copy Markdown
Author

2jun0 commented Oct 28, 2025

@YuriiMotov Understood, thank you for reviewing :)

@2jun0 2jun0 closed this Oct 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conflicts Automatically generated when a PR has a merge conflict feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants