Baked Queries

bakedQuery オブジェクトに対して別の生成パターンを提供します。これは、オブジェクトの構築と文字列のコンパイルのステップをキャッシュすることを可能にします。つまり、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

次に、上記のコードに関するいくつかの所見を示します。:

  1. baked_query オブジェクトは BakedQuery のインスタンスです。このオブジェクトは本質的には実際のorm Query オブジェクトの”ビルダ”ですが、それ自身は*実際の* Query オブジェクトではありません。

  1. 実際の Query オブジェクトは、 Result.all() が呼び出された関数の最後まで構築されません。

  1. baked_query オブジェクトに追加されるステップは、すべてPython関数(通常はラムダ)で表現されます。 bakery() 関数に与えられる最初のラムダは引数として Session を受け取ります。残りのラムダはそれぞれ引数として Query を受け取ります。

  1. 上記のコードでは、アプリケーションが search_for_user() を何度も呼び出す可能性があり、各呼び出し内でまったく新しい BakedQuery オブジェクトを構築しても、すべてのラムダは1回だけ呼び出されます。このクエリがパン屋にキャッシュされている限り、各ラムダは2回目に呼び出されることは ありません

  1. キャッシュは、 lambdaオブジェクト自身 への参照を保存してキャッシュキーを作成することで実現されます。つまり、Pythonインタプリタがこれらの関数にPython内のIDを割り当てるという事実が、連続して実行されるクエリを識別する方法を決定します。 email パラメータが指定された search_for_user() の呼び出しでは、呼び出し可能な lambda q:q.filter(User.email==bindparam('email')) が取得されるキャッシュキーの一部になります。 emailNone の場合、この呼び出し可能なものはキャッシュキーの一部ではありません。

  1. ラムダはすべて一度だけ呼び出されるので、呼び出しによって変化する可能性のある変数が ラムダ内 で参照されないことが重要です。代わりに、これらが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

BakedQuery

A builder object for Query objects.

bakery

Construct a new bakery.

Bakery

Callable which returns a BakedQuery.

function sqlalchemy.ext.baked.bakery(size=200, _size_alert=None)

Construct a new bakery.

Returns:

an instance of Bakery

class sqlalchemy.ext.baked.BakedQuery

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 a BakedQuery 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 this BakedQuery.

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 the BakedQuery up until this point will be pulled from the cache. If True, then the entire Query 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 the Query 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, the Session 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 class Session object, that is assumed to be within the context of an enclosing BakedQuery 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 new BakedQuery with modifications.

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 a Session.

The Result object is where the actual Query object gets created, or retrieved from the cache, against a target Session, 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 the Query.params() and Query.execution_options() methods.

Warning

Result.with_post_criteria() functions are applied to the Query object after the query’s SQL statement object has been retrieved from the cache. Only Query.params() and Query.execution_options() methods should be used.

New in version 1.2.