预编译查询

baked 提供了一种针对 Query 对象的替代创建模式,它允许缓存对象的构建和字符串编译步骤。这意味着,对于某个特定的 Query 构建场景,如果该场景被多次使用,那么从初始构建到生成 SQL 字符串的所有 Python 函数调用步骤只会执行 **一次**,而不是每次构建和执行查询时都执行。

该系统的原理是大大减少从 **SQL 生成之前** 的所有操作中产生的 Python 解释器开销。该“预编译”系统的缓存 **不会** 以任何方式减少 SQL 调用或缓存从数据库返回的 **结果集**。在 Dogpile 缓存 中提供了一种演示 SQL 调用和结果集本身缓存的技术。

自版本 1.4 起已弃用: SQLAlchemy 1.4 和 2.0 提供了一个全新的直接查询缓存系统,消除了对 BakedQuery 系统的需求。现在,对于所有 Core 和 ORM 查询,缓存都已透明地启用,无需用户采取任何操作,使用的是 SQL 编译缓存 中所述的系统。

深度炼金

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

概述

预编译系统的使用从生成一个所谓的“烘焙器”开始,它代表了特定系列查询对象的存储空间

from sqlalchemy.ext import baked

bakery = baked.bakery()

上面的“烘焙器”将把缓存数据存储在一个 LRU 缓存中,默认情况下缓存包含 200 个元素,需要注意的是,ORM 查询通常会为 ORM 查询的调用包含一个条目,以及针对每个数据库方言的 SQL 字符串包含一个条目。

烘焙器允许我们通过指定其构建方式作为一系列 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 对象的“构建器”,但它本身并不是真正的 Query 对象。

  2. 实际的 Query 对象直到函数的最后部分才会构建,此时会调用 Result.all()

  3. 添加到 baked_query 对象中的步骤都用 Python 函数(通常是 lambda 函数)表示。传递给 bakery() 函数的第一个 lambda 函数接收一个 Session 作为参数。其余的 lambda 函数都接收一个 Query 作为参数。

  4. 在上面的代码中,即使我们的应用程序可能多次调用 search_for_user(),并且即使在每次调用中,我们都构建了一个全新的 BakedQuery 对象,所有这些 lambda 函数都只调用一次。只要此查询在烘焙器中缓存,每个 lambda 函数 **永远不会** 被再次调用。

  5. 缓存是通过存储对 **lambda 对象本身** 的引用来实现的,以便形成缓存键;也就是说,Python 解释器对这些函数分配 Python 内部的标识符,正是这一点决定了如何在后续运行中识别查询。对于那些 email 参数已指定的 search_for_user() 调用,可调用对象 lambda q: q.filter(User.email == bindparam('email')) 将成为检索到的缓存键的一部分;当 emailNone 时,此可调用对象将不会成为缓存键的一部分。

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

与等效的“预编译”查询进行比较

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) 来移除其 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')) 上节省了时间,这将为我们跳过构建核心表达式以及将其发送到 Query.filter()。但是,这种方法在每次调用 Query.all() 时仍然会重新生成完整的 Select 对象,此外,这个全新的 Select 会在每次调用时被发送到字符串编译步骤,对于像上面这样简单的案例,这可能占了大约 70% 的开销。

为了减少额外的开销,我们需要一些更专业的逻辑,一些方法来记忆 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()

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

我们可以使用“烘焙”方法来重新构建上述代码,使其看起来不像“构建 lambda”方法那样不寻常,更像是对简单“重用查询”方法的一种简单改进

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

上面,我们以非常类似于简单的“缓存查询”系统的方式使用了“烘焙”系统。但是,它减少了两行代码,不需要制造一个名为“my_key”的缓存键,并且还包含了与我们自定义的“bake”函数相同的特性,该函数缓存了从查询的构造函数到 filter 调用、到 Select 对象的生成,再到字符串编译步骤的 100% Python 调用工作。

从上面可以看出,如果我们问自己,“如果查找需要根据查询结构做出条件决策呢?”,这里可能就能明白为什么“烘焙”是现在这个样子。我们不是从一个函数构建参数化查询(这原本是我们认为烘焙工作方式的方式),而是可以从任意数量的函数构建。考虑我们的简单例子,如果我们需要根据条件在查询中添加一个额外的子句

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_frobnizzle”标志,因为这个标志的存在意味着生成的 SQL 将完全不同。应该很清楚,随着查询构建复杂度的增加,缓存这些查询的任务会很快变得很繁重。我们可以将上面的例子转换为直接使用“烘焙”的方式,如下所示

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() 这样的方法,并将它们缩短为运算符,并鼓励(当然不是强制要求!)使用简单的 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()

上面,这种方法更容易实现,并且在代码流程方面更类似于非缓存查询函数,因此更容易移植代码。

