how to access relationships with async sqlalchemy?
Question:
import asyncio
from sqlalchemy import Column
from sqlalchemy import DateTime
from sqlalchemy import ForeignKey
from sqlalchemy import func
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.future import select
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import selectinload
from sqlalchemy.orm import sessionmaker
engine = create_async_engine(
"postgresql+asyncpg://user:pass@localhost/db",
echo=True,
)
# expire_on_commit=False will prevent attributes from being expired
# after commit.
async_session = sessionmaker(
engine, expire_on_commit=False, class_=AsyncSession
)
Base = declarative_base()
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
name = Column(String, unique=True)
data = Column(String)
create_date = Column(DateTime, server_default=func.now())
bs = relationship("B")
# required in order to access columns with server defaults
# or SQL expression defaults, subsequent to a flush, without
# triggering an expired load
__mapper_args__ = {"eager_defaults": True}
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
data = Column(String)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
async with async_session() as session:
async with session.begin():
session.add_all(
[
A(bs=[B(), B()], data="a1"),
A(bs=[B()], data="a2"),
]
)
async with async_session() as session:
result = await session.execute(select(A).order_by(A.id))
a1 = result.scalars().first()
# no issue:
print(a1.name, a1.data)
# throws error:
print(a1.bs)
Trying to access a1.bs
gives this error:
59 current = greenlet.getcurrent()
60 if not isinstance(current, _AsyncIoGreenlet):
---> 61 raise exc.MissingGreenlet(
62 "greenlet_spawn has not been called; can't call await_() here. "
63 "Was IO attempted in an unexpected place?"
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)
Answers:
This is how:
from sqlalchemy.orm import selectinload
async with async_session() as session:
result = await session.execute(select(A).order_by(A.id)
.options(selectinload(A.bs)))
a = result.scalars().first()
print(a.bs)
key is using the selectinload
method to prevent implicit IO
UPDATE
There are a few alternatives to selectinload
like joinedload
, lazyload
. I am still trying to understand the differences.
From docs, there are 2 keys:
1. Use selectinload
stmt = select(A).options(selectinload(A.bs))
2. Set expire_on_commit=False when creating async session
# create AsyncSession with expire_on_commit=False
async_session = AsyncSession(engine, expire_on_commit=False)
# sessionmaker version
async_session = sessionmaker(
engine, expire_on_commit=False, class_=AsyncSession
)
async with async_session() as session:
result = await session.execute(select(A).order_by(A.id))
a1 = result.scalars().first()
# commit would normally expire all attributes
await session.commit()
# access attribute subsequent to commit; this is what
# expire_on_commit=False allows
print(a1.data)
muon’s answer is correct if you want eager loading (which is better).
But if for some reason you already have loaded your model and later want to load a relationship, there is a way starting with SQLAlchemy 2.0.4:
Using session.refresh
, you can tell it to load a1.bs
:
await session.refresh(a1, attribute_names=["bs"])
print(a1.bs) # This works
From the docs:
New in version 2.0.4: Added support for AsyncSession.refresh()
and the underlying Session.refresh()
method to force lazy-loaded relationships to load, if they are named explicitly in the Session.refresh.attribute_names
parameter.
import asyncio
from sqlalchemy import Column
from sqlalchemy import DateTime
from sqlalchemy import ForeignKey
from sqlalchemy import func
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.future import select
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import selectinload
from sqlalchemy.orm import sessionmaker
engine = create_async_engine(
"postgresql+asyncpg://user:pass@localhost/db",
echo=True,
)
# expire_on_commit=False will prevent attributes from being expired
# after commit.
async_session = sessionmaker(
engine, expire_on_commit=False, class_=AsyncSession
)
Base = declarative_base()
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
name = Column(String, unique=True)
data = Column(String)
create_date = Column(DateTime, server_default=func.now())
bs = relationship("B")
# required in order to access columns with server defaults
# or SQL expression defaults, subsequent to a flush, without
# triggering an expired load
__mapper_args__ = {"eager_defaults": True}
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
data = Column(String)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
async with async_session() as session:
async with session.begin():
session.add_all(
[
A(bs=[B(), B()], data="a1"),
A(bs=[B()], data="a2"),
]
)
async with async_session() as session:
result = await session.execute(select(A).order_by(A.id))
a1 = result.scalars().first()
# no issue:
print(a1.name, a1.data)
# throws error:
print(a1.bs)
Trying to access a1.bs
gives this error:
59 current = greenlet.getcurrent()
60 if not isinstance(current, _AsyncIoGreenlet):
---> 61 raise exc.MissingGreenlet(
62 "greenlet_spawn has not been called; can't call await_() here. "
63 "Was IO attempted in an unexpected place?"
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)
This is how:
from sqlalchemy.orm import selectinload
async with async_session() as session:
result = await session.execute(select(A).order_by(A.id)
.options(selectinload(A.bs)))
a = result.scalars().first()
print(a.bs)
key is using the selectinload
method to prevent implicit IO
UPDATE
There are a few alternatives to selectinload
like joinedload
, lazyload
. I am still trying to understand the differences.
From docs, there are 2 keys:
1. Use selectinload
stmt = select(A).options(selectinload(A.bs))
2. Set expire_on_commit=False when creating async session
# create AsyncSession with expire_on_commit=False
async_session = AsyncSession(engine, expire_on_commit=False)
# sessionmaker version
async_session = sessionmaker(
engine, expire_on_commit=False, class_=AsyncSession
)
async with async_session() as session:
result = await session.execute(select(A).order_by(A.id))
a1 = result.scalars().first()
# commit would normally expire all attributes
await session.commit()
# access attribute subsequent to commit; this is what
# expire_on_commit=False allows
print(a1.data)
muon’s answer is correct if you want eager loading (which is better).
But if for some reason you already have loaded your model and later want to load a relationship, there is a way starting with SQLAlchemy 2.0.4:
Using session.refresh
, you can tell it to load a1.bs
:
await session.refresh(a1, attribute_names=["bs"])
print(a1.bs) # This works
From the docs:
New in version 2.0.4: Added support for
AsyncSession.refresh()
and the underlyingSession.refresh()
method to force lazy-loaded relationships to load, if they are named explicitly in theSession.refresh.attribute_names
parameter.