Skip to content

Commit 7f09156

Browse files
committed
add lazy baked prepared statement
1 parent 42ac8f7 commit 7f09156

File tree

7 files changed

+75
-43
lines changed

7 files changed

+75
-43
lines changed

docs/how-to/bakery.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,24 @@ suffix, plus it is directly executable.
191191
.. seealso::
192192

193193
Please see API document of :mod:`gino.bakery` for more information.
194+
195+
196+
I don't want the prepared statements.
197+
-------------------------------------
198+
199+
If you don't need all the baked queries (``m``) to create prepared statements for all
200+
the active database connections (``n``) in the beginning, you could set
201+
``prebake=False`` in the engine initialization to prevent the default initial
202+
``m x n`` prepare calls::
203+
204+
e = await gino.create_engine("postgresql://...", bakery=bakery, prebake=False)
205+
206+
Or if you're using bind::
207+
208+
await db.set_bind("postgresql://...", prebake=False)
209+
210+
This is useful when you're depending on ``db.gino.create_all()`` to create the tables,
211+
because the prepared statements can only be created after the table creation.
212+
213+
The prepared statements will then be created and cached lazily on demand.
214+

src/gino/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ def create_engine(*args, **kwargs):
1111
1212
.. versionchanged:: 1.1
1313
Added the ``bakery`` keyword argument, please see :class:`~.bakery.Bakery`.
14+
15+
.. versionchanged:: 1.1
16+
Added the ``prebake`` keyword argument to choose when to create the prepared
17+
statements for the queries in the bakery:
18+
19+
* **Pre-bake** immediately when connected to the database (default).
20+
* No **pre-bake** but create prepared statements lazily when needed for the first
21+
time.
1422
"""
1523

1624
from sqlalchemy import create_engine

src/gino/declarative.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -168,17 +168,17 @@ def __table_args__(cls):
168168
169169
Added ``with_table`` parameter which works after the ``__table__`` is created::
170170
171-
class User(db.Model):
172-
__tablename__ = "users"
171+
class User(db.Model):
172+
__tablename__ = "users"
173173
174-
...
174+
...
175175
176-
@db.declared_attr(with_table=True)
177-
def table_name(cls):
178-
# this is called only once when defining the class
179-
return cls.__table__.name
176+
@db.declared_attr(with_table=True)
177+
def table_name(cls):
178+
# this is called only once when defining the class
179+
return cls.__table__.name
180180
181-
assert User.table_name == "users"
181+
assert User.table_name == "users"
182182
183183
"""
184184
if m is None:

src/gino/dialects/asyncpg.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,19 @@ def executor(state, timeout_):
190190
async def execute_baked(self, baked_query, timeout, args, one):
191191
conn, timeout = await self._acquire(timeout)
192192
stmt = conn.baked_queries.get(baked_query)
193-
if not stmt:
194-
raise RuntimeError("query is not baked yet")
195-
196-
# work around prepared statement limit per connection acquisition
197-
# https://github.com/MagicStack/asyncpg/issues/190
198-
stmt._con_release_ctr = conn._pool_release_ctr
193+
if stmt:
194+
# work around prepared statement limit per connection acquisition
195+
# https://github.com/MagicStack/asyncpg/issues/190
196+
stmt._con_release_ctr = conn._pool_release_ctr
197+
else:
198+
if timeout:
199+
before = time.monotonic()
200+
stmt = await conn.prepare(baked_query.sql, timeout=timeout)
201+
after = time.monotonic()
202+
timeout -= after - before
203+
else:
204+
stmt = await conn.prepare(baked_query.sql)
205+
conn.baked_queries[baked_query] = stmt
199206

200207
if one:
201208
rv = await stmt.fetchrow(*args, timeout=timeout)
@@ -218,17 +225,17 @@ def get_statusmsg(self):
218225

219226

220227
class Pool(base.Pool):
221-
def __init__(self, url, loop, bakery=None, **kwargs):
228+
def __init__(self, url, loop, bakery=None, prebake=True, **kwargs):
222229
self._url = url
223230
self._loop = loop
224231
self._kwargs = kwargs
225232
self._pool = None
226233
self._bakery = bakery
227234
self._init_hook = None
235+
self._prebake = prebake
228236

229237
async def _init(self):
230238
args = self._kwargs.copy()
231-
self._init_hook = args.pop("init", None)
232239

