Baked Queries

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

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

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

深入炼金术

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

概要

baked 系统的使用首先生成所谓的 “bakery”,它代表特定系列查询对象的存储

from sqlalchemy.ext import baked

bakery = baked.bakery()

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

bakery 允许我们通过将 Query 对象的构造指定为一系列 Python 可调用对象(通常是 lambdas)来构建它。 为了简洁使用,它重载了 += 运算符,因此典型的查询构建过程如下所示

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 函数,通常是 lambdas。 提供给 bakery() 函数的第一个 lambda 接收一个 Session 作为其参数。 剩余的 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')) 将成为检索到的缓存键的一部分; 当 emailNone 时,此可调用对象不属于缓存键。

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

性能

baked query 可能看起来有点奇怪、有点笨拙和有点冗长。 但是,对于在应用程序中多次调用的查询,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

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

原理

上面的 “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')),这将为我们跳过构建 Core 表达式以及将其发送到 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()

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

我们可以使用 “bakery” 方法以一种看起来不如 “构建 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()

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

从上面来看,如果我们问自己,“如果查找需要对查询的结构做出有条件的决定怎么办?”,那么希望这会变得显而易见,为什么 “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()

我们 “简单” 的参数化系统现在必须负责生成缓存键,这些缓存键要考虑到是否传递了 “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()

上面,我们再次缓存的不仅是查询对象,还有它生成 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_() 方法历史上根据传递给该方法的项目列表呈现一组可变数量的绑定参数。 这不适用于 baked query,因为该列表的长度可能会在不同的调用中更改。 为了解决这个问题,bindparam.expanding 参数支持延迟呈现的 IN 表达式,该表达式可以安全地缓存在 baked query 内部。 元素的实际列表在语句执行时呈现,而不是在语句编译时呈现

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 当前处于 baked 形式,则可以使用中间方法来检索 Query 对象,使用 BakedQuery.to_query() 方法。 此方法传递给 SessionQuery,它是用于生成 baked query 的特定步骤的 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 对象,则对特定 Query 使用 QueryEvents.before_compile() 事件将阻止 baked 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 版本新增: - 向 QueryEvents.before_compile() 事件添加了 “bake_ok” 标志,并且如果未设置此标志,则禁止通过 “baked” 扩展对返回新 Query 对象的事件处理程序进行缓存。

会话范围内禁用 Baked Queries

可以将标志 Session.enable_baked_queries 设置为 False,从而导致所有 baked query 在针对该 Session 使用时都不使用缓存

session = Session(engine, enable_baked_queries=False)

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

此标志的直接原理是,如果应用程序遇到潜在问题,可能是由于用户定义的 baked query 或其他 baked query 问题的缓存键冲突引起的,则可以关闭此行为,以便识别或消除 baked query 作为问题的原因。

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 添加一个 criteria 函数。

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

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

构造一个新的 bakery。

返回:

Bakery 的实例

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

为此 BakedQuery 返回 Result 对象。

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

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

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

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

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

参数:

full – 如果为 False,则仅添加到此 BakedQuery 对象中的 spoil 步骤之后的函数将是非缓存的; BakedQuery 到此点的状态将从缓存中拉取。 如果为 True,则每次都从头开始构建整个 Query 对象,并在每次调用时调用所有创建函数。

method sqlalchemy.ext.baked.BakedQuery.to_query(query_or_session)

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

此方法应在用于生成封闭 BakedQuery 的步骤的 lambda 可调用对象中使用。 该参数通常应该是 Query 对象,该对象传递给 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())

在子查询用于针对 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

class sqlalchemy.ext.baked.Bakery

返回 BakedQuery 的可调用对象。

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

1.2 版本新增。

class sqlalchemy.ext.baked.Result

针对 Session 调用 BakedQuery

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

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

返回所有行。

等同于 Query.all()

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

返回 “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() 函数在从缓存中检索查询的 SQL 语句对象后应用于 Query 对象。 仅应使用 Query.params()Query.execution_options() 方法。

1.2 版本新增。