Skip to content

Commit a5bc1b1

Browse files
authored
Implement baked query (#659)
* implement baked query
1 parent c4f8ba8 commit a5bc1b1

File tree

12 files changed

+790
-33
lines changed

12 files changed

+790
-33
lines changed

docs/how-to/bakery.rst

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
Bake Queries
2+
============
3+
4+
Baked queries are used to boost execution performance for constantly-used queries.
5+
Similar to the :doc:`orm/extensions/baked` in SQLAlchemy, GINO could also cache the
6+
object’s construction and string-compilation steps. Furthermore, GINO automatically
7+
manages a prepared statement for each baked query in every active connection in the
8+
pool. Executing baked queries is at least 40% faster than running normal queries, but
9+
you need to **bake them before creating the engine**.
10+
11+
GINO provides two approaches for baked queries:
12+
13+
1. Low-level :class:`~gino.bakery.Bakery` API
14+
2. High-level :meth:`Gino.bake() <gino.api.Gino.bake>` integration
15+
16+
17+
Use Bakery with Bare Engine
18+
---------------------------
19+
20+
First, we need a bakery::
21+
22+
import gino
23+
24+
bakery = gino.Bakery()
25+
26+
Then, let's bake some queries::
27+
28+
db_time = bakery.bake("SELECT now()")
29+
30+
Or queries with parameters::
31+
32+
user_query = bakery.bake("SELECT * FROM users WHERE id = :uid")
33+
34+
Let's assume we have this ``users`` table defined in SQLAlchemy Core::
35+
36+
import sqlalchemy as sa
37+
38+
metadata = sa.MetaData()
39+
user_table = sa.Table(
40+
"users", metadata,
41+
sa.Column("id", sa.Integer, primary_key=True),
42+
sa.Column("name", sa.String),
43+
)
44+
45+
Now we can bake a similar query with SQLAlchemy Core::
46+
47+
user_query = bakery.bake(
48+
sa.select([user_table]).where(user.c.id == sa.bindparam("uid"))
49+
)
50+
51+
These baked queries are usually global, and supposed to be shared across the
52+
application. To run them, we need an engine with the bakery::
53+
54+
engine = await gino.create_engine("postgresql://localhost/", bakery=bakery)
55+
56+
By doing so, GINO will bake the queries in the bakery. As new connections are added to
57+
the DB pool, the prepared statements are automatically created behind the scene.
58+
59+
To execute the baked queries, you could treat the :class:`~gino.bakery.BakedQuery`
60+
instances as if they are the queries themselves, for example::
61+
62+
now = await engine.scalar(db_time)
63+
64+
Pass in parameter values::
65+
66+
row = await engine.first(user_query, uid=123)
67+
68+
69+
Use the :class:`~gino.api.Gino` Integration
70+
--------------------------------------------
71+
72+
In a more common scenario, there will be a :class:`~gino.api.Gino>` instance, which has
73+
usually a ``bind`` set - either explicitly or by the Web framework extensions::
74+
75+
from gino import Gino
76+
77+
db = Gino()
78+
79+
async def main():
80+
async with db.with_bind("postgresql://localhost/"):
81+
...
82+
83+
A :class:`~gino.bakery.Bakery` is automatically created in the ``db`` instance, and fed
84+
to the engine implicitly. You can immediately start to bake queries without further
85+
ado::
86+
87+
class User(db.Model):
88+
__tablename__ = "users"
89+
90+
id = db.Column(db.Integer, primary_key=True)
91+
name = db.Column(db.String)
92+
93+
db_time = db.bake("SELECT now()")
94+
user_getter = db.bake(User.query.where(User.id == db.bindparam("uid")))
95+
96+
And the execution is also simplified with the same ``bind`` magic::
97+
98+
async def main():
99+
async with db.with_bind("postgresql://localhost/"):
100+
print(await db_time.scalar())
101+
102+
user: User = await user_getter.first(uid=1)
103+
print(user.name)
104+
105+
To make things more easier, you could even define the baked queries directly on the
106+
model::
107+
108+
class User(db.Model):
109+
__tablename__ = "users"
110+
111+
id = db.Column(db.Integer, primary_key=True)
112+
name = db.Column(db.String)
113+
114+
@db.bake
115+
def getter(cls):
116+
return cls.query.where(cls.id == db.bindparam("uid"))
117+
118+
@classmethod
119+
async def get(cls, uid):
120+
return await cls.getter.one_or_none(uid=uid)
121+
122+
Here GINO treats the ``getter()`` as a :meth:`~gino.declarative.declared_attr` with
123+
``with_table=True``, therefore it takes one positional argument ``cls`` for the ``User``
124+
class.
125+
126+
127+
How to customize loaders?
128+
-------------------------
129+
130+
If possible, you could bake the additional execution options into the query::
131+
132+
user_getter = db.bake(
133+
User.query.where(User.id == db.bindparam("uid")).execution_options(
134+
loader=User.load(comment="Added by loader.")
135+
)
136+
)
137+
138+
The :meth:`~gino.bakery.Bakery.bake` method accepts keyword arguments as execution
139+
options to e.g. simplify the example above into::
140+
141+
user_getter = db.bake(
142+
User.query.where(User.id == db.bindparam("uid")),
143+
loader=User.load(comment="Added by loader."),
144+
)
145+
146+
If the query construction is complex, :meth:`~gino.bakery.Bakery.bake` could also be
147+
used as a decorator::
148+
149+
@db.bake
150+
def user_getter():
151+
return User.query.where(User.id == db.bindparam("uid")).execution_options(
152+
loader=User.load(comment="Added by loader.")
153+
)
154+
155+
Or with short execution options::
156+
157+
@db.bake(loader=User.load(comment="Added by loader."))
158+
def user_getter():
159+
return User.query.where(User.id == db.bindparam("uid"))
160+
161+
Meanwhile, it is also possible to override the loader at runtime::
162+
163+
user: User = await user_getter.load(User).first(uid=1)
164+
print(user.name) # no more comment on user!
165+
166+
.. hint::
167+
168+
This override won't affect the baked query - it's used only in this execution.
169+
170+
171+
What APIs are available on :class:`~gino.bakery.BakedQuery`?
172+
------------------------------------------------------------
173+
174+
:class:`~gino.bakery.BakedQuery` is a :class:`~gino.api.GinoExecutor`, so it inherited
175+
all the APIs like :meth:`~gino.api.GinoExecutor.all`,
176+
:meth:`~gino.api.GinoExecutor.first`, :meth:`~gino.api.GinoExecutor.one`,
177+
:meth:`~gino.api.GinoExecutor.one_or_none`, :meth:`~gino.api.GinoExecutor.scalar`,
178+
:meth:`~gino.api.GinoExecutor.status`, :meth:`~gino.api.GinoExecutor.load`,
179+
:meth:`~gino.api.GinoExecutor.timeout`, etc.
180+
181+
:class:`~gino.api.GinoExecutor` is actually the chained ``.gino`` helper API seen
182+
usually in queries like this::
183+
184+
user = await User.query.where(User.id == 123).gino.first()
185+
186+
So a :class:`~gino.bakery.BakedQuery` can be seen as a normal query with the ``.gino``
187+
suffix, plus it is directly executable.
188+
189+
.. seealso::
190+
191+
Please see API document of :mod:`gino.bakery` for more information.

src/gino/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
from .api import Gino # NOQA
2+
from .bakery import Bakery
23
from .engine import GinoEngine, GinoConnection # NOQA
34
from .exceptions import * # NOQA
45
from .strategies import GinoStrategy # NOQA
56

67

78
def create_engine(*args, **kwargs):
8-
"""Shortcut for :func:`sqlalchemy.create_engine` with ``strategy="gino"``."""
9+
"""
10+
Shortcut for :func:`sqlalchemy.create_engine` with ``strategy="gino"``.
11+
12+
.. versionchanged:: 1.1
13+
Added the ``bakery`` keyword argument, please see :class:`~.bakery.Bakery`.
14+
"""
915

1016
from sqlalchemy import create_engine
1117

src/gino/api.py

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
from sqlalchemy.sql.base import Executable
66
from sqlalchemy.sql.schema import SchemaItem
77

8+
from . import json_support
89
from .crud import CRUDModel
910
from .declarative import declarative_base, declared_attr
1011
from .exceptions import UninitializedError
1112
from .schema import GinoSchemaVisitor, patch_schema
12-
from . import json_support
1313

1414

1515
class GinoExecutor:
@@ -75,8 +75,7 @@ def model(self, model):
7575
"""
7676
if model is not None:
7777
model = weakref.ref(model)
78-
self._query = self._query.execution_options(model=model)
79-
return self
78+
return self.execution_options(model=model)
8079

8180
def return_model(self, switch):
8281
"""
@@ -86,8 +85,7 @@ def return_model(self, switch):
8685
information.
8786
8887
"""
89-
self._query = self._query.execution_options(return_model=switch)
90-
return self
88+
return self.execution_options(return_model=switch)
9189

9290
def timeout(self, timeout):
9391
"""
@@ -97,8 +95,7 @@ def timeout(self, timeout):
9795
information.
9896
9997
"""
100-
self._query = self._query.execution_options(timeout=timeout)
101-
return self
98+
return self.execution_options(timeout=timeout)
10299

103100
def load(self, value):
104101
"""
@@ -113,7 +110,20 @@ def load(self, value):
113110
information.
114111
115112
"""
116-
self._query = self._query.execution_options(loader=value)
113+
return self.execution_options(loader=value)
114+
115+
def execution_options(self, **options):
116+
"""
117+
Set execution options to this query in a chaining call.
118+
119+
Read :meth:`~gino.engine.GinoConnection.execution_options` for more
120+
information.
121+
122+
:param options: Multiple execution options.
123+
124+
.. versionadded:: 1.1
125+
"""
126+
self._query = self._query.execution_options(**options)
117127
return self
118128

119129
async def all(self, *multiparams, **params):
@@ -185,10 +195,7 @@ def iterate(self, *multiparams, **params):
185195
(:class:`Gino`) is bound, while metadata is found in this query.
186196
187197
"""
188-
connection = self._query.bind.current_connection
189-
if connection is None:
190-
raise ValueError("No Connection in context, please provide one")
191-
return connection.iterate(self._query, *multiparams, **params)
198+
return self._query.bind.iterate(self._query, *multiparams, **params)
192199

193200

194201
class _BindContext:
@@ -354,6 +361,9 @@ def __init__(
354361
self._model = declarative_base(self, model_classes)
355362
self.declared_attr = declared_attr
356363
self.quoted_name = sa.sql.quoted_name
364+
from .bakery import Bakery
365+
366+
self._bakery = Bakery()
357367
for mod in json_support, sa:
358368
for key in mod.__all__:
359369
if not hasattr(self, key) and key not in self.no_delegate:
@@ -414,7 +424,7 @@ async def set_bind(self, bind, loop=None, **kwargs):
414424
if isinstance(bind, URL):
415425
from . import create_engine
416426

417-
bind = await create_engine(bind, loop=loop, **kwargs)
427+
bind = await create_engine(bind, loop=loop, bakery=self._bakery, **kwargs)
418428
self.bind = bind
419429
return bind
420430

@@ -429,6 +439,9 @@ def pop_bind(self):
429439
:return: :class:`~.engine.GinoEngine` or ``None`` if self is not bound.
430440
431441
"""
442+
from .bakery import Bakery
443+
444+
self._bakery = Bakery()
432445
bind, self.bind = self.bind, None
433446
return bind
434447

@@ -531,6 +544,23 @@ def transaction(self, *args, **kwargs):
531544
"""
532545
return self.bind.transaction(*args, **kwargs)
533546

547+
def bake(self, func_or_elem=None, **execution_options):
548+
"""
549+
A delegate of :meth:`Bakery.bake() <.bakery.Bakery.bake>`.
550+
551+
.. versionadded:: 1.1
552+
"""
553+
return self._bakery.bake(func_or_elem, metadata=self, **execution_options)
554+
555+
@property
556+
def bakery(self):
557+
"""
558+
The bundled :class:`~.bakery.Bakery` instance.
559+
560+
.. versionadded:: 1.1
561+
"""
562+
return self._bakery
563+
534564

535565
class _PlaceHolder:
536566
__slots__ = "_exception"
@@ -547,3 +577,6 @@ def __setattr__(self, key, value):
547577
if key == "_exception":
548578
return super().__setattr__(key, value)
549579
raise self._exception
580+
581+
def __bool__(self):
582+
return False

0 commit comments

Comments
 (0)