233240
class Connection(args.pop("connection_class", asyncpg.Connection)):
234241
__slots__ = ("baked_queries",)
@@ -244,9 +251,11 @@ def __init__(self, *pargs, **kwargs):
244251
user=self._url.username,
245252
database=self._url.database,
246253
password=self._url.password,
247-
init=self._bake,
248254
connection_class=Connection,
249255
)
256+
if self._prebake and self._bakery:
257+
self._init_hook = args.pop("init", None)
258+
args["init"] = self._bake
250259
self._pool = await asyncpg.create_pool(**args)
251260
return self
252261

@@ -474,10 +483,11 @@ class AsyncpgDialect(PGDialect, base.AsyncDialectMixin):
474483
cursor_cls = DBAPICursor
475484
init_kwargs = set(
476485
itertools.chain(
486+
("bakery", "prebake"),
477487
*[
478488
inspect.getfullargspec(f).kwonlydefaults.keys()
479489
for f in [asyncpg.create_pool, asyncpg.connect]
480-
]
490+
],
481491
)
482492
)
483493
colspecs = util.update_copy(
@@ -490,7 +500,7 @@ class AsyncpgDialect(PGDialect, base.AsyncDialectMixin):
490500
},
491501
)
492502

493-
def __init__(self, *args, **kwargs):
503+
def __init__(self, *args, bakery=None, **kwargs):
494504
self._pool_kwargs = {}
495505
self._init_hook = None
496506
for k in self.init_kwargs:
@@ -500,7 +510,7 @@ def __init__(self, *args, **kwargs):
500510
else:
501511
self._pool_kwargs[k] = kwargs.pop(k)
502512
super().__init__(*args, **kwargs)
503-
self._init_mixin()
513+
self._init_mixin(bakery)
504514

505515
async def init_pool(self, url, loop, pool_class=None):
506516
if pool_class is None:

src/gino/dialects/base.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -411,10 +411,20 @@ class AsyncDialectMixin:
411411
dbapi_class = BaseDBAPI
412412
_bakery = None
413413

414-
def _init_mixin(self):
414+
def _init_mixin(self, bakery):
415415
self._sa_conn = _SAConnection(
416416
_SAEngine(self), _DBAPIConnection(self.cursor_cls)
417417
)
418+
if bakery:
419+
if bakery._closed:
420+
raise InitializedError("Cannot reuse a closed bakery!")
421+
self._bakery = bakery
422+
for bq in bakery:
423+
conn = self._sa_conn.execution_options(compiled_cache=bq)
424+
context = conn.execute(bq.query, _bypass_no_param).context
425+
# noinspection PyProtectedMember
426+
bq._set_sql(context.statement)
427+
bakery._closed = True
418428

419429
@classmethod
420430
def dbapi(cls):
@@ -432,14 +442,3 @@ async def init_pool(self, url, loop):
432442

433443
def transaction(self, raw_conn, args, kwargs):
434444
raise NotImplementedError
435-
436-
def set_bakery(self, bakery):
437-
if bakery._closed:
438-
raise InitializedError("Cannot reuse a closed bakery!")
439-
self._bakery = bakery
440-
for bq in bakery:
441-
conn = self._sa_conn.execution_options(compiled_cache=bq)
442-
context = conn.execute(bq.query, _bypass_no_param).context
443-
# noinspection PyProtectedMember
444-
bq._set_sql(context.statement)
445-
bakery._closed = True

src/gino/strategies.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,6 @@ async def create(self, name_or_url, loop=None, **kwargs):
5151
dialect_args["dbapi"] = dbapi
5252

5353
dialect = dialect_cls(**dialect_args)
54-
55-
bakery = kwargs.pop("bakery", None)
56-
if bakery:
57-
dialect.set_bakery(bakery)
58-
5954
pool_class = kwargs.pop("pool_class", None)
6055
pool = await dialect.init_pool(u, loop, pool_class=pool_class)
6156

tests/test_bakery.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,11 @@ class BakeOnClass(db.Model):
173173
def getter(cls):
174174
return cls.query.where(cls.name == db.bindparam("name"))
175175

176-
e = sqlalchemy.create_engine(PG_URL)
177-
db.create_all(e)
178-
try:
179-
async with db.with_bind(PG_URL):
176+
async with db.with_bind(PG_URL, prebake=False):
177+
await db.gino.create_all()
178+
try:
180179
await BakeOnClass.create(name="exist")
181180
assert (await BakeOnClass.getter.one(name="exist")).name == "exist"
182181
assert (await BakeOnClass.getter.one_or_none(name="nonexist")) is None
183-
finally:
184-
db.drop_all(e)
182+
finally:
183+
await db.gino.drop_all()

0 commit comments

Comments
 (0)