版本:1.1.0b2 |发布日期:2016年7月1日

SQLAlchemy 1.1文档

烘焙查询

bakedQuery对象提供了替代的创建模式,可以缓存对象的构造和字符串编译步骤。这意味着对于不止一次使用的特定的Query构建场景,从构建查询起,通过生成SQL字符串构建查询所涉及的所有Python函数调用将仅发生在一次,而不是每次查询建立和执行。

The rationale for this system is to greatly reduce Python interpreter overhead for everything that occurs before the SQL is emitted. The caching of the “baked” system does not in any way reduce SQL calls or cache the return results from the database. 示范SQL调用和结果集本身缓存的技术可以在Dogpile Caching中找到。

版本1.0.0中的新功能

注意

从1.0.0开始,sqlalchemy.ext.baked扩展名应该被认为是实验它提供了一个显着不同的查询系统,尚未得到大规模的证明。

概要¶ T0>

烘焙系统的使用开始于生成所谓的“面包店”,该面包店代表特定系列查询对象的存储:

from sqlalchemy.ext import baked

bakery = baked.bakery()

上述“面包店”将缓存的数据存储在默认为200个元素的LRU缓存中,注意到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的一个实例。This object is essentially the “builder” for a real orm Query object, but it is not itself the actual Query object.
  2. The actual Query object is not built at all, until the very end of the function when Result.all() is called.
  3. 添加到baked_query对象的步骤全部表示为Python函数,通常是lambda表达式。赋给bakery()函数的第一个lambda接收一个Session作为它的参数。其余的lambda每个接收一个Query作为它们的参数。
  4. In the above code, even though our application may call upon search_for_user() many times, and even though within each invocation we build up an entirely new BakedQuery object, all of the lambdas are only called once. Each lambda is never called a second time for as long as this query is cached in the bakery.
  5. 通过存储对lambda对象本身的引用来实现高速缓存以便形成高速缓存密钥;也就是说,Python解释器为这些函数分配一个Python内部身份的事实决定了如何在连续运行中识别查询。For those invocations of search_for_user() where the email parameter is specified, the callable lambda q: q.filter(User.email == bindparam('email')) will be part of the cache key that’s retrieved; when email is None, this callable is not part of the cache key.
  6. Because the lambdas are all called only once, it is essential that no variables which may change across calls are referenced within the lambdas; instead, assuming these are values to be bound into the SQL string, we use bindparam() to construct named parameters, where we apply their actual values later using Result.params().

性能¶ T0>

烘焙的查询可能看起来有点奇怪,有点尴尬,有点冗长。但是,在应用程序中调用很多次的查询所节省的Python性能非常显着。Performance中演示的示例套件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

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

测量两次,切一次

有关如何配置SQLAlchemy应用程序的背景信息,请参阅Performance部分。尝试提高应用程序的性能时,使用性能测量技术是非常重要的。

¶ T0>

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

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

上述方法使我们获得了非常小的性能优势。By re-using a Query, we save on the Python work within the session.query(Model) constructor as well as calling upon filter(Model.id == bindparam('id')), which will skip for us the building up of the Core expression as well as sending it to Query.filter(). However, the approach still regenerates the full Select object every time when Query.all() is called and additionally this brand new Select is sent off to the string compilation step every time, which for a simple case like the above is probably about 70% of the overhead.

为了减少额外的开销,我们需要一些更专业的逻辑,一些方法来记住选择对象的构造和SQL的构造。在这个维基上的例子是BakedQuery,它是这个特性的前身,然而在这个系统中,我们没有缓存查询的构造为了消除所有的开销,我们需要缓存查询的构造以及SQL编译。Let’s assume we adapted the recipe in this way and made ourselves a method .bake() that pre-compiles the SQL for the query, producing a new object that can be invoked with minimal overhead. 我们的例子变成:

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”的缓存键,而且还包含了与我们自定义的“烘焙”功能相同的功能,这个功能缓存了Python调用工作的100%查询过滤器调用到生成Select对象,到字符串编译步骤。

从以上所述,如果我们自问,“如果查询需要对查询的结构做出条件性决定呢?”,这就是希望变得明显,为什么“烘焙”就是这样。我们可以从任何数目的函数构建它,而不是从一个函数(这是我们认为的烘焙最初可能工作的原理)构建的参数化查询。考虑一下我们天真的例子,如果我们需要在条件基础上有一个额外的条款:

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执行以及用条件构建的查询等附加问题,从而导致最终方法。

