-
Notifications
You must be signed in to change notification settings - Fork 0
Eager Loading of Relationships in Async Database Access
The data model for User contains a relationship to a User_Password object, like this:
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True, nullable=False)
email: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
username: Mapped[str] = mapped_column(String(50))
created_at: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), server_default=func.now())
user_password: Mapped["UserPassword"] = relationship("UserPassword", uselist=False, backref="user",
cascade="all, delete-orphan")
class UserPassword(Base):
...
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"),
nullable=False)The application performs database I/O using an async database driver with SqlAlchemy's async ORM, and the data access functions uses async code as well. For example:
# user_dao.py
from sqlalchemy.ext.asyncio import AsyncSession
async def get_user_by_id(session: AsyncSession, user_id: int) -> models.User | None:
"""Get a user from database using the primary key (id)."""
...
result = await session.get(models.User, user_id)
return resultAn sqlalchemy.exc.MissingGreenlet exception is raised when I do something like this:
user = await user_dao.get_user_by_id(session, user_id=1)
if user.user_password: <---- Raised sqlalchemy.exc.MissingGreenlet exception
# get the hashed passwordThe cause is that user.user_password tries to access a lazy-loaded relationship outside of an async context or without the proper async database session.
If user.user_password is not eagerly loaded, SQLAlchemy tries to fetch it lazily. In async mode (with SQLAlchemy 1.4+ and async drivers), lazy loading requires a running greenlet (async context). If you access the relationship after the session is closed or outside the async context, SQLAlchemy can't perform the database query and raises MissingGreenlet.
Option 1: Eagerly load the relationship
Use eager loading for relationships you need immediately after object creation or retrieval.
Modify the query in create_user to eagerly load user_password using selectinload or joinedload:
# In your DAO or query
from sqlalchemy.orm import selectinload, joinedload
stmt = select(User).options(selectinload(User.user_password)).where(User.id == user_id)
result = await session.execute(stmt)
user = result.scalar_one()or
stmt = select(User).options(joinedload(User.user_password))).where(User.id == user_id)
result = await session.execute(stmt)
user = result.scalar_one()Comparison
| Command | SQL statements | Risk of Duplicate User rows? | Use Case |
|---|---|---|---|
joinedload |
1 | Yes - one per parent x child | Many-to-one or small one-to-many where performance matters |
selectinload |
2 | No | General use; one-to-many or many-to-many |
-
joinedloaduses aLEFT OUTER JOIN -
selectinloaduses 2 selects:SELECT usersandSELECT userpasswords IN (...) - For one-to-one, as in this case,
joinedloadis preferred. You can also add a.unique()operation to the statement to mitigate duplicate rows.
Can also use with SqlAlchemy ORM get statement:
from sqlalchemy.orm import joinedload
user = await session.get(User, user_id, options=[joinedload(User.user_password)])Option 2: Avoid accessing unloaded relationships
If you know the relationship should be None after creation, you can check the foreign key or avoid accessing the relationship property directly.