Baked Queries¶
baked
は Query
オブジェクトに対して別の生成パターンを提供します。これは、オブジェクトの構築と文字列のコンパイルのステップをキャッシュすることを可能にします。つまり、2回以上使用される特定の Query
構築シナリオでは、最初の構築からSQL文字列の生成に至るまで、クエリの構築に関わるすべてのPython関数の呼び出しは、クエリが構築されて実行されるたびではなく、 1回 だけ発生します。
このシステムの理論的根拠は、 SQLが発行される 前に発生するすべてのことに対するPythonインタプリタのオーバーヘッドを大幅に削減することです。”ベイク処理された”システムのキャッシュは、SQL呼び出しを削減したり、データベースからの 結果をキャッシュしたりする ことは ありません 。SQL呼び出しと結果セット自体のキャッシュを示すテクニックは、 Dogpile Caching で利用できます。
Deprecated since version 1.4: SQLAlchemy 1.4と2.0は、 :class`.BakedQuery` システムを必要としない、まったく新しい直接クエリキャッシュシステムを特徴としています。キャッシュは、 SQL Compilation Caching で説明されているシステムを使用して、ユーザがアクションを起こすことなく、すべてのコアクエリとORMクエリに対して透過的にアクティブになります。
Deep Alchemy
SQLAlchemy.ext.baked
拡張は 初心者向けではありません 。これを正しく使用するには、SQLAlchemy、データベースドライバ、バックエンドデータベースがどのように相互作用するかを高度に理解する必要があります。この拡張は、通常は必要とされない非常に特殊な種類の最適化を提供します。上で述べたように、これは クエリをキャッシュせず 、SQL自体の文字列定式化のみをキャッシュします。
Synopsis¶
ベイク処理されたシステムの使用は、特定の一連のクエリオブジェクトのストレージを表す、いわゆる”ベーカリー”を作成することから始まります:
from sqlalchemy.ext import baked
bakery = baked.bakery()
上記の”bakery”は、デフォルトで200要素のLRUキャッシュにキャッシュされたデータを保存しますが、ORMクエリには通常、呼び出されたORMクエリに対して1つのエントリと、SQL文字列に対してデータベースダイアレクトごとに1つのエントリが含まれることに注意してください。
このベーカリーでは、Pythonの一連の呼び出し可能オブジェクト(通常はラムダ)としてその構造を指定することで、 Query
オブジェクトを構築することができます。簡潔に使用するために、これは +=
演算子をオーバーライドして、一般的なクエリの構築は次のようになります。:
from sqlalchemy import bindparam
def search_for_user(session, username, email=None):
baked_query = bakery(lambda session: session.query(User))
baked_query += lambda q: q.filter(User.name == bindparam("username"))
baked_query += lambda q: q.order_by(User.id)
if email:
baked_query += lambda q: q.filter(User.email == bindparam("email"))
result = baked_query(session).params(username=username, email=email).all()
return result
次に、上記のコードに関するいくつかの所見を示します。:
baked_query
オブジェクトはBakedQuery
のインスタンスです。このオブジェクトは本質的には実際のormQuery
オブジェクトの”ビルダ”ですが、それ自身は*実際の*Query
オブジェクトではありません。
実際の
Query
オブジェクトは、Result.all()
が呼び出された関数の最後まで構築されません。
baked_query
オブジェクトに追加されるステップは、すべてPython関数(通常はラムダ)で表現されます。bakery()
関数に与えられる最初のラムダは引数としてSession
を受け取ります。残りのラムダはそれぞれ引数としてQuery
を受け取ります。
上記のコードでは、アプリケーションが
search_for_user()
を何度も呼び出す可能性があり、各呼び出し内でまったく新しいBakedQuery
オブジェクトを構築しても、すべてのラムダは1回だけ呼び出されます。このクエリがパン屋にキャッシュされている限り、各ラムダは2回目に呼び出されることは ありません 。
キャッシュは、 lambdaオブジェクト自身 への参照を保存してキャッシュキーを作成することで実現されます。つまり、Pythonインタプリタがこれらの関数にPython内のIDを割り当てるという事実が、連続して実行されるクエリを識別する方法を決定します。
email
パラメータが指定されたsearch_for_user()
の呼び出しでは、呼び出し可能なlambda q:q.filter(User.email==bindparam('email'))
が取得されるキャッシュキーの一部になります。email
がNone
の場合、この呼び出し可能なものはキャッシュキーの一部ではありません。
ラムダはすべて一度だけ呼び出されるので、呼び出しによって変化する可能性のある変数が ラムダ内 で参照されないことが重要です。代わりに、これらがSQL文字列にバインドされる値であると仮定して、
bindparam()
を使用して名前付きパラメータを構築し、後でResult.params()
を使用して実際の値を適用します。
Performance¶
ベイク処理されたクエリは、おそらく少し奇妙で、少しぎこちなく、少し冗長に見えます。しかし、アプリケーション内で何度も呼び出されるクエリのPythonパフォーマンスの節約は非常に劇的です。 Performance で示されているサンプルスイート short_selects
は、次のような通常のクエリのように、それぞれが1行だけを返すクエリの比較を示しています:
session = Session(bind=engine)
for id_ in random.sample(ids, n):
session.query(Customer).filter(Customer.id == id_).one()
対応する”ベイク処理された”問い合わせと比較して:
bakery = baked.bakery()
s = Session(bind=engine)
for id_ in random.sample(ids, n):
q = bakery(lambda s: s.query(Customer))
q += lambda q: q.filter(Customer.id == bindparam("id"))
q(s).params(id=id_).one()
各ブロックに対して10000回の呼び出しが繰り返された場合のPython関数呼び出し回数の違いは次のとおりです。:
test_baked_query : test a baked query of the full entity.
(10000 iterations); total fn calls 1951294
test_orm_query : test a straight ORM query of the full entity.
(10000 iterations); total fn calls 7900535
強力なノートパソコンでの秒数に関しては、次のようになります。:
test_baked_query : test a baked query of the full entity.
(10000 iterations); total time 2.174126 sec
test_orm_query : test a straight ORM query of the full entity.
(10000 iterations); total time 7.958516 sec
このテストでは、意図的に1行のみを返すクエリを使用していることに注意してください。多くの行を返すクエリの場合、ベイク処理されたクエリのパフォーマンス上の利点は、行のフェッチに費やされる時間に比例して、影響が少なくなります。 ベイク処理されたクエリ機能は、クエリ自体の構築にのみ適用され、結果のフェッチには適用されない ことに注意してください。ベイク処理された機能を使用しても、アプリケーションの速度が大幅に向上する保証はありません。この特定の形式のオーバーヘッドの影響を受けていると測定されたアプリケーションにとって、潜在的に有用な機能にすぎません。
Rationale¶
上記の「ラムダ」アプローチは、より伝統的な「パラメータ化された」アプローチのスーパーセットです。 Query
を一度だけ構築し、それを再利用のために辞書に保存する単純なシステムを構築したいと思ったとします。これは、クエリを構築し、 my_cached_query = query.with_session(None)
を呼び出してその Session
を削除するだけで可能です:
my_simple_cache = {}
def lookup(session, id_argument):
if "my_key" not in my_simple_cache:
query = session.query(Model).filter(Model.id == bindparam("id"))
my_simple_cache["my_key"] = query.with_session(None)
else:
query = my_simple_cache["my_key"].with_session(session)
return query.params(id=id_argument).all()
上記のアプローチでは、パフォーマンス上のメリットはごくわずかです。 Query
を再利用することで、 session.query(Model)
コンストラクタ内でのPythonの作業を節約し、 filter(Model.id == bindparam('id'))
を呼び出すことで、Core式の構築と Query.filter()
への送信をスキップします。ただし、このアプローチでは、 Query.all()
が呼び出されるたびに完全な Select
オブジェクトが再生成され、さらに、このまったく新しい Select
が毎回文字列コンパイルステップに送られます。これは、上記のような単純なケースでは、おそらくオーバーヘッドの約70%になります。
追加のオーバーヘッドを減らすためには、より特殊なロジック、つまり選択オブジェクトの構築とSQLの構築を記憶する何らかの方法が必要です。ウィキのセクション BakedQuery に、この機能の前身となる例がありますが、そのシステムでは、クエリの*構築*をキャッシュしていません。すべてのオーバーヘッドを取り除くためには、クエリの構築とSQLコンパイルの両方をキャッシュする必要があります。この方法でレシピを適応させ、クエリのSQLを事前にコンパイルし、最小限のオーバーヘッドで呼び出すことができる新しいオブジェクトを生成するメソッド .bake()
を作成したとします。この例は次のようになります。:
my_simple_cache = {}
def lookup(session, id_argument):
if "my_key" not in my_simple_cache:
query = session.query(Model).filter(Model.id == bindparam("id"))
my_simple_cache["my_key"] = query.with_session(None).bake()
else:
query = my_simple_cache["my_key"].with_session(session)
return query.params(id=id_argument).all()
上記ではパフォーマンスの状況を修正しましたが、このストリング・キャッシュ・キーはまだ処理する必要があります。
“bakery”アプローチを使用して、”building up lambdas”アプローチよりも珍しいことではなく、単純な”reuse a query”アプローチの単純な改善のように見える方法で、上記を再構成することができます。:
bakery = baked.bakery()
def lookup(session, id_argument):
def create_model_query(session):
return session.query(Model).filter(Model.id == bindparam("id"))
parameterized_query = bakery.bake(create_model_query)
return parameterized_query(session).params(id=id_argument).all()
上記では、”ベイク処理された:システムを、単純な「クエリをキャッシュする」システムと非常によく似た方法で使用しています。ただし、コードの行数が2行少なく、「my_key」のキャッシュキーを作成する必要がなく、また、クエリのコンストラクタからフィルタ呼び出し、 Select
オブジェクトの生成まで、Python呼び出し作業の100%を文字列コンパイルステップにキャッシュするカスタム「ベイク処理」関数と同じ機能も含まれています。
上記のことから、”ルックアップがクエリの構造に関して条件付きの決定を行う必要がある場合はどうなるか?”と自問すると、ここで”ベイク処理”がなぜそのようになっているのかが明らかになることが期待されます。パラメータ化されたクエリが1つの関数だけから構築されるのではなく(これは、ベイク処理された関数が本来機能すると考えられていた方法です)、 任意の数 の関数から構築することができます。条件付きでクエリに追加の句を含める必要がある場合、単純な例を考えてみましょう。:
my_simple_cache = {}
def lookup(session, id_argument, include_frobnizzle=False):
if include_frobnizzle:
cache_key = "my_key_with_frobnizzle"
else:
cache_key = "my_key_without_frobnizzle"
if cache_key not in my_simple_cache:
query = session.query(Model).filter(Model.id == bindparam("id"))
if include_frobnizzle:
query = query.filter(Model.frobnizzle == True)
my_simple_cache[cache_key] = query.with_session(None).bake()
else:
query = my_simple_cache[cache_key].with_session(session)
return query.params(id=id_argument).all()
“単純な”パラメータ化されたシステムでは、”include_frobnizle”フラグが渡されたかどうかを考慮してキャッシュキーを生成する必要があります。このフラグが存在すると、生成されるSQLがまったく異なることになるからです。クエリ構築の複雑さが増すにつれて、これらのクエリをキャッシュするタスクが非常に急速に負担となることは明らかです。上記の例を次のように”bakery”を直接使用するように変換できます。:
bakery = baked.bakery()
def lookup(session, id_argument, include_frobnizzle=False):
def create_model_query(session):
return session.query(Model).filter(Model.id == bindparam("id"))
parameterized_query = bakery.bake(create_model_query)
if include_frobnizzle:
def include_frobnizzle_in_query(query):
return query.filter(Model.frobnizzle == True)
parameterized_query = parameterized_query.with_criteria(
include_frobnizzle_in_query
)
return parameterized_query(session).params(id=id_argument).all()
上記では、クエリオブジェクトだけでなく、SQLを生成するために必要なすべての作業をキャッシュします。また、行ったすべての構造変更を正確に考慮したキャッシュキーを確実に生成する必要もなくなりました。これは自動的に処理され、間違いの可能性もありません。
このコードサンプルは、単純な例よりも数行短く、キャッシュキーを処理する必要がなく、完全ないわゆる”ベイク処理された”機能によるパフォーマンス上の大きな利点があります。しかし、まだ少し冗長です!そのため、 BakedQuery.add_criteria()
や BakedQuery.with_criteria()
のようなメソッドを演算子に短縮し、冗長さを減らす手段としてのみ、単純なラムダを使用することを推奨します(もちろん必須ではありません!):
bakery = baked.bakery()
def lookup(session, id_argument, include_frobnizzle=False):
parameterized_query = bakery.bake(
lambda s: s.query(Model).filter(Model.id == bindparam("id"))
)
if include_frobnizzle:
parameterized_query += lambda q: q.filter(Model.frobnizzle == True)
return parameterized_query(session).params(id=id_argument).all()
上記の場合、このアプローチは実装が簡単で、キャッシュされていないクエリ関数とコードフローが似ているため、コードの移植が容易になる。
上記の説明は、本質的に、現在の”ベイク処理された”アプローチに到達するために使用される設計プロセスの要約である。”通常の”アプローチから始めて、キャッシュキーの構築と管理、すべての冗長なPython実行の削除、および条件付きで構築されたクエリの追加の問題に対処する必要があり、最終的なアプローチにつながりました。
Special Query Techniques¶
このセクションでは、特定のクエリ状況でのいくつかのテクニックについて説明します。
Using IN expressions¶
SQLAlchemyの ColumnOperators.in_()
メソッドは歴史的に、そのメソッドに渡された項目のリストに基づいて、バインドされたパラメータの変数セットを描画します。このリストの長さは呼び出しごとに変わる可能性があるので、これはベイク処理されたクエリでは機能しません。この問題を解決するために、 bindparam.expanding
パラメータは、ベイク処理されたクエリ内に安全にキャッシュできる、後で描画されるIN式をサポートしています。要素の実際のリストは、文のコンパイル時ではなく、文の実行時に描画されます:
bakery = baked.bakery()
baked_query = bakery(lambda session: session.query(User))
baked_query += lambda q: q.filter(User.name.in_(bindparam("username", expanding=True)))
result = baked_query.with_session(session).params(username=["ed", "fred"]).all()
Using Subqueries¶
Query
オブジェクトを使用する場合、 Query
オブジェクトを使用して別のオブジェクト内に副問い合わせを生成する必要があります。 Query
が現在ベイク処理された形式である場合、 BakedQuery.to_query()
メソッドを使用して、中間メソッドを使用して Query
オブジェクトを取得することができます。このメソッドには、ベイク処理されたクエリの特定のステップを生成するために使用されるlambda呼び出し可能オブジェクトへの引数である Session
または Query
が渡されます。:
bakery = baked.bakery()
# a baked query that will end up being used as a subquery
my_subq = bakery(lambda s: s.query(User.id))
my_subq += lambda q: q.filter(User.id == Address.user_id)
# select a correlated subquery in the top columns list,
# we have the "session" argument, pass that
my_q = bakery(lambda s: s.query(Address.id, my_subq.to_query(s).as_scalar()))
# use a correlated subquery in some of the criteria, we have
# the "query" argument, pass that.
my_q += lambda q: q.filter(my_subq.to_query(q).exists())
New in version 1.3.
Using the before_compile event¶
SQLAlchemy 1.3.11では、特定の Query
に対して QueryEvents.before_compile()
イベントを使用すると、渡されたものとは異なる新しい Query
オブジェクトがイベントフックから返された場合、ベイク処理されたクエリシステムがクエリをキャッシュすることができなくなります。これは、特定の Query
が使用されるたびに QueryEvents.before_compile()
フックを呼び出して、毎回異なる方法でクエリを変更するフックに対応できるようにするためです。 QueryEvents.before_compile()
が sqlalchemy.orm.Query()
オブジェクトを変更できるようにしながら、結果をキャッシュできるようにするには、イベントを登録して bake_ok=True
フラグを渡すことができます:
@event.listens_for(Query, "before_compile", retval=True, bake_ok=True)
def my_event(query):
for desc in query.column_descriptions:
if desc["type"] is User:
entity = desc["entity"]
query = query.filter(entity.deleted == False)
return query
上記の方法は、特定のパラメータや変化する外部状態に依存せず、常にまったく同じ方法で与えられた Query
を変更するイベントに適しています。
New in version 1.3.11: - “bake_ok”フラグを QueryEvents.before_compile()
イベントに追加し、このフラグが設定されていない場合に新しい Query
オブジェクトを返すイベントハンドラに対して、”bake”拡張によるキャッシュが発生しないようにしました。
Disabling Baked Queries Session-wide¶
フラグ Session.enable_baked_queries
をFalseに設定すると、ベイク処理されたすべてのクエリが、その Session
に対して使用されたときにキャッシュを使用しなくなります:
session = Session(engine, enable_baked_queries=False)
すべてのセッションフラグと同様に、これは sessionmaker
のようなファクトリオブジェクトや sessionmaker.configure()
のようなメソッドでも受け付けられます。
このフラグの直接的な根拠は、ユーザ定義のベイク処理されたクエリやその他のベイク処理されたクエリの問題によるキャッシュキーの競合に起因する可能性のある問題を検出したアプリケーションが、問題の原因としてベイク処理されたクエリを特定または排除するために、この動作をオフにできるようにすることです。
New in version 1.2.
Lazy Loading Integration¶
Changed in version 1.4: SQLAlchemy 1.4の時点で、”ベイク処理されたクエリ”システムは関係読み込みシステムの一部ではなくなりました。代わりに native caching システムが使用されます。
API Documentation¶
Object Name | Description |
---|---|
A builder object for |
|
Construct a new bakery. |
|
Callable which returns a |
- function sqlalchemy.ext.baked.bakery(size=200, _size_alert=None)¶
Construct a new bakery.
- Returns:
an instance of
Bakery
- class sqlalchemy.ext.baked.BakedQuery¶
Members
add_criteria(), bakery(), for_session(), spoil(), to_query(), with_criteria()
A builder object for
Query
objects.-
method
sqlalchemy.ext.baked.BakedQuery.
add_criteria(fn, *args)¶ Add a criteria function to this
BakedQuery
.This is equivalent to using the
+=
operator to modify aBakedQuery
in-place.
-
classmethod
sqlalchemy.ext.baked.BakedQuery.
bakery(size=200, _size_alert=None)¶ Construct a new bakery.
- Returns:
an instance of
Bakery
-
method
sqlalchemy.ext.baked.BakedQuery.
for_session(session)¶ Return a
Result
object for thisBakedQuery
.This is equivalent to calling the
BakedQuery
as a Python callable, e.g.result = my_baked_query(session)
.
-
method
sqlalchemy.ext.baked.BakedQuery.
spoil(full=False)¶ Cancel any query caching that will occur on this BakedQuery object.
The BakedQuery can continue to be used normally, however additional creational functions will not be cached; they will be called on every invocation.
This is to support the case where a particular step in constructing a baked query disqualifies the query from being cacheable, such as a variant that relies upon some uncacheable value.
- Parameters:
full¶ – if False, only functions added to this
BakedQuery
object subsequent to the spoil step will be non-cached; the state of theBakedQuery
up until this point will be pulled from the cache. If True, then the entireQuery
object is built from scratch each time, with all creational functions being called on each invocation.
-
method
sqlalchemy.ext.baked.BakedQuery.
to_query(query_or_session)¶ Return the
Query
object for use as a subquery.This method should be used within the lambda callable being used to generate a step of an enclosing
BakedQuery
. The parameter should normally be theQuery
object that is passed to the lambda:sub_bq = self.bakery(lambda s: s.query(User.name)) sub_bq += lambda q: q.filter( User.id == Address.user_id).correlate(Address) main_bq = self.bakery(lambda s: s.query(Address)) main_bq += lambda q: q.filter( sub_bq.to_query(q).exists())
In the case where the subquery is used in the first callable against a
Session
, theSession
is also accepted:sub_bq = self.bakery(lambda s: s.query(User.name)) sub_bq += lambda q: q.filter( User.id == Address.user_id).correlate(Address) main_bq = self.bakery( lambda s: s.query( Address.id, sub_bq.to_query(q).scalar_subquery()) )
- Parameters:
query_or_session¶ –
a
Query
object or a classSession
object, that is assumed to be within the context of an enclosingBakedQuery
callable.New in version 1.3.
-
method
sqlalchemy.ext.baked.BakedQuery.
with_criteria(fn, *args)¶ Add a criteria function to a
BakedQuery
cloned from this one.This is equivalent to using the
+
operator to produce a newBakedQuery
with modifications.
-
method
- class sqlalchemy.ext.baked.Bakery¶
Callable which returns a
BakedQuery
.This object is returned by the class method
BakedQuery.bakery()
. It exists as an object so that the “cache” can be easily inspected.New in version 1.2.
- class sqlalchemy.ext.baked.Result
Invokes a
BakedQuery
against aSession
.The
Result
object is where the actualQuery
object gets created, or retrieved from the cache, against a targetSession
, and is then invoked for results.-
method
sqlalchemy.ext.baked.Result.
all() Return all rows.
Equivalent to
Query.all()
.
-
method
sqlalchemy.ext.baked.Result.
count() return the ‘count’.
Equivalent to
Query.count()
.Note this uses a subquery to ensure an accurate count regardless of the structure of the original statement.
-
method
sqlalchemy.ext.baked.Result.
first() Return the first row.
Equivalent to
Query.first()
.
-
method
sqlalchemy.ext.baked.Result.
get(ident) Retrieve an object based on identity.
Equivalent to
Query.get()
.
-
method
sqlalchemy.ext.baked.Result.
one() Return exactly one result or raise an exception.
Equivalent to
Query.one()
.
-
method
sqlalchemy.ext.baked.Result.
one_or_none() Return one or zero results, or raise an exception for multiple rows.
Equivalent to
Query.one_or_none()
.
-
method
sqlalchemy.ext.baked.Result.
params(*args, **kw) Specify parameters to be replaced into the string SQL statement.
-
method
sqlalchemy.ext.baked.Result.
scalar() Return the first element of the first result or None if no rows present. If multiple rows are returned, raises MultipleResultsFound.
Equivalent to
Query.scalar()
.
-
method
sqlalchemy.ext.baked.Result.
with_post_criteria(fn) Add a criteria function that will be applied post-cache.
This adds a function that will be run against the
Query
object after it is retrieved from the cache. This currently includes only theQuery.params()
andQuery.execution_options()
methods.Warning
Result.with_post_criteria()
functions are applied to theQuery
object after the query’s SQL statement object has been retrieved from the cache. OnlyQuery.params()
andQuery.execution_options()
methods should be used.New in version 1.2.
-
method