上面的描述基本上是对用于实现当前“烘焙”方法的设计过程的总结。从“正常”方法开始,需要解决缓存键构建和管理、删除所有冗余 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 对象用于在另一个对象中生成子查询。如果 Query 当前处于烘焙形式,则可以使用一种中间方法来检索 Query 对象,使用 BakedQuery.to_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 开始,如果事件钩子返回一个与传入对象不同的新的 Query 对象,则在特定 Query 上使用 QueryEvents.before_compile() 事件将不允许烘焙查询系统缓存查询。这是为了使 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 中的新增功能: - 在 QueryEvents.before_compile() 事件中添加了“bake_ok”标志,并禁止通过“烘焙”扩展进行缓存,如果事件处理程序返回一个新的 Query 对象,而该标志未设置。

在会话级禁用烘焙查询

可以使用 Session.enable_baked_queries 标志将其设置为 False,从而导致所有烘焙查询在针对该 Session 时不使用缓存

session = Session(engine, enable_baked_queries=False)

与所有会话标志一样,它也为工厂对象(如 sessionmaker)和方法(如 sessionmaker.configure())所接受。

此标志的直接原因是,对于由于用户定义的烘焙查询或其他烘焙查询问题导致缓存键冲突而遇到问题的应用程序,可以关闭此行为,以便识别或消除烘焙查询作为问题根源的可能性。

版本 1.2 中的新增功能。

延迟加载集成

在版本 1.4 中变更: 从 SQLAlchemy 1.4 开始,"baked query" 系统不再是关系加载系统的一部分。取而代之的是使用 本地缓存 系统。

API 文档

对象名称 描述

BakedQuery

用于 Query 对象的构建器对象。

bakery

构造一个新的 bakery。

Bakery

可调用函数,返回一个 BakedQuery

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

构造一个新的 bakery。

返回:

一个 Bakery 实例

class sqlalchemy.ext.baked.BakedQuery

用于 Query 对象的构建器对象。

method sqlalchemy.ext.baked.BakedQuery.add_criteria(fn, *args)

将一个条件函数添加到这个 BakedQuery 中。

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

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

构造一个新的 bakery。

返回:

一个 Bakery 实例

method sqlalchemy.ext.baked.BakedQuery.for_session(session)

返回这个 BakedQueryResult 对象。

这等同于将 BakedQuery 作为 Python 可调用函数进行调用,例如 result = my_baked_query(session)

method sqlalchemy.ext.baked.BakedQuery.spoil(full=False)

取消对这个 BakedQuery 对象进行的任何查询缓存。

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

这是为了支持在构建 baked query 的某个步骤中,查询无法被缓存的情况,例如一个依赖于某些不可缓存值的变体。

参数:

full – 如果为 False,则只有在 spoil 步骤之后添加到这个 BakedQuery 对象中的函数将不会被缓存;在该步骤之前的 BakedQuery 状态将从缓存中提取。如果为 True,则每次都会从头开始构建整个 Query 对象,每次调用都会调用所有创建函数。

method sqlalchemy.ext.baked.BakedQuery.to_query(query_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())

在子查询在第一个可调用函数中针对 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 中的新增功能。

method sqlalchemy.ext.baked.BakedQuery.with_criteria(fn, *args)

将一个条件函数添加到从这个 BakedQuery 克隆出来的 BakedQuery 中。

这等同于使用 + 运算符生成一个具有修改的新 BakedQuery

class sqlalchemy.ext.baked.Bakery

可调用函数,返回一个 BakedQuery

此对象由类方法 BakedQuery.bakery() 返回。它作为对象存在,以便可以轻松地检查“缓存”。

版本 1.2 中的新增功能。

class sqlalchemy.ext.baked.Result

Session 上调用 BakedQuery

Result 对象是在目标 Session 上创建或从缓存中检索实际 Query 对象的地方,然后调用它获取结果。

method sqlalchemy.ext.baked.Result.all()

返回所有行。

等效于 Query.all().

method sqlalchemy.ext.baked.Result.count()

返回“计数”。

等效于 Query.count().

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

method sqlalchemy.ext.baked.Result.first()

返回第一行。

等效于 Query.first().

method sqlalchemy.ext.baked.Result.get(ident)

根据标识符检索对象。

等效于 Query.get().

method sqlalchemy.ext.baked.Result.one()

返回正好一个结果,或者抛出异常。

等效于 Query.one().

method sqlalchemy.ext.baked.Result.one_or_none()

返回一个或零个结果,或者对于多行抛出异常。

等效于 Query.one_or_none().

method sqlalchemy.ext.baked.Result.params(*args, **kw)

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

method sqlalchemy.ext.baked.Result.scalar()

返回第一个结果的第一个元素,如果没有行,则返回 None。如果返回多行,则会引发 MultipleResultsFound。

等效于 Query.scalar().

method sqlalchemy.ext.baked.Result.with_post_criteria(fn)

添加一个将在缓存后应用的条件函数。

这将添加一个将在 Query 对象从缓存中检索后对其运行的函数。目前这仅包括 Query.params()Query.execution_options() 方法。

警告

Result.with_post_criteria() 函数应用于 Query 对象,查询的 SQL 语句对象从缓存中检索后。只有 Query.params()Query.execution_options() 方法应该使用。

版本 1.2 中的新增功能。