@@ -85,25 +85,69 @@ Synopsis - ORM
85
85
---------------
86
86
87
87
Using :term: `2.0 style ` querying, the :class: `_asyncio.AsyncSession ` class
88
- provides full ORM functionality. Within the default mode of use, special care
89
- must be taken to avoid :term: `lazy loading ` of ORM relationships and column
90
- attributes, as below where the :func: `_orm.selectinload ` eager loading strategy
91
- is used to ensure the ``A.bs `` on each ``A `` object is loaded::
88
+ provides full ORM functionality. Within the default mode of use, special care
89
+ must be taken to avoid :term: `lazy loading ` or other expired-attribute access
90
+ involving ORM relationships and column attributes; the next
91
+ section :ref: `asyncio_orm_avoid_lazyloads ` details this. The example below
92
+ illustrates a complete example including mapper and session configuration::
92
93
93
94
import asyncio
94
95
95
- from sqlalchemy.ext.asyncio import create_async_engine
96
+ from sqlalchemy import Column
97
+ from sqlalchemy import DateTime
98
+ from sqlalchemy import ForeignKey
99
+ from sqlalchemy import func
100
+ from sqlalchemy import Integer
101
+ from sqlalchemy import String
96
102
from sqlalchemy.ext.asyncio import AsyncSession
103
+ from sqlalchemy.ext.asyncio import create_async_engine
104
+ from sqlalchemy.ext.declarative import declarative_base
105
+ from sqlalchemy.future import select
106
+ from sqlalchemy.orm import relationship
107
+ from sqlalchemy.orm import selectinload
108
+ from sqlalchemy.orm import sessionmaker
109
+
110
+ Base = declarative_base()
111
+
112
+
113
+ class A(Base):
114
+ __tablename__ = "a"
115
+
116
+ id = Column(Integer, primary_key=True)
117
+ data = Column(String)
118
+ create_date = Column(DateTime, server_default=func.now())
119
+ bs = relationship("B")
120
+
121
+ # required in order to access columns with server defaults
122
+ # or SQL expression defaults, subsequent to a flush, without
123
+ # triggering an expired load
124
+ __mapper_args__ = {"eager_defaults": True}
125
+
126
+
127
+ class B(Base):
128
+ __tablename__ = "b"
129
+ id = Column(Integer, primary_key=True)
130
+ a_id = Column(ForeignKey("a.id"))
131
+ data = Column(String)
132
+
97
133
98
134
async def async_main():
99
135
engine = create_async_engine(
100
- "postgresql+asyncpg://scott:tiger@localhost/test", echo=True,
136
+ "postgresql+asyncpg://scott:tiger@localhost/test",
137
+ echo=True,
101
138
)
139
+
102
140
async with engine.begin() as conn:
103
141
await conn.run_sync(Base.metadata.drop_all)
104
142
await conn.run_sync(Base.metadata.create_all)
105
143
106
- async with AsyncSession(engine) as session:
144
+ # expire_on_commit=False will prevent attributes from being expired
145
+ # after commit.
146
+ async_session = sessionmaker(
147
+ engine, expire_on_commit=False, class_=AsyncSession
148
+ )
149
+
150
+ async with async_session() as session:
107
151
async with session.begin():
108
152
session.add_all(
109
153
[
@@ -119,6 +163,7 @@ is used to ensure the ``A.bs`` on each ``A`` object is loaded::
119
163
120
164
for a1 in result.scalars():
121
165
print(a1)
166
+ print(f"created at: {a1.create_date}")
122
167
for b1 in a1.bs:
123
168
print(b1)
124
169
@@ -130,31 +175,111 @@ is used to ensure the ``A.bs`` on each ``A`` object is loaded::
130
175
131
176
await session.commit()
132
177
178
+ # access attribute subsequent to commit; this is what
179
+ # expire_on_commit=False allows
180
+ print(a1.data)
181
+
182
+
133
183
asyncio.run(async_main())
134
184
135
- Above, the :func: `_orm.selectinload ` eager loader is employed in order
136
- to eagerly load the ``A.bs `` collection within the scope of the
137
- ``await session.execute() `` call. If the default loader strategy of
138
- "lazyload" were left in place, the access of the ``A.bs `` attribute would
139
- raise an asyncio exception. Using traditional asyncio, the application
140
- needs to avoid any points at which IO-on-attribute access may occur.
141
- This also includes that methods such as :meth: `_orm.Session.expire ` should be
142
- avoided in favor of :meth: `_asyncio.AsyncSession.refresh `, and that
143
- appropriate loader options should be employed for :func: `_orm.deferred `
144
- columns as well as for :func: `_orm.relationship ` constructs.
145
- The full list of available loaders is documented in the section
146
- :doc: `/orm/loading_relationships `.
147
-
148
- In the example above the :class: `_asyncio.AsyncSession ` is instantiated with an
149
- :class: `_asyncio.AsyncEngine ` associated with a particular database URL.
150
- It is then used in a Python asynchronous context manager (i.e. ``async with: `` statement)
151
- so that it is automatically closed at the end of the block; this is equivalent
152
- to calling the :meth: `_asyncio.AsyncSession.close ` method.
185
+ In the example above, the :class: `_asyncio.AsyncSession ` is instantiated using
186
+ the optional :class: `_orm.sessionmaker ` helper, and associated with an
187
+ :class: `_asyncio.AsyncEngine ` against particular database URL. It is
188
+ then used in a Python asynchronous context manager (i.e. ``async with: ``
189
+ statement) so that it is automatically closed at the end of the block; this is
190
+ equivalent to calling the :meth: `_asyncio.AsyncSession.close ` method.
191
+
192
+ .. _asyncio_orm_avoid_lazyloads :
193
+
194
+ Preventing Implicit IO when Using AsyncSession
195
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
196
+
197
+ Using traditional asyncio, the application needs to avoid any points at which
198
+ IO-on-attribute access may occur. Above, the following measures are taken to
199
+ prevent this:
200
+
201
+ * The :func: `_orm.selectinload ` eager loader is employed in order to eagerly
202
+ load the ``A.bs `` collection within the scope of the
203
+ ``await session.execute() `` call::
204
+
205
+ stmt = select(A).options(selectinload(A.bs))
206
+
207
+ ..
208
+
209
+ If the default loader strategy of "lazyload" were left in place, the access
210
+ of the ``A.bs `` attribute would raise an asyncio exception.
211
+ There are a variety of ORM loader options available, which may be configured
212
+ at the default mapping level or used on a per-query basis, documented at
213
+ :ref: `loading_toplevel `.
214
+
215
+
216
+ * The :class: `_asyncio.AsyncSession ` is configured using
217
+ :paramref: `_orm.Session.expire_on_commit ` set to False, so that we may access
218
+ attributes on an object subsequent to a call to
219
+ :meth: `_asyncio.AsyncSession.commit `, as in the line at the end where we
220
+ access an attribute::
221
+
222
+ # create AsyncSession with expire_on_commit=False
223
+ async_session = AsyncSession(engine, expire_on_commit=False)
224
+
225
+ # sessionmaker version
226
+ async_session = sessionmaker(
227
+ engine, expire_on_commit=False, class_=AsyncSession
228
+ )
229
+
230
+ async with async_session() as session:
231
+
232
+ result = await session.execute(select(A).order_by(A.id))
233
+
234
+ a1 = result.scalars().first()
235
+
236
+ # commit would normally expire all attributes
237
+ await session.commit()
238
+
239
+ # access attribute subsequent to commit; this is what
240
+ # expire_on_commit=False allows
241
+ print(a1.data)
242
+
243
+ * The :paramref: `_schema.Column.server_default ` value on the ``created_at ``
244
+ column will not be refreshed by default after an INSERT; instead, it is
245
+ normally
246
+ :ref: `expired so that it can be loaded when needed <orm_server_defaults >`.
247
+ Similar behavior applies to a column where the
248
+ :paramref: `_schema.Column.default ` parameter is assigned to a SQL expression
249
+ object. To access this value with asyncio, it has to be refreshed within the
250
+ flush process, which is achieved by setting the
251
+ :paramref: `_orm.mapper.eager_defaults ` parameter on the mapping::
252
+
253
+
254
+ class A(Base):
255
+ # ...
256
+
257
+ # column with a server_default, or SQL expression default
258
+ create_date = Column(DateTime, server_default=func.now())
259
+
260
+ # add this so that it can be accessed
261
+ __mapper_args__ = {"eager_defaults": True}
262
+
263
+ Other guidelines include:
264
+
265
+ * Methods like :meth: `_asyncio.AsyncSession.expire ` should be avoided in favor of
266
+ :meth: `_asyncio.AsyncSession.refresh `
267
+
268
+ * Appropriate loader options should be employed for :func: `_orm.deferred `
269
+ columns, if used at all, in addition to that of :func: `_orm.relationship `
270
+ constructs as noted above. See :ref: `deferred ` for background on
271
+ deferred column loading.
272
+
273
+ * The "dynamic" relationship loader strategy described at
274
+ :ref: `dynamic_relationship ` is not compatible with the asyncio approach and
275
+ cannot be used, unless invoked within the
276
+ :meth: `_asyncio.AsyncSession.run_sync ` method described at
277
+ :ref: `session_run_sync `.
153
278
154
279
.. _session_run_sync :
155
280
156
- Adapting ORM Lazy loads to asyncio
157
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
281
+ Running Synchronous Methods and Functions under asyncio
282
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
158
283
159
284
.. deepalchemy :: This approach is essentially exposing publicly the
160
285
mechanism by which SQLAlchemy is able to provide the asyncio interface
0 commit comments