---------------
Using :term:`2.0 style` querying, the :class:`_asyncio.AsyncSession` class
-provides full ORM functionality. Within the default mode of use, special care
-must be taken to avoid :term:`lazy loading` of ORM relationships and column
-attributes, as below where the :func:`_orm.selectinload` eager loading strategy
-is used to ensure the ``A.bs`` on each ``A`` object is loaded::
+provides full ORM functionality. Within the default mode of use, special care
+must be taken to avoid :term:`lazy loading` or other expired-attribute access
+involving ORM relationships and column attributes; the next
+section :ref:`asyncio_orm_avoid_lazyloads` details this. The example below
+illustrates a complete example including mapper and session configuration::
import asyncio
- from sqlalchemy.ext.asyncio import create_async_engine
+ 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.ext.declarative import declarative_base
+ from sqlalchemy.future import select
+ from sqlalchemy.orm import relationship
+ from sqlalchemy.orm import selectinload
+ from sqlalchemy.orm import sessionmaker
+
+ Base = declarative_base()
+
+
+ class A(Base):
+ __tablename__ = "a"
+
+ id = Column(Integer, primary_key=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 def async_main():
engine = create_async_engine(
- "postgresql+asyncpg://scott:tiger@localhost/test", echo=True,
+ "postgresql+asyncpg://scott:tiger@localhost/test",
+ echo=True,
)
+
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
- async with AsyncSession(engine) as session:
+ # expire_on_commit=False will prevent attributes from being expired
+ # after commit.
+ async_session = sessionmaker(
+ engine, expire_on_commit=False, class_=AsyncSession
+ )
+
+ async with async_session() as session:
async with session.begin():
session.add_all(
[
for a1 in result.scalars():
print(a1)
+ print(f"created at: {a1.create_date}")
for b1 in a1.bs:
print(b1)
await session.commit()
+ # access attribute subsequent to commit; this is what
+ # expire_on_commit=False allows
+ print(a1.data)
+
+
asyncio.run(async_main())
-Above, the :func:`_orm.selectinload` eager loader is employed in order
-to eagerly load the ``A.bs`` collection within the scope of the
-``await session.execute()`` call. If the default loader strategy of
-"lazyload" were left in place, the access of the ``A.bs`` attribute would
-raise an asyncio exception. Using traditional asyncio, the application
-needs to avoid any points at which IO-on-attribute access may occur.
-This also includes that methods such as :meth:`_orm.Session.expire` should be
-avoided in favor of :meth:`_asyncio.AsyncSession.refresh`, and that
-appropriate loader options should be employed for :func:`_orm.deferred`
-columns as well as for :func:`_orm.relationship` constructs.
-The full list of available loaders is documented in the section
-:doc:`/orm/loading_relationships`.
-
-In the example above the :class:`_asyncio.AsyncSession` is instantiated with an
-:class:`_asyncio.AsyncEngine` associated with a particular database URL.
-It is then used in a Python asynchronous context manager (i.e. ``async with:`` statement)
-so that it is automatically closed at the end of the block; this is equivalent
-to calling the :meth:`_asyncio.AsyncSession.close` method.
+In the example above, the :class:`_asyncio.AsyncSession` is instantiated using
+the optional :class:`_orm.sessionmaker` helper, and associated with an
+:class:`_asyncio.AsyncEngine` against particular database URL. It is
+then used in a Python asynchronous context manager (i.e. ``async with:``
+statement) so that it is automatically closed at the end of the block; this is
+equivalent to calling the :meth:`_asyncio.AsyncSession.close` method.
+
+.. _asyncio_orm_avoid_lazyloads:
+
+Preventing Implicit IO when Using AsyncSession
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Using traditional asyncio, the application needs to avoid any points at which
+IO-on-attribute access may occur. Above, the following measures are taken to
+prevent this:
+
+* The :func:`_orm.selectinload` eager loader is employed in order to eagerly
+ load the ``A.bs`` collection within the scope of the
+ ``await session.execute()`` call::
+
+ stmt = select(A).options(selectinload(A.bs))
+
+ ..
+
+ If the default loader strategy of "lazyload" were left in place, the access
+ of the ``A.bs`` attribute would raise an asyncio exception.
+ There are a variety of ORM loader options available, which may be configured
+ at the default mapping level or used on a per-query basis, documented at
+ :ref:`loading_toplevel`.
+
+
+* The :class:`_asyncio.AsyncSession` is configured using
+ :paramref:`_orm.Session.expire_on_commit` set to False, so that we may access
+ attributes on an object subsequent to a call to
+ :meth:`_asyncio.AsyncSession.commit`, as in the line at the end where we
+ access an attribute::
+
+ # 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)
+
+* The :paramref:`_schema.Column.server_default` value on the ``created_at``
+ column will not be refreshed by default after an INSERT; instead, it is
+ normally
+ :ref:`expired so that it can be loaded when needed <orm_server_defaults>`.
+ Similar behavior applies to a column where the
+ :paramref:`_schema.Column.default` parameter is assigned to a SQL expression
+ object. To access this value with asyncio, it has to be refreshed within the
+ flush process, which is achieved by setting the
+ :paramref:`_orm.mapper.eager_defaults` parameter on the mapping::
+
+
+ class A(Base):
+ # ...
+
+ # column with a server_default, or SQL expression default
+ create_date = Column(DateTime, server_default=func.now())
+
+ # add this so that it can be accessed
+ __mapper_args__ = {"eager_defaults": True}
+
+Other guidelines include:
+
+* Methods like :meth:`_asyncio.AsyncSession.expire` should be avoided in favor of
+ :meth:`_asyncio.AsyncSession.refresh`
+
+* Appropriate loader options should be employed for :func:`_orm.deferred`
+ columns, if used at all, in addition to that of :func:`_orm.relationship`
+ constructs as noted above. See :ref:`deferred` for background on
+ deferred column loading.
+
+* The "dynamic" relationship loader strategy described at
+ :ref:`dynamic_relationship` is not compatible with the asyncio approach and
+ cannot be used, unless invoked within the
+ :meth:`_asyncio.AsyncSession.run_sync` method described at
+ :ref:`session_run_sync`.
.. _session_run_sync:
-Adapting ORM Lazy loads to asyncio
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Running Synchronous Methods and Functions under asyncio
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deepalchemy:: This approach is essentially exposing publicly the
mechanism by which SQLAlchemy is able to provide the asyncio interface
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.future import select
from sqlalchemy.orm import relationship
from sqlalchemy.orm import selectinload
+from sqlalchemy.orm import sessionmaker
Base = declarative_base()
id = Column(Integer, primary_key=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"
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
- async with AsyncSession(engine) as session:
+ # expire_on_commit=False will prevent attributes from being expired
+ # after commit.
+ async_session = sessionmaker(
+ engine, expire_on_commit=False, class_=AsyncSession
+ )
+
+ async with async_session() as session:
async with session.begin():
session.add_all(
[
# result is a buffered Result object.
for a1 in result.scalars():
print(a1)
+ print(f"created at: {a1.create_date}")
for b1 in a1.bs:
print(b1)