烘焙查询


bakedQuery 对象,允许缓存对象的构造和字符串编译步骤。这意味着,对于多次使用的特定 Query 构建场景,从初始构建到生成 SQL 字符串,构建查询所涉及的所有 Python 函数调用将只发生一次,而不是在每次构建和执行该查询时发生。


该系统的基本原理是大大减少 SQL 发出之前发生的所有事情的 Python 解释器开销。“baked” 系统的缓存不会以任何方式减少 SQL 调用或缓存来自数据库的返回结果。 一种演示 SQL 调用和结果集本身的缓存在 Dogpile 缓存


1.4 版后已移除: SQLAlchemy 1.4 和 2.0 具有全新的直接查询缓存系统,无需 BakedQuery 系统。现在,缓存对所有 Core 和 ORM 查询都是透明的,用户无需执行任何作,使用 SQL 编译缓存中描述的系统。


深度炼金术


sqlalchemy.ext.baked 扩展不适合初学者。正确使用它需要对 SQLAlchemy、数据库驱动程序和后端数据库如何相互交互有很好的高度理解。此扩展提供了一种通常不需要的非常特殊的优化类型。如上所述,它不缓存查询,只缓存 SQL 本身的字符串表达式。


剧情简介


使用烘焙系统首先生成所谓的 “面包店”,它表示特定系列查询对象的存储:

from sqlalchemy.ext import baked

bakery = baked.bakery()


上面的 “bakery” 将缓存数据存储在 LRU 缓存中,默认为 200 个元素,请注意,ORM 查询通常包含调用的 ORM 查询的一个条目,以及 SQL 字符串的每个数据库方言的一个条目。


面包店允许我们通过将 Query 对象的构造指定为一系列 Python 可调用对象(通常是 lambda)来构建 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 的 “builder” object,但它本身并不是实际Query 对象。


  2. 实际的 Query 对象根本不会构建,直到函数的末尾调用 Result.all() 时。


  3. 添加到 baked_query 对象的步骤都表示为 Python 函数,通常为 lambda。提供给 bakery() 函数的第一个 lambda 接收一个 Session 作为其参数。其余 lambda 每个 lambda 都会收到一个 Query 作为他们的论点。


  4. 在上面的代码中,即使我们的应用程序可能会调用 search_for_user() 多次调用,尽管在每次调用中我们都会构建一个全新的 BakedQuery 对象 所有 lambda 都只调用一次。只要此查询缓存在 Bakery 中,就永远不会再次调用每个 lambda。


  5. 缓存是通过存储对 lambda 对象的引用来实现的 自己来制定缓存键;也就是说,Python 解释器为这些函数分配 Python 内标识这一事实决定了如何在连续运行中识别查询。对于指定了 email 参数的 search_for_user() 调用,可调用的 lambda q: q.filter(User.email == bindparam('email')) 将是检索的缓存键的一部分;当电子邮件None,则此 callable 不是 cache key 的一部分。


  6. 由于 lambda 都只调用一次,因此在 lambda 中不得引用可能在调用之间更改的变量;相反,假设这些是要绑定到 SQL 字符串中的值,我们使用 bindparam() 来构造命名参数,稍后使用 Result.params() 应用它们的实际值。


性能


烘焙的查询可能看起来有点奇怪、有点尴尬和有点冗长。但是,对于在应用程序中多次调用的查询,Python 性能的节省非常显著。示例套件 short_selects 性能 中演示了每个仅返回一行的查询的比较,例如以下常规查询:

session = Session(bind=engine)
for id_ in random.sample(ids, n):
    session.query(Customer).filter(Customer.id == id_).one()


与等效的 “baked” 查询相比:

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


请注意,此测试非常有意地包含仅返回一行的查询。对于返回许多行的查询,烘焙查询的性能优势的影响将越来越小,这与获取行所花费的时间成正比。请务必记住,烘焙查询功能仅适用于 构建查询本身,而不是获取结果。使用烘焙功能绝不能保证应用程序更快;它仅对于那些被测量为受这种特定形式的开销影响的应用程序来说,才是一个可能有用的功能。


基本原理


上面的 “lambda” 方法是更传统的 “参数化” 方法的超集。假设我们希望构建一个简单的系统,我们只构建一次 Query,然后将其存储在字典中以供重用。现在,只需构建查询,然后通过调用 my_cached_query = query.with_session(None) :

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()。但是,该方法仍会重新生成完整的 Select object 每次调用 Query.all() 时,此外,这个全新的 Select 每次都会被发送到字符串编译步骤,对于像上面这样简单的情况,这可能大约是开销的 70%。


