烘焙查询

优质
小牛编辑
132浏览
2023-12-01

bakedQuery 对象,它允许缓存对象的构造和字符串编译步骤。这就意味着 Query 构建不止一次使用的场景时,从最初构建到生成SQL字符串的查询构建过程中涉及的所有python函数调用都只会发生。 once 而不是每次建立和执行查询时。

这个系统的基本原理是大大减少发生的所有事情的python解释器开销。 在发出SQL之前 . “烘焙”系统的缓存 not 以任何方式减少SQL调用或缓存 返回结果 从数据库中。演示SQL调用和结果集本身的缓存的技术在 狗堆缓存 .

1.4 版后已移除: SQLAlchemy 1.4和2.0提供了一个全新的直接查询缓存系统,消除了对 BakedQuery 系统。现在,对于所有核心和ORM查询,缓存都是透明的活动的,用户无需使用中描述的系统执行任何操作 SQL编译缓存 .

Deep Alchemy

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

简介

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

from sqlalchemy.ext import baked

bakery = baked.bakery()

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

面包店让我们建立了一个 Query 通过将其构造指定为一系列python可调用文件(通常是lambda)来实现。为了简洁起见,它重写 += 运算符,以便典型的查询组合如下所示:

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。给的第一个lambda bakery() 函数接收 Session 作为它的论点。其余的羊羔每人得到一个 Query 作为他们的论点。

  4. 在上面的代码中,即使我们的应用程序可能调用 search_for_user() 很多次,即使在每次调用中,我们构建了一个全新的 BakedQuery 对象, 所有的羊羔只叫一次 . 每个lambda是 从未 只要此查询缓存在面包房中,就再次调用。

  5. 缓存是通过存储对 lambda对象本身 为了构造一个缓存键,也就是说,Python解释器为这些函数分配了一个in-python标识,这决定了如何在连续运行时标识查询。对于那些调用 search_for_user() 何处 email 参数已指定,可调用 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

注意,这个测试非常有意地提供只返回一行的查询。对于返回多行的查询,烘焙查询的性能优势将受到越来越少的影响,这与获取行所花费的时间成比例。重要的是要记住 烘焙查询功能只适用于构建查询本身,而不适用于获取结果。 . 使用baked特性并不能保证应用程序速度更快;对于那些被测量为受到这种特殊形式开销影响的应用程序来说,它只是一个潜在的有用特性。

量两次,切一次

有关如何配置SQLAlchemy应用程序的背景信息,请参阅部分 性能 . 在试图提高应用程序的性能时,必须使用性能度量技术。

理论基础

上面的“lambda”方法是更传统的“参数化”方法的超集。假设我们希望建立一个简单的系统, Query 只需一次,然后将其存储在字典中以便重复使用。现在只需构建查询并删除其 Session 通过呼叫 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) 以及调用 filter(Model.id == bindparam('id')) 这将跳过核心表达式的构建,并将其发送到 Query.filter() . 但是,该方法仍然可以重新生成 Select 每次当 Query.all() 被称为,另外这个全新的 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”方法重新构建上述框架,使其看起来不像“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()

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

从上面来看,如果我们问自己,“如果查找需要对查询的结构作出有条件的决定,该怎么办?”希望这就是为什么“烘焙”就是它的样子的原因。我们可以从一个函数(我们认为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所需的所有工作。我们也不再需要处理确保生成一个缓存键,它准确地考虑到我们所做的所有结构修改;现在这是自动处理的,并且不会出错。

这个代码示例比简单的示例短了几行,消除了处理缓存键的需要,并且具有完整的所谓“烘焙”特性的巨大性能优势。但还是有点冗长!因此我们采取的方法 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执行以及使用条件构建的查询等其他问题,最终得到最终方法。

特殊查询技术

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

在表达式中使用

这个 ColumnOperators.in_() sqlAlchemy中的方法历史上根据传递给该方法的项的列表呈现绑定参数的变量集。这不适用于烘焙查询,因为该列表的长度可以在不同的调用上更改。为了解决这个问题, bindparam.expanding 参数支持在表达式中安全缓存在烘焙查询中的延迟呈现。元素的实际列表在语句执行时呈现,而不是在语句编译时呈现:

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

参见

bindparam.expanding

ColumnOperators.in_()

使用子查询

使用时 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开始,使用 QueryEvents.before_compile() 针对特定事件 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 新版功能: -在 QueryEvents.before_compile() 对于返回新的 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”系统不再是关系加载系统的一部分。这个 native caching 而是使用系统。

API文档

Object NameDescription

BakedQuery

的生成器对象 Query 物体。

bakery

建造一个新的面包店。

Bakery

可调用,返回 BakedQuery .

Result

调用一个 BakedQuery 反对 Session .

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

建造一个新的面包店。

返回

一个实例 Bakery

class sqlalchemy.ext.baked.BakedQuery(bakery, initial_fn, args=())

的生成器对象 Query 物体。

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

向此添加条件函数 BakedQuery .

这相当于使用 += 要修改的运算符 BakedQuery 就位。

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

建造一个新的面包店。

返回

一个实例 Bakery

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

返回A Result 为此目的 BakedQuery .

这相当于调用 BakedQuery 作为可调用的python,例如 result = my_baked_query(session) .

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

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

bakedQuery可以继续正常使用,但是不会缓存其他创建函数;每次调用时都会调用它们。

这是为了支持这样一种情况,即构造烘焙查询的特定步骤会取消查询的可缓存性,例如依赖某些不可缓存值的变量。

参数

full -- 如果为false,则仅向此添加函数 BakedQuery 弃土步骤之后的对象将不缓存; BakedQuery 直到这一点被从缓存中拉出。如果是真的,那么整个 Query 对象每次都是从头开始构建的,每次调用都会调用所有创建函数。

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

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

此方法应在用于生成封闭步骤的lambda可调用范围内使用 BakedQuery . 参数通常应为 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 , the 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(cls_, cache)

可调用,返回 BakedQuery .

此对象由Class方法返回 BakedQuery.bakery() . 它作为一个对象存在,因此可以很容易地检查“缓存”。

1.2 新版功能.

class sqlalchemy.ext.baked.Result(bq, session)

调用一个 BakedQuery 反对 Session .

这个 Result 对象是实际 Query 对象根据目标创建或从缓存中检索 Session ,然后为结果调用。

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

返回所有行。

相当于 Query.all() .

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

返回“count”。

相当于 Query.count() .

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

1.1.6 新版功能.

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

1.0.9 新版功能.

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

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

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

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

相当于 Query.scalar() .

1.1.6 新版功能.

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

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

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

警告

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

1.2 新版功能.