延迟加载整合

烘焙的查询可以透明地与SQLAlchemy的懒加载程序功能集成。未来版本的SQLAlchemy可能默认启用这个功能,因为它在延迟加载中的使用是完全透明的。现在,要为全系统的所有lazyloader启用烘焙加载,请调用bake_lazy_loaders()函数。这将影响所有使用lazy='select'策略的关系以及所有使用lazyload()的每个查询策略。

通过使用baked_select加载器策略,可以基于per- relationship()启用“烘焙”延迟加载:

class MyClass(Base):
    # ...

    widgets = relationship("Widget", lazy="baked_select")

一旦应用程序的任何部分导入了sqlalchemy.ext.baked模块,就可以使用baked_select策略。这个特征所使用的“面包房”对于MyClass的映射器是本地的。

对于每个查询使用,可以使用baked_lazyload()策略,该策略与任何其他的加载程序选项相同。

选择与bake_queries标志

relationship()结构包含一个标志relationship.bake_queries,当设置为False时,将导致该关系退出烘焙查询系统,当应用程序范围bake_lazy_loaders()函数被默认调用来启用烘焙查询加载器。

API文档

sqlalchemy.ext.baked.bakery(cls, size=200)

建造一个新的面包店。

class sqlalchemy.ext.baked。 BakedQuery tt> 面包店initial_fn args =()

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

add_criteria(fn, *args)

为这个BakedQuery添加一个标准函数。

这相当于使用+=运算符就地修改BakedQuery

classmethod bakery(size=200)

建造一个新的面包店。

for_session T0> ( T1> 会话 T2> ) T3> ¶ T4>

为这个BakedQuery返回一个Result对象。

这相当于把BakedQuery作为一个可调用的Python来调用。 结果 = my_baked_query(会话)

弃土 T0> ( T1> 满=假 T2> ) T3> ¶ T4>

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

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

这是为了支持在构建烘焙查询中的特定步骤使查询不能被缓存的情况,例如依赖于一些不可缓存的值的变体。

参数: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. 如果为True,那么整个Query对象都是从头开始构建的,每个调用都会调用所有的创建函数。
with_criteria(fn, *args)

向这个克隆的BakedQuery添加一个标准函数。

这相当于使用+运算符产生新的BakedQuery并进行修改。

class sqlalchemy.ext.baked。 tt> 结果bq T5> ) T6> ¶ T7>

针对Session调用BakedQuery

The Result object is where the actual query.Query object gets created, or retrieved from the cache, against a target Session, and is then invoked for results.

所有 T0> ( T1> ) T2> ¶ T3>

返回所有行。

等同于Query.all()

第一 T0> ( T1> ) T2> ¶ T3>

返回第一行。

相当于Query.first()

获得 T0> ( T1> IDENT T2> ) T3> ¶ T4>

基于身份检索对象。

等同于Query.get()

一个 T0> ( T1> ) T2> ¶ T3>

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

等同于Query.one()

one_or_none T0> ( T1> ) T2> ¶ T3>

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

等同于Query.one_or_none()

版本1.0.9中的新功能

params * args** kw T5>

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

sqlalchemy.ext.baked。 T0> bake_lazy_loaders T1> ( T2> ) T3> ¶ T4>

为全系统的所有lazyloader启用烘焙查询。

这个操作对于所有懒惰的加载器应该是安全的,并且会减少这些操作的Python开销。

sqlalchemy.ext.baked。 T0> unbake_lazy_loaders T1> ( T2> ) T3> ¶ T4>

全系统禁用所有lazyloader的烘焙查询的使用。

该操作将恢复bake_lazy_loaders()产生的更改。

sqlalchemy.ext.baked。 T0> baked_lazyload T1> ( T2> *键 T3> ) T4> ¶ T5>

表明给定的属性应该使用加载中使用的“烘焙”查询使用“惰性”加载加载。

sqlalchemy.ext.baked。 T0> baked_lazyload_all T1> ( T2> *键 T3> ) T4> ¶ T5>

orm.baked_lazyload()生成一个独立的“全部”选项。

从版本0.9.0开始弃用:“_all()”样式被方法链接取代,例如:

session.query(MyClass).options(
    baked_lazyload("someattribute").baked_lazyload("anotherattribute")
)