为了减少额外的开销,我们需要一些更专业的 logic,某种方法来记住 select 对象的构造和 SQL 的构造。在 wiki 的 BakedQuery 部分有一个示例,它是此功能的前身,但是在该系统中,我们没有缓存查询的构造。为了消除所有开销,我们需要缓存查询的构造以及 SQL 编译。假设我们以这种方式调整了配方,并为自己制作了一个 .bake() 方法,该方法为查询预编译 SQL,从而生成一个可以以最小开销调用的新对象。我们的示例变为:

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()


上面,我们已经修复了性能问题,但我们仍然需要处理这个字符串缓存键。


我们可以使用 “面包店” 方法以一种看起来不如 “构建 lambdas” 方法不寻常的方式重新构建上述内容,而更像是对简单的 “重用查询” 方法的简单改进:

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()


在上面,我们以一种与简单的 “cache a query” 系统非常相似的方式使用 “baked” 系统。但是,它使用的代码少了两行,不需要制造 “my_key” 的缓存键,并且还包括与我们的自定义 “bake” 函数相同的功能,该函数缓存 100% 的 Python 调用工作,从查询的构造函数到过滤器调用,到 Select 对象的生成,再到字符串编译步骤。


从上面,如果我们问自己,“如果 lookup 需要对查询的结构做出有条件的决定怎么办?”,希望这就是为什么 “baked” 是这样的地方。我们可以从任意数量的函数构建参数化查询,而不是只从一个函数构建参数化查询(这就是我们认为 baked 最初的工作方式)。考虑我们的简单示例,如果我们需要在查询中有一个额外的子句作为条件:

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()


我们的 “simple” 参数化系统现在必须负责生成缓存键,该键会考虑是否传递了 “include_frobnizzle” 标志,因为此标志的存在意味着生成的 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()


在上面,我们不仅缓存了 query 对象,还缓存了生成 SQL 需要做的所有工作。我们也不再需要处理确保我们生成的缓存键准确考虑了我们所做的所有结构修改;现在,这是自动处理的,不会出错。


此代码示例比简单的示例短了几行,无需处理缓存键,并且具有完整的所谓 “baked” 功能的巨大性能优势。但还是有点啰嗦!因此,我们采用 BakedQuery.add_criteria()BakedQuery.with_criteria() 并将它们缩短为运算符,并鼓励(尽管肯定不是必需的)使用简单的 lambda,仅作为减少冗长的一种手段:

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()


在上面,该方法更易于实现,并且在代码流中与非缓存查询函数的外观更相似,因此使代码更易于移植。


上面的描述本质上是用于得出当前 “baked” 方法的设计过程的总结。从 “正常” 方法开始,需要解决缓存键构造和管理、删除所有冗余 Python 执行以及使用条件构建的查询等其他问题,从而得出最终方法。


特殊查询技术


本节将介绍针对特定查询情况的一些技术。


使用 IN 表达式


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()


使用子查询


使用 Query 对象时,通常需要一个 Query object 用于在另一个子查询中生成子查询。 在 Query 当前处于烘焙形式,可以使用 BakedQuery.to_query() 使用临时方法来检索 Query 对象 方法。 此方法将 SessionQuery 传递给用于生成烘焙查询的特定步骤的 lambda 可调用对象的参数:

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())


在 1.3 版本加入.


使用 before_compile 事件


从 SQLAlchemy 1.3.11 开始,使用 QueryEvents.before_compile() event 将不允许烘焙的 Query 系统缓存 Query,如果事件钩子返回新的 Query 对象,该对象与传入的对象不同。 这样, QueryEvents.before_compile() 钩子可以针对特定的 query,以容纳 每次都以不同的方式更改查询。 要允许 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 的事件,而不依赖于特定参数或更改的外部状态。


在 1.3.11 版本加入: - 添加了 “bake_ok” 标志到 QueryEvents.before_compile() 事件,并且不允许通过“baked”扩展进行缓存,如果未设置此标志,则返回新的 Query 对象。


在会话范围内禁用 Baked Queries


标志 Session.enable_baked_queries 可以设置为 False, 导致所有烘焙查询在用于该缓存时不使用缓存 场次

session = Session(engine, enable_baked_queries=False)


像所有 session 标志一样,它也被工厂对象接受,比如 sessionmaker 和类似 sessionmaker.configure() 的方法。


此标志的直接理由是,如果应用程序发现可能由于用户定义的烘焙查询或其他烘焙查询的缓存键冲突而导致的问题,则可以关闭该行为,以便识别或消除烘焙查询作为问题的原因。


在 1.2 版本加入.


延迟加载集成


在 1.4 版本发生变更: 从 SQLAlchemy 1.4 开始,“烘焙查询”系统不再是关系加载系统的一部分。而是使用本机缓存系统。


API 文档


对象名称

描述


烘焙查询


Query 对象的 builder 对象。


面包店


建造一个新的面包店。


面包店


Callable,它返回一个 BakedQuery


函数 sqlalchemy.ext.baked 中。面包店size=200_size_alert=None


建造一个新的面包店。


结果


Bakery 的一个实例


sqlalchemy.ext.baked 中。BakedQuery


Query 对象的 builder 对象。


方法 sqlalchemy.ext.baked.BakedQuery. add_criteriafn*args


向此 BakedQuery 添加 criteria 函数。


这等效于使用 += 运算符就地修改 BakedQuery


classmethod sqlalchemy.ext.baked.BakedQuery. 面包店size=200_size_alert=None


建造一个新的面包店。


结果


Bakery 的一个实例


方法 sqlalchemy.ext.baked.BakedQuery. for_session会话


为此返回一个 Result 对象 BakedQuery 的 BakedQuery 中。


这相当于将 BakedQuery 作为 Python 可调用对象调用,例如 result = my_baked_query(session) .


方法 sqlalchemy.ext.baked.BakedQuery. spoilfull=False


取消将在此 BakedQuery 对象上发生的任何查询缓存。


BakedQuery 可以继续正常使用,但不会缓存其他创建函数;他们将在每次调用时被调用。


这是为了支持构造烘焙查询中的特定步骤使查询不符合可缓存资格的情况,例如依赖于某些不可缓存值的变体。


参数


full —— 如果为 False,则仅向此添加函数 BakedQuery 对象将不缓存;到目前为止 BakedQuery 的状态将从缓存中提取。如果为 True,则每次都从头开始构建整个 Query 对象,并在每次调用时调用所有创建函数。


方法 sqlalchemy.ext.baked.BakedQuery. to_queryquery_or_session


返回 Query 对象以用作子查询。


此方法应在用于生成封闭 BakedQuery 步骤的 lambda 可调用对象中使用。该参数通常应为传递给 lambda 的 Query 对象:

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())


如果 subquery 用于针对 Session 的第一个可调用对象,则 Session 也会被接受:

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())
)

参数

query_or_session


Query 对象或类 Session 对象,假定该对象位于封闭的 BakedQuery 可调用对象的上下文中。


在 1.3 版本加入.


方法 sqlalchemy.ext.baked.BakedQuery. with_criteriafn*args


将 criteria 函数添加到从此函数克隆的 BakedQuery 中。


这相当于使用 + 运算符生成经过修改的新 BakedQuery


sqlalchemy.ext.baked 中。面包店


Callable,它返回一个 BakedQuery


此对象由类 method 返回 BakedQuery.bakery() 的 BakedQuery.bakery() 中。它以对象的形式存在,以便可以轻松检查 “缓存”。


在 1.2 版本加入.


sqlalchemy.ext.baked 中。结果


Session 调用 BakedQuery


Result 对象是实际 Query 的位置 对象被创建或从缓存中检索, 针对目标 Session,然后调用以获取结果。


方法 sqlalchemy.ext.baked.Result 中。全部()


返回所有行。


等效于 Query.all()。


方法 sqlalchemy.ext.baked.Result 中。计数


返回 'count'。


等效于 Query.count()。


请注意,无论原始语句的结构如何,这都使用子查询来确保准确计数。


方法 sqlalchemy.ext.baked.Result 中。first()


返回第一行。


等效于 Query.first()。


方法 sqlalchemy.ext.baked.Result 中。getident


根据身份检索对象。


等效于 Query.get()。


方法 sqlalchemy.ext.baked.Result 中。 ()


只返回一个结果或引发异常。


等效于 Query.one()。


方法 sqlalchemy.ext.baked.Result 中。one_or_none()


返回一个或零个结果,或者为多行引发异常。


等效于 Query.one_or_none()。


方法 sqlalchemy.ext.baked.Result 中。参数 *args**kw


指定要替换到字符串 SQL 语句中的参数。


方法 sqlalchemy.ext.baked.Result 中。标量


返回第一个结果的第一个元素,如果不存在行,则返回 None。如果返回多行,则引发 MultipleResultsFound。


等效于 Query.scalar()。


方法 sqlalchemy.ext.baked.Result 中。with_post_criteria fn


添加将在缓存后应用的 criteria 函数。


这将添加一个函数,该函数将针对 缓存中检索对象后的 Query 对象。这目前包括 Query.params()Query.execution_options() 方法。


警告


Result.with_post_criteria() 函数已应用 到 Query object 查询的 SQL 语句之后 对象已从缓存中检索。 只 Query.params()Query.execution_options() 应该使用 methods。


在 1.2 版本加入.