SqlAlchemy 2.0 中文文档(四十四)(8)

本文涉及的产品
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
日志服务 SLS,月写入数据量 50GB 1个月
简介: SqlAlchemy 2.0 中文文档(四十四)

SqlAlchemy 2.0 中文文档(四十四)(7)https://developer.aliyun.com/article/1563074


模式名称的翻译

为了支持将共享的表集分布到多个模式的多租户应用程序,可以使用 Connection.execution_options.schema_translate_map 执行选项将一组 Table 对象重新用不同的模式名称渲染,而不需要进行任何更改。

给定一张表:

user_table = Table(
    "user",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
)

Table 的“模式”由 Table.schema 属性定义为 NoneConnection.execution_options.schema_translate_map 可以指定所有模式为 NoneTable 对象应将模式渲染为 user_schema_one

connection = engine.connect().execution_options(
    schema_translate_map={None: "user_schema_one"}
)
result = connection.execute(user_table.select())

上述代码将在数据库上调用以下形式的 SQL:

SELECT  user_schema_one.user.id,  user_schema_one.user.name  FROM
user_schema_one.user

也就是说,模式名称被替换为我们翻译过的名称。映射可以指定任意数量的目标->目的地模式:

connection = engine.connect().execution_options(
    schema_translate_map={
        None: "user_schema_one",  # no schema name -> "user_schema_one"
        "special": "special_schema",  # schema="special" becomes "special_schema"
        "public": None,  # Table objects with schema="public" will render with no schema
    }
)

Connection.execution_options.schema_translate_map 参数影响从 SQL 表达语言生成的所有 DDL 和 SQL 构造,这些构造是从 TableSequence 对象派生而来的。它 不会 影响通过 text() 构造使用的文本字符串 SQL,也不会影响传递给 Connection.execute() 的普通字符串。

此功能仅在模式的名称直接从TableSequence 派生的情况下生效;它不影响直接传递字符串模式名称的方法。根据此模式,在调用诸如MetaData.create_all()MetaData.drop_all() 等方法执行的“可以创建”/“可以删除”检查中生效,并且在使用给定 Table 对象的表反射时生效。然而,它不会影响Inspector 对象上存在的操作,因为模式名称是显式传递给这些方法的。

提示

要在 ORM Session 中使用模式转换功能,请将此选项设置在Engine 的级别上,然后将该引擎传递给 SessionSession 为每个事务使用一个新的Connection

schema_engine = engine.execution_options(schema_translate_map={...})
session = Session(schema_engine)
...

警告

当在没有扩展的情况下使用 ORM Session 时,仅支持每个 Session 一个单一的模式转换映射。如果在每个语句的基础上给出了不同的模式转换映射,则不会生效,因为 ORM Session 不会考虑当前模式转换值对各个对象的影响。

要在多个 schema_translate_map 配置中使用单个 Session,可以使用水平分片扩展。请参阅水平分片中的示例。

SQL 编译缓存

从版本 1.4 开始:SQLAlchemy 现在具有一个透明的查询缓存系统,大大降低了将 SQL 语句结构转换为 SQL 字符串时涉及的 Python 计算开销,包括 Core 和 ORM。请参阅透明 SQL 编译缓存添加到 Core、ORM 中的所有 DQL、DML 语句的介绍。

SQLAlchemy 包括一个全面的缓存系统,用于 SQL 编译器及其 ORM 变体。此缓存系统在Engine中是透明的,并且提供了对于给定的 Core 或 ORM SQL 语句的 SQL 编译过程以及为该语句组装结果获取机制的相关计算,只会对该语句对象及所有具有相同结构的其他语句执行一次,只要特定结构在引擎的“编译缓存”中保持。关于“具有相同结构的语句对象”,这通常对应于在函数内构造的 SQL 语句,每次运行该函数时都会构建:

def run_my_statement(connection, parameter):
    stmt = select(table)
    stmt = stmt.where(table.c.col == parameter)
    stmt = stmt.order_by(table.c.id)
    return connection.execute(stmt)

上述语句将生成类似于SELECT id, col FROM table WHERE col = :col ORDER BY id的 SQL 语句,注意,虽然parameter的值是一个普通的 Python 对象,比如一个字符串或一个整数,但是语句的字符串 SQL 形式不包括此值,因为它使用了绑定参数。上述run_my_statement()函数的后续调用将在connection.execute()调用的范围内使用缓存的编译构造以提高性能。

注意

需要注意的是,SQL 编译缓存仅缓存传递给数据库的SQL 字符串,而不是查询返回的数据。它绝不是数据缓存,也不会影响特定 SQL 语句返回的结果,也不会暗示与结果行提取相关联的任何内存使用。

虽然 SQLAlchemy 自 1.x 系列早期就有了一个基本的语句缓存,并且此外还提供了 ORM 的“烘焙查询”扩展,但这两个系统都需要高度特殊的 API 使用,以便缓存起作用。自 1.4 版本以来的新缓存完全是自动的,不需要更改编程风格即可生效。

缓存是自动使用的,无需进行任何配置更改,也不需要任何特殊步骤来启用它。以下部分详细介绍了缓存的配置和高级使用模式。

配置

缓存本身是一个类似字典的对象,称为LRUCache,它是一个内部 SQLAlchemy 字典子类,跟踪特定键的使用情况,并具有定期的“修剪”步骤,当缓存的大小达到一定阈值时会删除最近未使用的项目。此缓存的大小默认为 500,可以使用create_engine.query_cache_size参数进行配置:

engine = create_engine(
    "postgresql+psycopg2://scott:tiger@localhost/test", query_cache_size=1200
)

缓存的大小可以增长到给定大小的 150%左右,然后将其修剪回目标大小。因此,大小为 1200 的缓存可以增长到 1800 个元素的大小,然后将其修剪回 1200。

缓存的大小基于每个唯一 SQL 语句渲染的单个条目,每个引擎。从 Core 和 ORM 生成的 SQL 语句被等同对待。DDL 语句通常不会被缓存。为了确定缓存正在做什么,引擎日志将包含有关缓存行为的详细信息,下一节描述了此信息。

使用日志估算缓存性能

上述缓存大小为 1200 实际上是相当大的。对于小型应用程序,大小为 100 可能足够。要估算缓存的最佳大小,假设目标主机上有足够的内存,缓存的大小应基于在使用中的目标引擎中可以呈现的唯一 SQL 字符串的数量。最快速的方法是使用 SQL 回显,最直接的方法是使用create_engine.echo 标志启用,或者使用 Python 日志记录;有关日志记录配置的背景,请参阅配置日志记录 部分。

作为示例,我们将检查以下程序产生的日志记录:

from sqlalchemy import Column
from sqlalchemy import create_engine
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import select
from sqlalchemy import String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Session
Base = declarative_base()
class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    data = Column(String)
    bs = relationship("B")
class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))
    data = Column(String)
e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)
s = Session(e)
s.add_all([A(bs=[B(), B(), B()]), A(bs=[B(), B(), B()]), A(bs=[B(), B(), B()])])
s.commit()
for a_rec in s.scalars(select(A)):
    print(a_rec.bs)

运行时,每个记录的 SQL 语句将在传递的参数左侧包含一个带方括号的缓存统计徽章。我们可能看到的四种消息类型总结如下:

  • [原始 SQL] - 驱动程序或最终用户使用Connection.exec_driver_sql() 发出原始 SQL - 不适用缓存
  • [无键] - 该语句对象是一个不被缓存的 DDL 语句,或者该语句对象包含不可缓存的元素,如用户定义的结构或任意大的 VALUES 子句。
  • [在 X 秒内生成] - 该语句是一个缓存未命中,必须被编译,然后存储在缓存中。生成编译结构花费了 X 秒。数字 X 将是小数秒数。
  • [自 X 秒前缓存] - 该语句是缓存命中,不需要重新编译。该语句已经存储在缓存中自 X 秒前。数字 X 与应用程序运行的时间以及语句被缓存的时间成比例,例如 24 小时的时间段将是 86400。

下面更详细地描述了每个徽章。

我们看到上面程序的第一个语句将是 SQLite 方言检查 “a” 和 “b” 表的存在:

INFO sqlalchemy.engine.Engine PRAGMA temp.table_info("a")
INFO sqlalchemy.engine.Engine [raw sql] ()
INFO sqlalchemy.engine.Engine PRAGMA main.table_info("b")
INFO sqlalchemy.engine.Engine [raw sql] ()

对于上述两个 SQLite PRAGMA 语句,徽章显示为[raw sql],这表示驱动程序正在使用Connection.exec_driver_sql()直接将 Python 字符串发送到数据库。对于这样的语句,缓存不适用,因为它们已经以字符串形式存在,而且由于 SQLAlchemy 不会提前解析 SQL 字符串,因此不知道将返回什么类型的结果行。

接下来我们看到的是 CREATE TABLE 语句:

INFO  sqlalchemy.engine.Engine
CREATE  TABLE  a  (
  id  INTEGER  NOT  NULL,
  data  VARCHAR,
  PRIMARY  KEY  (id)
)
INFO  sqlalchemy.engine.Engine  [no  key  0.00007s]  ()
INFO  sqlalchemy.engine.Engine
CREATE  TABLE  b  (
  id  INTEGER  NOT  NULL,
  a_id  INTEGER,
  data  VARCHAR,
  PRIMARY  KEY  (id),
  FOREIGN  KEY(a_id)  REFERENCES  a  (id)
)
INFO  sqlalchemy.engine.Engine  [no  key  0.00006s]  ()

对于每个语句,徽章显示为[no key 0.00006s]。这表示这两个特定语句,由于以 DDL 为导向的CreateTable构造未生成缓存键,因此缓存未发生。DDL 构造通常不参与缓存,因为它们通常不会被重复执行,而且 DDL 也是一个数据库配置步骤,性能并不那么关键。

[no key] 徽章还有一个重要原因,即它可能适用于可缓存的 SQL 语句,除了某些当前不可缓存的特定子构造。这些例子包括未定义缓存参数的自定义用户定义 SQL 元素,以及生成任意长且不可重现的 SQL 字符串的某些构造,主要示例包括Values构造以及使用Insert.values()方法进行“多值插入”时。

到目前为止,我们的缓存仍然是空的。然而,接下来的语句将被缓存,一个片段看起来像:

INFO  sqlalchemy.engine.Engine  INSERT  INTO  a  (data)  VALUES  (?)
INFO  sqlalchemy.engine.Engine  [generated  in  0.00011s]  (None,)
INFO  sqlalchemy.engine.Engine  INSERT  INTO  a  (data)  VALUES  (?)
INFO  sqlalchemy.engine.Engine  [cached  since  0.0003533s  ago]  (None,)
INFO  sqlalchemy.engine.Engine  INSERT  INTO  a  (data)  VALUES  (?)
INFO  sqlalchemy.engine.Engine  [cached  since  0.0005326s  ago]  (None,)
INFO  sqlalchemy.engine.Engine  INSERT  INTO  b  (a_id,  data)  VALUES  (?,  ?)
INFO  sqlalchemy.engine.Engine  [generated  in  0.00010s]  (1,  None)
INFO  sqlalchemy.engine.Engine  INSERT  INTO  b  (a_id,  data)  VALUES  (?,  ?)
INFO  sqlalchemy.engine.Engine  [cached  since  0.0003232s  ago]  (1,  None)
INFO  sqlalchemy.engine.Engine  INSERT  INTO  b  (a_id,  data)  VALUES  (?,  ?)
INFO  sqlalchemy.engine.Engine  [cached  since  0.0004887s  ago]  (1,  None)

在上面,我们看到了两个基本上是唯一的 SQL 字符串;"INSERT INTO a (data) VALUES (?)""INSERT INTO b (a_id, data) VALUES (?, ?)"。由于 SQLAlchemy 对所有文字值使用绑定参数,即使这些语句为不同对象重复多次,由于参数是分开的,实际的 SQL 字符串保持不变。

注意

上述两个语句是由 ORM 工作单元流程生成的,并且实际上将这些语句缓存在每个映射器本地的单独缓存中。然而,机制和术语是相同的。下面的部分禁用或使用替代字典缓存某些(或全部)语句将描述用户代码如何在每个语句基础上使用替代缓存容器。

我们在看到这两个语句的第一次出现时看到的缓存徽章是[生成于 0.00011s]。这表示该语句不在缓存中,已在 0.00011s 内编译为字符串,然后被缓存。当我们看到[生成]徽章时,我们知道这意味着发生了缓存未命中。这对于特定语句的第一次出现是可以预料的。然而,如果一个长时间运行的应用程序通常一遍又一遍地使用相同的一系列 SQL 语句,并且观察到大量新的[生成]徽章,这可能是create_engine.query_cache_size参数设置过小的迹象。当一个已被缓存的语句由于 LRU 缓存删除了不常用的项而被逐出缓存时,当它下次被使用时,它将显示[生成]徽章。

我们随后看到的每个这两个语句的后续出现的缓存徽章看起来像[自 0.0003533s 前缓存]。这表示该语句在缓存中找到,并且最初放入缓存中 0.0003533 秒前。重要的是要注意,虽然[生成][自]徽章都指的是秒数,但它们表示的是不同的含义;对于[生成],数字是编译语句所需的大致时间,并且将是一个极小的时间量。对于[自],这是语句在缓存中存在的总时间。对于运行了六个小时的应用程序,这个数字可能读作[自 21600 秒前缓存],这是件好事。看到“自”徽章的高数字表明这些语句很长时间没有发生缓存未命中。即使应用程序运行了很长时间,语句经常具有较低的“自”数也可能表明这些语句太频繁地发生缓存未命中,而create_engine.query_cache_size可能需要增加。

我们的示例程序然后执行了一些 SELECT,我们可以看到“生成”然后“缓存”的相同模式,对于“a”表的 SELECT 以及“b”表的后续延迟加载:

INFO sqlalchemy.engine.Engine SELECT a.id AS a_id, a.data AS a_data
FROM a
INFO sqlalchemy.engine.Engine [generated in 0.00009s] ()
INFO sqlalchemy.engine.Engine SELECT b.id AS b_id, b.a_id AS b_a_id, b.data AS b_data
FROM b
WHERE ? = b.a_id
INFO sqlalchemy.engine.Engine [generated in 0.00010s] (1,)
INFO sqlalchemy.engine.Engine SELECT b.id AS b_id, b.a_id AS b_a_id, b.data AS b_data
FROM b
WHERE ? = b.a_id
INFO sqlalchemy.engine.Engine [cached since 0.0005922s ago] (2,)
INFO sqlalchemy.engine.Engine SELECT b.id AS b_id, b.a_id AS b_a_id, b.data AS b_data
FROM b
WHERE ? = b.a_id

从我们上面的程序中,完整运行显示一共缓存了四个不同的 SQL 字符串。这表明缓存大小为将是足够的。这显然是一个极小的大小,而默认大小为 500 是可以保持不变的。

缓存使用多少内存?

前一节详细介绍了一些技术,用于检查create_engine.query_cache_size是否需要更大。我们如何知道缓存不会太大?我们可能希望将create_engine.query_cache_size设置为不高于某个数字的原因是,我们可能有一个应用程序,可能会使用非常多不同的语句,比如一个从搜索 UX 动态构建查询的应用程序,如果过去 24 小时运行了十万个不同的查询并且它们都被缓存,我们不希望我们的主机内存耗尽。

测量 Python 数据结构占用多少内存是非常困难的,然而,通过使用top进程来测量内存增长,当连续添加 250 个新语句到缓存时,表明一个中等大小的核心语句大约占用 12K,而一个小型 ORM 语句大约占用 20K,包括 ORM 的结果获取结构,对于 ORM 来说,这将更大。

禁用或使用备用字典来缓存一些(或全部)语句

使用的内部缓存称为LRUCache,但这主要只是一个字典。可以通过使用Connection.execution_options.compiled_cache选项作为执行选项,为任何一系列语句使用任何字典作为缓存。执行选项可以在语句上设置,在EngineConnection上设置,以及在使用 ORM Session.execute()方法进行 SQLAlchemy-2.0 风格调用时设置。例如,要运行一系列 SQL 语句并将它们缓存在特定字典中:

my_cache = {}
with engine.connect().execution_options(compiled_cache=my_cache) as conn:
    conn.execute(table.select())

SQLAlchemy ORM 在工作单元“flush”过程中使用上述技术来保留每个映射器缓存,这些缓存与Engine上配置的默认缓存分开,以及一些关系加载器查询。

也可以通过发送None值来禁用缓存:

# disable caching for this connection
with engine.connect().execution_options(compiled_cache=None) as conn:
    conn.execute(table.select())
```### 第三方方言的缓存
缓存功能要求方言的编译器生成安全可重用的 SQL 字符串,给定一个特定的与该 SQL 字符串关联的缓存键,这意味着语句中的任何文字值,例如 SELECT 的 LIMIT/OFFSET 值,不能在方言的编译方案中硬编码,因为编译后的字符串将无法重复使用。SQLAlchemy 支持使用`BindParameter.render_literal_execute()`方法呈现绑定参数,该方法可以应用于自定义编译器的现有`Select._limit_clause`和`Select._offset_clause`属性,这些属性稍后在本节中进行了说明。
由于有许多第三方方言,其中许多可能会从 SQL 语句中生成文字值而没有新的“文字执行”功能的好处,因此 SQLAlchemy 在版本 1.4.5 中为方言添加了一个名为`Dialect.supports_statement_cache`的属性。此属性在运行时直接在特定方言类上检查其是否存在,即使它已经存在于超类上,因此即使第三方方言是现有可缓存的 SQLAlchemy 方言的子类,比如`sqlalchemy.dialects.postgresql.PGDialect`,也必须明确包含此属性以启用缓存。该属性应该在方言经过必要的修改并经过测试以确保编译的 SQL 语句具有不同参数的可重用性后才能**启用**。
对于所有不支持此属性的第三方方言,该方言的日志将指示`方言不支持缓存`。
当方言经过缓存测试,特别是 SQL 编译器已更新以不直接在 SQL 字符串中呈现任何文字 LIMIT / OFFSET 时,方言作者可以按照以下方式应用该属性:
```py
from sqlalchemy.engine.default import DefaultDialect
class MyDialect(DefaultDialect):
    supports_statement_cache = True

标志需要应用到方言的所有子类中:

class MyDBAPIForMyDialect(MyDialect):
    supports_statement_cache = True

版本 1.4.5 中的新功能:添加了Dialect.supports_statement_cache属性。

方言修改的典型情况如下。

示例:使用后编译参数渲染 LIMIT / OFFSET

举个例子,假设一个方言重写了SQLCompiler.limit_clause()方法,该方法为 SQL 语句生成“LIMIT / OFFSET”子句,如下所示:

# pre 1.4 style code
def limit_clause(self, select, **kw):
    text = ""
    if select._limit is not None:
        text += " \n LIMIT %d" % (select._limit,)
    if select._offset is not None:
        text += " \n OFFSET %d" % (select._offset,)
    return text

上述例程将Select._limitSelect._offset整数值呈现为嵌入在 SQL 语句中的字面整数。这对于不支持在 SELECT 语句的 LIMIT/OFFSET 子句中使用绑定参数的数据库是常见的要求。但是,在初始编译阶段内呈现整数值直接不兼容缓存,因为Select对象的 limit 和 offset 整数值不是缓存键的一部分,因此许多带有不同 limit/offset 值的Select语句将无法以正确的值呈现。

以上代码的修正是将字面整数移到 SQLAlchemy 的后编译设施中,这将使字面整数在初始编译阶段之外渲染,而是在执行时在将语句发送到 DBAPI 之前。这在编译阶段使用BindParameter.render_literal_execute()方法进行访问,与使用Select._limit_clauseSelect._offset_clause属性结合使用,这些属性将 LIMIT/OFFSET 表示为完整的 SQL 表达式,如下所示:

# 1.4 cache-compatible code
def limit_clause(self, select, **kw):
    text = ""
    limit_clause = select._limit_clause
    offset_clause = select._offset_clause
    if select._simple_int_clause(limit_clause):
        text += " \n LIMIT %s" % (
            self.process(limit_clause.render_literal_execute(), **kw)
        )
    elif limit_clause is not None:
        # assuming the DB doesn't support SQL expressions for LIMIT.
        # Otherwise render here normally
        raise exc.CompileError(
            "dialect 'mydialect' can only render simple integers for LIMIT"
        )
    if select._simple_int_clause(offset_clause):
        text += " \n OFFSET %s" % (
            self.process(offset_clause.render_literal_execute(), **kw)
        )
    elif offset_clause is not None:
        # assuming the DB doesn't support SQL expressions for OFFSET.
        # Otherwise render here normally
        raise exc.CompileError(
            "dialect 'mydialect' can only render simple integers for OFFSET"
        )
    return text

上述方法将生成一个编译后的 SELECT 语句,看起来像:

SELECT  x  FROM  y
LIMIT  __[POSTCOMPILE_param_1]
OFFSET  __[POSTCOMPILE_param_2]

在上述情况下,__[POSTCOMPILE_param_1]__[POSTCOMPILE_param_2]指示符将在语句执行时填充其相应的整数值,此时 SQL 字符串已从缓存中检索出。

在做出适当的类似上述的更改后,应将Dialect.supports_statement_cache标志设置为True。强烈建议第三方方言使用dialect third party test suite,该套件将断言 SELECT 带有 LIMIT/OFFSET 的操作是否正确呈现和缓存。

另请参阅

我升级到 1.4 和/或 2.x 后,为什么我的应用程序变慢了? - 在常见问题部分 ### 使用 Lambda 在语句生成中添加显著的速度增益

深度炼金术

除了在非常性能密集的场景中通常是非必要的,并且专为有经验的 Python 程序员而设计,此技术也不适合初学者 Python 开发人员。虽然相当简单,但它涉及到不适合初学者 Python 开发人员的元编程概念。Lambda 方法可以稍后应用于现有代码,而只需付出最小的努力。

Python 函数,通常表示为 lambda,可以用于生成可基于 lambda 函数本身的 Python 代码位置以及 lambda 内的闭包变量进行缓存的 SQL 表达式。其原因是允许缓存 SQL 表达式构造的 SQL 字符串编译形式,这是 SQLAlchemy 在未使用 lambda 系统时的正常行为,以及 SQL 表达式构造本身的 Python 组合,这也具有一定程度的 Python 开销。

lambda SQL 表达式功能可作为性能增强功能使用,并且也可选择在 with_loader_criteria() ORM 选项中使用,以提供通用的 SQL 片段。

梗概

Lambda 语句是使用 lambda_stmt() 函数构造的,该函数返回 StatementLambdaElement 的实例,它本身是一个可执行的语句构造。可以使用 Python 加法运算符 + 或者 StatementLambdaElement.add_criteria() 方法向对象添加其他修饰符和条件,该方法允许更多选项。

假定 lambda_stmt() 构造在期望在应用程序中被多次使用的封闭函数或方法内被调用,以便超出第一次调用后的后续执行可以利用被缓存的编译 SQL。当 lambda 在 Python 的封闭函数内部构造时,也受到具有闭包变量的影响,这对整个方法至关重要:

from sqlalchemy import lambda_stmt
def run_my_statement(connection, parameter):
    stmt = lambda_stmt(lambda: select(table))
    stmt += lambda s: s.where(table.c.col == parameter)
    stmt += lambda s: s.order_by(table.c.id)
    return connection.execute(stmt)
with engine.connect() as conn:
    result = run_my_statement(some_connection, "some parameter")

在上面,用于定义 SELECT 语句结构的三个 lambda 可调用对象仅被调用一次,并且生成的 SQL 字符串被缓存在引擎的编译缓存中。从那时起,run_my_statement() 函数可以被调用任意次数,并且其中的 lambda 可调用对象不会被调用,而只用作缓存键来检索已编译的 SQL。

注意

当 lambda 系统未被使用时,已经存在 SQL 缓存,这一点很重要。lambda 系统只是在每个 SQL 语句调用时添加了额外的工作减少层,通过缓存 SQL 构建本身以及使用更简单的缓存键来实现。

Lambda 的快速指南

最重要的是,Lambda SQL 系统的重点是确保生成的 lambda 的缓存密钥与其将产生的 SQL 字符串之间永远不会不匹配。LambdaElement 和相关对象将运行并分析给定的 lambda,以计算每次运行时应如何缓存它,尝试检测任何潜在问题。基本指南包括:

  • 支持任何类型的语句 - 虽然 select() 构造预计是 lambda_stmt() 的主要用例,但 DML 语句,如 insert()update() 也同样可用:
def upd(id_, newname):
    stmt = lambda_stmt(lambda: users.update())
    stmt += lambda s: s.values(name=newname)
    stmt += lambda s: s.where(users.c.id == id_)
    return stmt
with engine.begin() as conn:
    conn.execute(upd(7, "foo"))
  • ORM 用例也直接支持 - lambda_stmt() 可完全容纳 ORM 功能,并可直接与 Session.execute() 一起使用:
def select_user(session, name):
    stmt = lambda_stmt(lambda: select(User))
    stmt += lambda s: s.where(User.name == name)
    row = session.execute(stmt).first()
    return row
  • 绑定参数会自动适应 - 与 SQLAlchemy 以前的“烘焙查询”系统相比,Lambda SQL 系统会自动适应成为 SQL 绑定参数的 Python 字面值。这意味着即使给定的 Lambda 只运行一次,但成为绑定参数的值会在每次运行时从 Lambda 的 闭包 中提取出来:
>>> def my_stmt(x, y):
...     stmt = lambda_stmt(lambda: select(func.max(x, y)))
...     return stmt
>>> engine = create_engine("sqlite://", echo=True)
>>> with engine.connect() as conn:
...     print(conn.scalar(my_stmt(5, 10)))
...     print(conn.scalar(my_stmt(12, 8)))
SELECT  max(?,  ?)  AS  max_1
[generated  in  0.00057s]  (5,  10)
10
SELECT  max(?,  ?)  AS  max_1
[cached  since  0.002059s  ago]  (12,  8)
12
  • 在上面的示例中,StatementLambdaElement 从每次调用 my_stmt() 时生成的 lambda 的 闭包 中提取了 xy 的值;这些值被替换为参数的值并嵌入到缓存的 SQL 结构中。
  • 理想情况下,Lambda 应该在所有情况下产生相同的 SQL 结构 - 避免在 lambda 内部使用条件语句或自定义可调用对象,这可能会根据输入产生不同的 SQL;如果函数可能会有条件地使用两个不同的 SQL 片段,请使用两个单独的 lambda:
# **Don't** do this:
def my_stmt(parameter, thing=False):
    stmt = lambda_stmt(lambda: select(table))
    stmt += lambda s: (
        s.where(table.c.x > parameter) if thing else s.where(table.c.y == parameter)
    )
    return stmt
# **Do** do this:
def my_stmt(parameter, thing=False):
    stmt = lambda_stmt(lambda: select(table))
    if thing:
        stmt += lambda s: s.where(table.c.x > parameter)
    else:
        stmt += lambda s: s.where(table.c.y == parameter)
    return stmt
  • 如果 lambda 不生成一致的 SQL 结构,则可能会发生各种失败,其中一些目前不容易检测到。
  • 不要在 lambda 内部使用函数生成绑定值 - 绑定值跟踪方法要求 SQL 语句中要使用的实际值在 lambda 的闭包中本地存在。如果值是从其他函数生成的,则不可能实现这一点,并且如果尝试执行此操作,LambdaElement通常会引发错误:
>>> def my_stmt(x, y):
...     def get_x():
...         return x
...
...     def get_y():
...         return y
...
...     stmt = lambda_stmt(lambda: select(func.max(get_x(), get_y())))
...     return stmt
>>> with engine.connect() as conn:
...     print(conn.scalar(my_stmt(5, 10)))
Traceback (most recent call last):
 # ...
sqlalchemy.exc.InvalidRequestError: Can't invoke Python callable get_x()
inside of lambda expression argument at
<code object <lambda> at 0x7fed15f350e0, file "<stdin>", line 6>;
lambda SQL constructs should not invoke functions from closure variables
to produce literal values since the lambda SQL system normally extracts
bound values without actually invoking the lambda or any functions within it.
  • 上面,如果需要,get_x()get_y()的使用应该在 lambda 的外部发生,并分配给本地闭包变量:
>>> def my_stmt(x, y):
...     def get_x():
...         return x
...
...     def get_y():
...         return y
...
...     x_param, y_param = get_x(), get_y()
...     stmt = lambda_stmt(lambda: select(func.max(x_param, y_param)))
...     return stmt
  • 避免在 lambda 内部引用非 SQL 构造,因为它们默认情况下不可缓存 - 这个问题涉及到LambdaElement如何从语句中的其他闭包变量创建缓存键。为了提供准确的缓存键的最佳保证,lambda 闭包中的所有对象都被认为是重要的,且默认情况下不会假设它们适合作为缓存键。因此,下面的示例也将引发一个相当详细的错误消息:
>>> class Foo:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
>>> def my_stmt(foo):
...     stmt = lambda_stmt(lambda: select(func.max(foo.x, foo.y)))
...     return stmt
>>> with engine.connect() as conn:
...     print(conn.scalar(my_stmt(Foo(5, 10))))
Traceback (most recent call last):
 # ...
sqlalchemy.exc.InvalidRequestError: Closure variable named 'foo' inside of
lambda callable <code object <lambda> at 0x7fed15f35450, file
"<stdin>", line 2> does not refer to a cacheable SQL element, and also
does not appear to be serving as a SQL literal bound value based on the
default SQL expression returned by the function.  This variable needs to
remain outside the scope of a SQL-generating lambda so that a proper cache
key may be generated from the lambda's state.  Evaluate this variable
outside of the lambda, set track_on=[<elements>] to explicitly select
closure elements to track, or set track_closure_variables=False to exclude
closure variables from being part of the cache key.
  • 上述错误表明LambdaElement不会假设传入的Foo对象在所有情况下都会保持相同的行为。它也不会默认假设它可以将Foo作为缓存键的一部分使用;如果将Foo对象用作缓存键的一部分,如果有许多不同的Foo对象,这将使缓存填满重复信息,并且还将长时间保留对所有这些对象的引用。
    解决上述情况的最佳方法是不要在 lambda 内部引用foo,而是在外部引用它:
>>> def my_stmt(foo):
...     x_param, y_param = foo.x, foo.y
...     stmt = lambda_stmt(lambda: select(func.max(x_param, y_param)))
...     return stmt
  • 在某些情况下,如果可以保证 lambda 的 SQL 结构不会根据输入改变,可以传递track_closure_variables=False来禁用对除绑定参数外的任何闭包变量的跟踪:
>>> def my_stmt(foo):
...     stmt = lambda_stmt(
...         lambda: select(func.max(foo.x, foo.y)), track_closure_variables=False
...     )
...     return stmt
  • 还有一种选择,即通过track_on参数将对象添加到元素中,以明确形成缓存键的一部分;使用此参数允许特定值作为缓存键,并且还将阻止考虑其他闭包变量。这对于构造的 SQL 的一部分源自某种上下文对象并且可能具有许多不同值的情况非常有用。在下面的示例中,SELECT 语句的第一个段将禁用对foo变量的跟踪,而第二个段将明确跟踪self作为缓存键的一部分:
>>> def my_stmt(self, foo):
...     stmt = lambda_stmt(
...         lambda: select(*self.column_expressions), track_closure_variables=False
...     )
...     stmt = stmt.add_criteria(lambda: self.where_criteria, track_on=[self])
...     return stmt
  • 使用track_on意味着给定的对象将长期存储在 lambda 的内部缓存中,并且只要缓存不清除这些对象(默认使用 1000 个条目的 LRU 方案)就会具有强引用。
缓存键生成

要理解与 lambda SQL 构造相关的一些选项和行为,了解缓存系统是有帮助的。

SQLAlchemy 的缓存系统通常通过生成一个表示构造内所有状态的结构来从给定的 SQL 表达式构造中生成缓存键:

>>> from sqlalchemy import select, column
>>> stmt = select(column("q"))
>>> cache_key = stmt._generate_cache_key()
>>> print(cache_key)  # somewhat paraphrased
CacheKey(key=(
 '0',
 <class 'sqlalchemy.sql.selectable.Select'>,
 '_raw_columns',
 (
 (
 '1',
 <class 'sqlalchemy.sql.elements.ColumnClause'>,
 'name',
 'q',
 'type',
 (
 <class 'sqlalchemy.sql.sqltypes.NullType'>,
 ),
 ),
 ),
 # a few more elements are here, and many more for a more
 # complicated SELECT statement
),)

上面的键存储在本质上是一个字典的缓存中,值是一个结构,其中包括 SQL 语句的字符串形式,本例中是短语 “SELECT q”。我们可以观察到,即使对于一个极短的查询,缓存键也非常冗长,因为它必须表示有关正在呈现和可能执行的所有内容。

相比之下,lambda 构造系统会创建一种不同类型的缓存键:

>>> from sqlalchemy import lambda_stmt
>>> stmt = lambda_stmt(lambda: select(column("q")))
>>> cache_key = stmt._generate_cache_key()
>>> print(cache_key)
CacheKey(key=(
 <code object <lambda> at 0x7fed1617c710, file "<stdin>", line 1>,
 <class 'sqlalchemy.sql.lambdas.StatementLambdaElement'>,
),)

上面,我们看到的缓存键比非 lambda 语句的要短得多,而且甚至生产 select(column("q")) 构造本身也不是必要的;Python lambda 本身包含一个称为 __code__ 的属性,它引用了一个在应用程序运行时是不可变和永久的 Python 代码对象。

当 lambda 还包含闭包变量时,在正常情况下,这些变量引用诸如列对象等 SQL 构造,它们将成为缓存键的一部分,或者如果它们引用将绑定参数的文字值,则它们将放置在缓存键的单独元素中:

>>> def my_stmt(parameter):
...     col = column("q")
...     stmt = lambda_stmt(lambda: select(col))
...     stmt += lambda s: s.where(col == parameter)
...     return stmt

上述 StatementLambdaElement 包含两个 lambda,两者都引用 col 闭包变量,因此缓存键将表示这两个段以及 column() 对象:

>>> stmt = my_stmt(5)
>>> key = stmt._generate_cache_key()
>>> print(key)
CacheKey(key=(
 <code object <lambda> at 0x7f07323c50e0, file "<stdin>", line 3>,
 (
 '0',
 <class 'sqlalchemy.sql.elements.ColumnClause'>,
 'name',
 'q',
 'type',
 (
 <class 'sqlalchemy.sql.sqltypes.NullType'>,
 ),
 ),
 <code object <lambda> at 0x7f07323c5190, file "<stdin>", line 4>,
 <class 'sqlalchemy.sql.lambdas.LinkedLambdaElement'>,
 (
 '0',
 <class 'sqlalchemy.sql.elements.ColumnClause'>,
 'name',
 'q',
 'type',
 (
 <class 'sqlalchemy.sql.sqltypes.NullType'>,
 ),
 ),
 (
 '0',
 <class 'sqlalchemy.sql.elements.ColumnClause'>,
 'name',
 'q',
 'type',
 (
 <class 'sqlalchemy.sql.sqltypes.NullType'>,
 ),
 ),
),)

缓存键的第二部分已检索到在调用语句时将使用的绑定参数:

>>> key.bindparams
[BindParameter('%(139668884281280 parameter)s', 5, type_=Integer())]

有关带有性能比较的 “lambda” 缓存的一系列示例,请参阅 性能 性能示例中的 “short_selects” 测试套件。

配置

缓存本身是一个名为 LRUCache 的类似字典的对象,它是一个内部 SQLAlchemy 字典子类,用于跟踪特定键的使用情况,并具有周期性的 “修剪” 步骤,当缓存的大小达到一定阈值时,将删除最近未使用的项目。该缓存的大小默认为 500,并可以使用 create_engine.query_cache_size 参数进行配置:

engine = create_engine(
    "postgresql+psycopg2://scott:tiger@localhost/test", query_cache_size=1200
)

缓存的大小可以增长为给定大小的 150%,然后将其修剪回目标大小。因此,大小为 1200 的缓存可以增长到 1800 个元素的大小,此时它将被修剪为 1200。

缓存的大小基于每个引擎呈现的唯一 SQL 语句的单个条目。来自 Core 和 ORM 的生成的 SQL 语句被等同对待。DDL 语句通常不会被缓存。为了确定缓存的行为,引擎日志将包括有关缓存行为的详细信息,将在下一节描述。

使用日志估算缓存性能

上述缓存大小为 1200 实际上相当大。对于小型应用程序,大小为 100 可能足够。要估算缓存的最佳大小,假设目标主机上有足够的内存,缓存的大小应基于可能在使用的目标引擎中呈现的唯一 SQL 字符串的数量。看到这一点最快捷的方法是使用 SQL 回显,最直接的方法是使用create_engine.echo标志启用,或使用 Python 记录;有关日志配置的背景,请参阅配置日志部分。

作为示例,我们将检查以下程序生成的日志:

from sqlalchemy import Column
from sqlalchemy import create_engine
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import select
from sqlalchemy import String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Session
Base = declarative_base()
class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    data = Column(String)
    bs = relationship("B")
class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))
    data = Column(String)
e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)
s = Session(e)
s.add_all([A(bs=[B(), B(), B()]), A(bs=[B(), B(), B()]), A(bs=[B(), B(), B()])])
s.commit()
for a_rec in s.scalars(select(A)):
    print(a_rec.bs)

运行时,每个记录的 SQL 语句都将在传递的参数左侧包含带方括号的缓存统计徽章。我们可能看到的四种消息总结如下:

  • [raw sql] - 驱动程序或最终用户使用Connection.exec_driver_sql()发出原始 SQL - 不适用缓存
  • [no key] - 该语句对象是一个未缓存的 DDL 语句,或者该语句对象包含无法缓存的元素,例如用户定义的构造或任意大的 VALUES 子句。
  • [generated in Xs] - 该语句是一个缓存未命中,必须编译,然后存储在缓存中。生成编译结构消耗了 X 秒。数字 X 将为小数秒。
  • [cached since Xs ago] - 该语句是一个缓存命中,无需重新编译。该语句自 X 秒前起已存储在缓存中。数字 X 将与应用程序运行的时间以及语句被缓存的时间成比例,因此,例如,对于 24 小时周期将为 86400。

下面更详细地描述了每个徽章。

对于上述程序,我们首先看到的语句将是 SQLite 方言检查“a”和“b”表是否存在:

INFO sqlalchemy.engine.Engine PRAGMA temp.table_info("a")
INFO sqlalchemy.engine.Engine [raw sql] ()
INFO sqlalchemy.engine.Engine PRAGMA main.table_info("b")
INFO sqlalchemy.engine.Engine [raw sql] ()

对于上面的两个 SQLite PRAGMA 语句,标记显示为 [原始 SQL],这表示驱动程序使用 Connection.exec_driver_sql() 将 Python 字符串直接发送到数据库。对这样的语句不适用缓存,因为它们已经以字符串形式存在,而且 SQLAlchemy 在事先不解析 SQL 字符串。

我们看到的下一条语句是 CREATE TABLE 语句:

INFO  sqlalchemy.engine.Engine
CREATE  TABLE  a  (
  id  INTEGER  NOT  NULL,
  data  VARCHAR,
  PRIMARY  KEY  (id)
)
INFO  sqlalchemy.engine.Engine  [no  key  0.00007s]  ()
INFO  sqlalchemy.engine.Engine
CREATE  TABLE  b  (
  id  INTEGER  NOT  NULL,
  a_id  INTEGER,
  data  VARCHAR,
  PRIMARY  KEY  (id),
  FOREIGN  KEY(a_id)  REFERENCES  a  (id)
)
INFO  sqlalchemy.engine.Engine  [no  key  0.00006s]  ()

对于这些语句的每个,标记显示为 [无键 0.00006s]。这表示这两个特定的语句,由于 DDL 导向的 CreateTable 构造未生成缓存键,因此未发生缓存。DDL 构造通常不参与缓存,因为它们通常不会被重复执行,而且 DDL 还是一个数据库配置步骤,性能并不那么关键。

[无键] 标记对另一个原因很重要,因为它可以用于生成可缓存的 SQL 语句,除了某些当前不可缓存的特定子构造。这些示例包括不定义缓存参数的自定义用户定义的 SQL 元素,以及一些生成任意长且不可重现的 SQL 字符串的构造,主要示例包括 Values 构造以及在使用 Insert.values() 方法进行“多值插入”时。

到目前为止,我们的缓存仍然是空的。然而,接下来的语句将被缓存,一个片段看起来像是:

INFO  sqlalchemy.engine.Engine  INSERT  INTO  a  (data)  VALUES  (?)
INFO  sqlalchemy.engine.Engine  [generated  in  0.00011s]  (None,)
INFO  sqlalchemy.engine.Engine  INSERT  INTO  a  (data)  VALUES  (?)
INFO  sqlalchemy.engine.Engine  [cached  since  0.0003533s  ago]  (None,)
INFO  sqlalchemy.engine.Engine  INSERT  INTO  a  (data)  VALUES  (?)
INFO  sqlalchemy.engine.Engine  [cached  since  0.0005326s  ago]  (None,)
INFO  sqlalchemy.engine.Engine  INSERT  INTO  b  (a_id,  data)  VALUES  (?,  ?)
INFO  sqlalchemy.engine.Engine  [generated  in  0.00010s]  (1,  None)
INFO  sqlalchemy.engine.Engine  INSERT  INTO  b  (a_id,  data)  VALUES  (?,  ?)
INFO  sqlalchemy.engine.Engine  [cached  since  0.0003232s  ago]  (1,  None)
INFO  sqlalchemy.engine.Engine  INSERT  INTO  b  (a_id,  data)  VALUES  (?,  ?)
INFO  sqlalchemy.engine.Engine  [cached  since  0.0004887s  ago]  (1,  None)

上面,我们基本上看到了两个唯一的 SQL 字符串;"INSERT INTO a (data) VALUES (?)""INSERT INTO b (a_id, data) VALUES (?, ?)"。由于 SQLAlchemy 对所有文本值使用绑定参数,即使这些语句为不同的对象重复多次,由于参数是分开的,实际的 SQL 字符串仍然相同。

注意

上述两个语句是由 ORM 工作单元流程生成的,实际上将这些语句缓存在每个映射器本地的单独缓存中。然而,机制和术语是相同的。下面的部分 禁用或使用备用字典来缓存一些(或全部)语句 将描述用户代码如何也可以在每个语句的基础上使用备用缓存容器。

我们在这两个语句的首次出现时看到的缓存徽章是[生成于 0.00011s]。这表明该语句不在缓存中,被编译为一个字符串需时 0.00011 秒,然后被缓存。当我们看到[生成]徽章时,我们知道这意味着缓存未命中。对于特定语句的首次出现,这是可以预料的。然而,如果在长时间运行的应用程序中频繁观察到大量新的[生成]徽章,而该应用程序通常会一遍又一遍地使用相同的一系列 SQL 语句,这可能是create_engine.query_cache_size参数设置过小的迹象。当一个被缓存的语句因为 LRU 缓存淘汰了较少使用的项而被驱逐出缓存时,它在下次使用时将显示[生成]徽章。

然后,我们看到每个这两个语句的后续出现所显示的缓存徽章类似于[缓存自 0.0003533 秒前]。这表明该语句在缓存中找到,并且最初是在 0.0003533 秒前放入缓存。需要注意的是,虽然[生成][缓存自]徽章都涉及到一定数量的秒数,但它们表示的是不同的含义;在[生成]的情况下,这个数字是编译语句所需的大致时间,将是一个极小的时间量。而在[缓存自]的情况下,这是语句在缓存中存在的总时间。对于运行了六小时的应用程序,这个数字可能会显示[缓存自 21600 秒前],这是一件好事。观察到“缓存自”数值较高是这些语句长时间没有遇到缓存未命中的迹象。即使应用程序运行了很长时间,频繁具有较低的“缓存自”数值的语句,可能表明这些语句太频繁地遇到缓存未命中,而create_engine.query_cache_size可能需要增加。

然后,我们的示例程序执行了一些 SELECT 查询,我们可以看到“生成”然后“缓存”的相同模式,对于“a”表的 SELECT 以及“b”表的后续惰性加载也是如此。

INFO sqlalchemy.engine.Engine SELECT a.id AS a_id, a.data AS a_data
FROM a
INFO sqlalchemy.engine.Engine [generated in 0.00009s] ()
INFO sqlalchemy.engine.Engine SELECT b.id AS b_id, b.a_id AS b_a_id, b.data AS b_data
FROM b
WHERE ? = b.a_id
INFO sqlalchemy.engine.Engine [generated in 0.00010s] (1,)
INFO sqlalchemy.engine.Engine SELECT b.id AS b_id, b.a_id AS b_a_id, b.data AS b_data
FROM b
WHERE ? = b.a_id
INFO sqlalchemy.engine.Engine [cached since 0.0005922s ago] (2,)
INFO sqlalchemy.engine.Engine SELECT b.id AS b_id, b.a_id AS b_a_id, b.data AS b_data
FROM b
WHERE ? = b.a_id

从我们上面的程序中,完整运行显示了总共四个不同的 SQL 字符串被缓存。这表明缓存大小为将足够。这显然是一个极小的大小,默认大小为 500 可以保持不变。

缓存使用了多少内存?

前一节详细介绍了一些技术,以检查是否需要增大 create_engine.query_cache_size。我们如何知道缓存大小是否不太大?我们可能希望设置 create_engine.query_cache_size 不要大于某个数值,因为我们的应用可能会使用非常多不同的语句,例如从搜索 UX 动态构建查询的应用程序,如果在过去 24 小时内运行了十万个不同的查询并且它们都被缓存,我们不希望主机耗尽内存。

测量 Python 数据结构占用的内存量非常困难,然而,通过使用 top 进程测量内存增长的过程,当连续添加 250 个新语句到缓存中时,暗示一个中等大小的核心语句大约占用 12K,而一个小型 ORM 语句大约占用 20K,其中包括 ORM 的结果获取结构,后者会更大。

禁用或使用替代字典缓存一些(或全部)语句

使用的内部缓存称为 LRUCache,但这基本上只是一个字典。可以通过将 Connection.execution_options.compiled_cache 选项作为执行选项来使用任何字典作为任何一系列语句的缓存。执行选项可以在语句、EngineConnection 上设置,以及在使用 SQLAlchemy-2.0 风格调用 ORM Session.execute() 方法时设置。例如,要运行一系列 SQL 语句并将它们缓存到特定字典中:

my_cache = {}
with engine.connect().execution_options(compiled_cache=my_cache) as conn:
    conn.execute(table.select())

SQLAlchemy ORM 使用上述技术在工作单元“刷新”过程中保持每个映射器缓存,这些缓存与 Engine 上配置的默认缓存分开,以及一些关系加载器查询。

通过将该参数设置为 None 可以禁用缓存:

# disable caching for this connection
with engine.connect().execution_options(compiled_cache=None) as conn:
    conn.execute(table.select())

第三方方言的缓存

缓存功能要求方言的编译器生成安全的 SQL 字符串,以便在给定特定缓存键的情况下重用许多语句调用,该缓存键与该 SQL 字符串的键对齐。这意味着语句中的任何字面值,例如 SELECT 的 LIMIT/OFFSET 值,不能在方言的编译方案中硬编码,因为编译后的字符串不可重复使用。SQLAlchemy 支持使用 BindParameter.render_literal_execute() 方法呈现绑定参数,该方法可以由自定义编译器应用于现有的 Select._limit_clauseSelect._offset_clause 属性,在本节后面有所说明。

由于存在许多第三方方言,其中许多可能从 SQL 语句中生成字面值而不使用较新的“字面量执行”功能,因此从版本 1.4.5 起,SQLAlchemy 已向方言添加了一个名为 Dialect.supports_statement_cache 的属性。此属性在运行时直接在特定方言的类上检查其存在,即使它已经存在于超类上,因此即使第三方方言是现有可缓存的 SQLAlchemy 方言的子类,例如 sqlalchemy.dialects.postgresql.PGDialect,仍然必须明确包含此属性以启用缓存。该属性应仅在方言已根据需要进行更改并已测试对具有不同参数的编译 SQL 语句的可重用性后才能启用。

对于所有不支持此属性的第三方方言,该方言的日志将指示 dialect does not support caching

当方言已经针对缓存进行了测试,并且特别是 SQL 编译器已经更新为不直接在 SQL 字符串中呈现任何字面 LIMIT / OFFSET 时,方言作者可以如下应用该属性:

from sqlalchemy.engine.default import DefaultDialect
class MyDialect(DefaultDialect):
    supports_statement_cache = True

该标志还需要应用于方言的所有子类:

class MyDBAPIForMyDialect(MyDialect):
    supports_statement_cache = True

新版本 1.4.5 中新增了 Dialect.supports_statement_cache 属性。

方言修改的典型案例如下。

示例:使用后编译参数呈现 LIMIT / OFFSET

例如,假设方言覆盖了 SQLCompiler.limit_clause() 方法,该方法为 SQL 语句生成“LIMIT / OFFSET”子句,如下所示:

# pre 1.4 style code
def limit_clause(self, select, **kw):
    text = ""
    if select._limit is not None:
        text += " \n LIMIT %d" % (select._limit,)
    if select._offset is not None:
        text += " \n OFFSET %d" % (select._offset,)
    return text

上述例程将Select._limitSelect._offset整数值呈现为嵌入在 SQL 语句中的文字整数。这是对于不支持在 SELECT 语句的 LIMIT/OFFSET 子句中使用绑定参数的数据库的常见要求。然而,在初始编译阶段呈现整数值直接不兼容缓存,因为Select对象的限制和偏移整数值不是缓存键的一部分,因此许多具有不同限制/偏移值的Select语句将无法正确呈现值。

以上代码的更正是将文字整数移至 SQLAlchemy 的后编译功能中,该功能将在初始编译阶段之外的执行时呈现文字整数,而是在将语句发送到 DBAPI 之前的执行时。这在编译阶段使用BindParameter.render_literal_execute()方法访问,同时使用Select._limit_clauseSelect._offset_clause属性,这些属性表示 LIMIT/OFFSET 作为完整的 SQL 表达式,如下所示:

# 1.4 cache-compatible code
def limit_clause(self, select, **kw):
    text = ""
    limit_clause = select._limit_clause
    offset_clause = select._offset_clause
    if select._simple_int_clause(limit_clause):
        text += " \n LIMIT %s" % (
            self.process(limit_clause.render_literal_execute(), **kw)
        )
    elif limit_clause is not None:
        # assuming the DB doesn't support SQL expressions for LIMIT.
        # Otherwise render here normally
        raise exc.CompileError(
            "dialect 'mydialect' can only render simple integers for LIMIT"
        )
    if select._simple_int_clause(offset_clause):
        text += " \n OFFSET %s" % (
            self.process(offset_clause.render_literal_execute(), **kw)
        )
    elif offset_clause is not None:
        # assuming the DB doesn't support SQL expressions for OFFSET.
        # Otherwise render here normally
        raise exc.CompileError(
            "dialect 'mydialect' can only render simple integers for OFFSET"
        )
    return text

上述方法将生成一个编译后的 SELECT 语句,看起来像:

SELECT  x  FROM  y
LIMIT  __[POSTCOMPILE_param_1]
OFFSET  __[POSTCOMPILE_param_2]

在上述情况下,__[POSTCOMPILE_param_1]__[POSTCOMPILE_param_2]指示符将在语句执行时填充其相应的整数值,此时 SQL 字符串已从缓存中检索出。

在适当进行类似上述更改之后,应将Dialect.supports_statement_cache标志设置为True。强烈建议第三方方言使用dialect 第三方测试套件,该套件将断言具有正确呈现和缓存的带有 LIMIT/OFFSET 的 SELECT 等操作。

另请参阅

升级到 1.4 和/或 2.x 后,为什么我的应用程序变慢? - 在常见问题部分

例子:使用后编译参数呈现 LIMIT / OFFSET

举例来说,假设一个方言重写了SQLCompiler.limit_clause()方法,该方法为 SQL 语句生成“LIMIT / OFFSET”子句,如下所示:

# pre 1.4 style code
def limit_clause(self, select, **kw):
    text = ""
    if select._limit is not None:
        text += " \n LIMIT %d" % (select._limit,)
    if select._offset is not None:
        text += " \n OFFSET %d" % (select._offset,)
    return text

上述例程将把 Select._limitSelect._offset 整数值呈现为嵌入在 SQL 语句中的字面整数。这是对于不支持在 SELECT 语句的 LIMIT/OFFSET 子句中使用绑定参数的数据库的常见要求。然而,将整数值呈现在初始编译阶段直接与缓存不兼容,因为 Select 对象的限制和偏移整数值不是缓存键的一部分,因此许多具有不同限制/偏移值的 Select 语句不会呈现正确的值。

以上代码的修正是将字面整数移动到 SQLAlchemy 的 post-compile 设施中,这将在初始编译阶段之外呈现字面整数,而是在执行时间之前将语句发送到 DBAPI。这在编译阶段使用 BindParameter.render_literal_execute() 方法访问,结合使用 Select._limit_clauseSelect._offset_clause 属性,这些属性表示 LIMIT/OFFSET 作为完整 SQL 表达式,如下所示:

# 1.4 cache-compatible code
def limit_clause(self, select, **kw):
    text = ""
    limit_clause = select._limit_clause
    offset_clause = select._offset_clause
    if select._simple_int_clause(limit_clause):
        text += " \n LIMIT %s" % (
            self.process(limit_clause.render_literal_execute(), **kw)
        )
    elif limit_clause is not None:
        # assuming the DB doesn't support SQL expressions for LIMIT.
        # Otherwise render here normally
        raise exc.CompileError(
            "dialect 'mydialect' can only render simple integers for LIMIT"
        )
    if select._simple_int_clause(offset_clause):
        text += " \n OFFSET %s" % (
            self.process(offset_clause.render_literal_execute(), **kw)
        )
    elif offset_clause is not None:
        # assuming the DB doesn't support SQL expressions for OFFSET.
        # Otherwise render here normally
        raise exc.CompileError(
            "dialect 'mydialect' can only render simple integers for OFFSET"
        )
    return text

上述方法将生成一个编译后的 SELECT 语句,如下所示:

SELECT  x  FROM  y
LIMIT  __[POSTCOMPILE_param_1]
OFFSET  __[POSTCOMPILE_param_2]

在上述情况下,__[POSTCOMPILE_param_1]__[POSTCOMPILE_param_2] 指示符将在语句执行时用其对应的整数值填充,此时 SQL 字符串已从缓存中检索到。

在适当进行了类似上述更改之后,应将 Dialect.supports_statement_cache 标志设置为 True。强烈建议第三方方言使用 方言第三方测试套件,该测试套件将断言像带有 LIMIT/OFFSET 的 SELECT 语句的操作是否正确呈现和缓存。

另请参阅

为什么升级到 1.4 和/或 2.x 后我的应用变慢? - 在 常见问题解答 部分

使用 Lambda 来显著提高语句生成的速度

深度合成

此技术通常在非常性能密集的情况下非必要,并且面向经验丰富的 Python 程序员。虽然相当简单直接,但涉及到不适合初学者 Python 开发者的元编程概念。Lambda 方法可以稍后应用于现有代码,而付出的努力很小。

Python 函数通常以 lambda 表达式的形式表达,可以用于生成可基于 lambda 函数本身的 Python 代码位置和 lambda 内的闭包变量进行缓存的 SQL 表达式。其原理是允许缓存不仅是 SQL 表达式构造的 SQL 字符串编译形式,这是 SQLAlchemy 在未使用 lambda 系统时的正常行为,还有 SQL 表达式构造本身的 Python 组合,这也具有一定程度的 Python 开销。

Lambda SQL 表达式特性可作为性能增强功能使用,也可选择性地用于 with_loader_criteria() ORM 选项中,以提供通用的 SQL 片段。

概要

Lambda 语句使用 lambda_stmt() 函数构建,该函数返回一个 StatementLambdaElement 实例,它本身是可执行的语句构造。可以使用 Python 加法运算符 + 或者 StatementLambdaElement.add_criteria() 方法向对象添加额外的修改器和条件,从而提供更多选项。

假设 lambda_stmt() 构造被调用在一个期望在应用程序中多次使用的封闭函数或方法中,以便在第一次执行之后可以利用已缓存的编译 SQL。当 lambda 在 Python 的封闭函数内构建时,它也可能具有闭包变量,这对整个方法至关重要:

from sqlalchemy import lambda_stmt
def run_my_statement(connection, parameter):
    stmt = lambda_stmt(lambda: select(table))
    stmt += lambda s: s.where(table.c.col == parameter)
    stmt += lambda s: s.order_by(table.c.id)
    return connection.execute(stmt)
with engine.connect() as conn:
    result = run_my_statement(some_connection, "some parameter")

在上述例子中,用于定义 SELECT 语句结构的三个 lambda 可调用对象仅被调用一次,并且生成的 SQL 字符串被缓存到引擎的编译缓存中。从那时起,可以多次调用 run_my_statement() 函数,而其中的 lambda 可调用对象不会被再次调用,仅用作缓存键以检索已经编译的 SQL。

注意

需要注意的是,当未使用 lambda 系统时,已经存在 SQL 缓存。Lambda 系统只是在每个 SQL 语句调用时增加了额外的工作减少层,通过缓存构建 SQL 构造本身并且使用更简单的缓存键。

Lambda 的快速指南

最重要的是,在 lambda SQL 系统中,重点是确保为 lambda 生成的缓存键和它将产生的 SQL 字符串之间永远不会出现不匹配。LambdaElement 和相关对象将运行和分析给定的 lambda,以计算在每次运行时应如何缓存它,试图检测任何潜在问题。基本准则包括:

  • 支持任何类型的语句 - 虽然预期 select() 构造是 lambda_stmt() 的主要用例,但诸如 insert()update() 等 DML 语句同样可用:
def upd(id_, newname):
    stmt = lambda_stmt(lambda: users.update())
    stmt += lambda s: s.values(name=newname)
    stmt += lambda s: s.where(users.c.id == id_)
    return stmt
with engine.begin() as conn:
    conn.execute(upd(7, "foo"))
  • ORM 使用案例也得到直接支持 - lambda_stmt() 可完全适应 ORM 功能,并可直接与 Session.execute() 一起使用:
def select_user(session, name):
    stmt = lambda_stmt(lambda: select(User))
    stmt += lambda s: s.where(User.name == name)
    row = session.execute(stmt).first()
    return row
  • 绑定参数会自动适应 - 与 SQLAlchemy 以前的“烘焙查询”系统相比,lambda SQL 系统会自动适应成为 SQL 绑定参数的 Python 文本值。这意味着即使给定的 lambda 只运行一次,但成为绑定参数的值是从 lambda 的 闭包 中提取的,每次运行都会提取:
>>> def my_stmt(x, y):
...     stmt = lambda_stmt(lambda: select(func.max(x, y)))
...     return stmt
>>> engine = create_engine("sqlite://", echo=True)
>>> with engine.connect() as conn:
...     print(conn.scalar(my_stmt(5, 10)))
...     print(conn.scalar(my_stmt(12, 8)))
SELECT  max(?,  ?)  AS  max_1
[generated  in  0.00057s]  (5,  10)
10
SELECT  max(?,  ?)  AS  max_1
[cached  since  0.002059s  ago]  (12,  8)
12
  • 在上面的例子中,StatementLambdaElement 从每次调用 my_stmt() 时生成的 lambda 的 闭包 中提取了 xy 的值;这些值被替换为参数的值,并缓存到 SQL 构造中。
  • 理想情况下,lambda 应该在所有情况下产生相同的 SQL 结构 - 避免在 lambda 内部使用条件语句或自定义可调用对象,这可能会根据输入产生不同的 SQL;如果一个函数可能会有条件地使用两个不同的 SQL 片段,那么请使用两个单独的 lambda:
# **Don't** do this:
def my_stmt(parameter, thing=False):
    stmt = lambda_stmt(lambda: select(table))
    stmt += lambda s: (
        s.where(table.c.x > parameter) if thing else s.where(table.c.y == parameter)
    )
    return stmt
# **Do** do this:
def my_stmt(parameter, thing=False):
    stmt = lambda_stmt(lambda: select(table))
    if thing:
        stmt += lambda s: s.where(table.c.x > parameter)
    else:
        stmt += lambda s: s.where(table.c.y == parameter)
    return stmt
  • 如果 lambda 未产生一致的 SQL 构造,可能会发生各种失败,并且有些失败目前并不容易检测到。
  • 不要在 lambda 内部使用函数来产生绑定值 - 绑定值跟踪方法要求 SQL 语句中要使用的实际值在 lambda 的闭包中是本地存在的。如果值是从其他函数生成的,则这是不可能的,并且LambdaElement通常应该在尝试这样做时引发错误:
>>> def my_stmt(x, y):
...     def get_x():
...         return x
...
...     def get_y():
...         return y
...
...     stmt = lambda_stmt(lambda: select(func.max(get_x(), get_y())))
...     return stmt
>>> with engine.connect() as conn:
...     print(conn.scalar(my_stmt(5, 10)))
Traceback (most recent call last):
 # ...
sqlalchemy.exc.InvalidRequestError: Can't invoke Python callable get_x()
inside of lambda expression argument at
<code object <lambda> at 0x7fed15f350e0, file "<stdin>", line 6>;
lambda SQL constructs should not invoke functions from closure variables
to produce literal values since the lambda SQL system normally extracts
bound values without actually invoking the lambda or any functions within it.
  • 上述情况下,如果需要使用get_x()get_y(),应该在 lambda 外部定义并分配给一个本地闭包变量:
>>> def my_stmt(x, y):
...     def get_x():
...         return x
...
...     def get_y():
...         return y
...
...     x_param, y_param = get_x(), get_y()
...     stmt = lambda_stmt(lambda: select(func.max(x_param, y_param)))
...     return stmt
  • 避免在 lambda 内部引用非 SQL 构造,因为它们默认情况下不能被缓存 - 这个问题涉及到LambdaElement如何从语句中的其他闭包变量创建缓存键。为了提供准确的缓存键保证,lambda 闭包中的所有对象都被认为是重要的,而且默认情况下不会被假定适合作为缓存键。因此,以下示例也会引发一个相当详细的错误消息:
>>> class Foo:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
>>> def my_stmt(foo):
...     stmt = lambda_stmt(lambda: select(func.max(foo.x, foo.y)))
...     return stmt
>>> with engine.connect() as conn:
...     print(conn.scalar(my_stmt(Foo(5, 10))))
Traceback (most recent call last):
 # ...
sqlalchemy.exc.InvalidRequestError: Closure variable named 'foo' inside of
lambda callable <code object <lambda> at 0x7fed15f35450, file
"<stdin>", line 2> does not refer to a cacheable SQL element, and also
does not appear to be serving as a SQL literal bound value based on the
default SQL expression returned by the function.  This variable needs to
remain outside the scope of a SQL-generating lambda so that a proper cache
key may be generated from the lambda's state.  Evaluate this variable
outside of the lambda, set track_on=[<elements>] to explicitly select
closure elements to track, or set track_closure_variables=False to exclude
closure variables from being part of the cache key.
  • 上述错误表明LambdaElement不会假定传递的Foo对象在所有情况下都会继续以相同的方式工作。它也不会假定默认情况下可以将Foo用作缓存键的一部分;如果它要将Foo对象用作缓存键,如果有许多不同的Foo对象,这将填满缓存重复信息,并且还将长期持有对所有这些对象的引用。
    解决上述情况的最佳方法是不要在 lambda 内部引用foo,而是在外部引用:
>>> def my_stmt(foo):
...     x_param, y_param = foo.x, foo.y
...     stmt = lambda_stmt(lambda: select(func.max(x_param, y_param)))
...     return stmt
  • 在某些情况下,如果 lambda 的 SQL 结构保证不会根据输入而改变,则可以传递track_closure_variables=False,这将禁用除绑定参数之外的任何闭包变量的跟踪:
>>> def my_stmt(foo):
...     stmt = lambda_stmt(
...         lambda: select(func.max(foo.x, foo.y)), track_closure_variables=False
...     )
...     return stmt
  • 还有一个选项是将对象添加到元素中,明确形成缓存键的一部分,使用track_on参数;使用该参数允许特定值作为缓存键,并且还将防止考虑其他闭包变量。这对于 SQL 的一部分是来自某种上下文对象的情况很有用,该对象可能具有许多不同的值。在下面的示例中,SELECT 语句的第一个段将禁用对foo变量的跟踪,而第二个段将明确跟踪self作为缓存键的一部分:
>>> def my_stmt(self, foo):
...     stmt = lambda_stmt(
...         lambda: select(*self.column_expressions), track_closure_variables=False
...     )
...     stmt = stmt.add_criteria(lambda: self.where_criteria, track_on=[self])
...     return stmt
  • 使用track_on意味着给定的对象将长期存储在 lambda 的内部缓存中,并且只要缓存不清除这些对象(默认情况下使用 1000 个条目的 LRU 方案)就会有强引用。
缓存键生成

为了理解 lambda SQL 构造中发生的一些选项和行为,了解缓存系统是有帮助的。

SQLAlchemy 的缓存系统通常通过生成一个表示构造内所有状态的结构来从给定的 SQL 表达式构造生成一个缓存键:

>>> from sqlalchemy import select, column
>>> stmt = select(column("q"))
>>> cache_key = stmt._generate_cache_key()
>>> print(cache_key)  # somewhat paraphrased
CacheKey(key=(
 '0',
 <class 'sqlalchemy.sql.selectable.Select'>,
 '_raw_columns',
 (
 (
 '1',
 <class 'sqlalchemy.sql.elements.ColumnClause'>,
 'name',
 'q',
 'type',
 (
 <class 'sqlalchemy.sql.sqltypes.NullType'>,
 ),
 ),
 ),
 # a few more elements are here, and many more for a more
 # complicated SELECT statement
),)

上述键存储在本质上是一个字典的缓存中,值是一个构造,其中包括 SQL 语句的字符串形式,本例中是短语 “SELECT q”。我们可以观察到,即使对于一个非常简短的查询,缓存键也相当冗长,因为它必须表示关于正在渲染和潜在执行的一切变化。

与此相反,lambda 构造系统创建了一种不同类型的缓存键:

>>> from sqlalchemy import lambda_stmt
>>> stmt = lambda_stmt(lambda: select(column("q")))
>>> cache_key = stmt._generate_cache_key()
>>> print(cache_key)
CacheKey(key=(
 <code object <lambda> at 0x7fed1617c710, file "<stdin>", line 1>,
 <class 'sqlalchemy.sql.lambdas.StatementLambdaElement'>,
),)

在上面,我们看到的缓存键远远比非 lambda 语句的键要短得多,并且甚至生产 select(column("q")) 构造本身也是不必要的;Python lambda 本身包含一个称为 __code__ 的属性,该属性引用应用程序运行时中不可变且永久的 Python 代码对象。

当 lambda 还包含闭包变量时,在这些变量引用 SQL 构造(如列对象)的常规情况下,它们将成为缓存键的一部分;或者如果它们引用将成为绑定参数的文字值,则它们将放置在缓存键的一个单独元素中:

>>> def my_stmt(parameter):
...     col = column("q")
...     stmt = lambda_stmt(lambda: select(col))
...     stmt += lambda s: s.where(col == parameter)
...     return stmt

上述 StatementLambdaElement 包括两个 lambda,两者都引用 col 闭包变量,因此缓存键将表示这两个段以及 column() 对象:

>>> stmt = my_stmt(5)
>>> key = stmt._generate_cache_key()
>>> print(key)
CacheKey(key=(
 <code object <lambda> at 0x7f07323c50e0, file "<stdin>", line 3>,
 (
 '0',
 <class 'sqlalchemy.sql.elements.ColumnClause'>,
 'name',
 'q',
 'type',
 (
 <class 'sqlalchemy.sql.sqltypes.NullType'>,
 ),
 ),
 <code object <lambda> at 0x7f07323c5190, file "<stdin>", line 4>,
 <class 'sqlalchemy.sql.lambdas.LinkedLambdaElement'>,
 (
 '0',
 <class 'sqlalchemy.sql.elements.ColumnClause'>,
 'name',
 'q',
 'type',
 (
 <class 'sqlalchemy.sql.sqltypes.NullType'>,
 ),
 ),
 (
 '0',
 <class 'sqlalchemy.sql.elements.ColumnClause'>,
 'name',
 'q',
 'type',
 (
 <class 'sqlalchemy.sql.sqltypes.NullType'>,
 ),
 ),
),)

缓存键的第二部分已检索到将在调用语句时使用的绑定参数:

>>> key.bindparams
[BindParameter('%(139668884281280 parameter)s', 5, type_=Integer())]

关于“lambda”缓存的一系列示例及性能比较,请参见性能示例中的“short_selects”测试套件。

概要

Lambda 语句使用 lambda_stmt() 函数构建,该函数返回一个 StatementLambdaElement 实例,它本身是一个可执行语句构造。使用 Python 加法运算符 + 或者 StatementLambdaElement.add_criteria() 方法可以向对象添加其他修饰符和条件。

假定 lambda_stmt() 构造被调用在一个期望在应用程序中被多次使用的封闭函数或方法内部,以便后续的执行除了第一次之外都可以利用已编译的 SQL 的缓存。当 lambda 在 Python 的封闭函数内部构造时,它也受到闭包变量的影响,这对整个方法都是重要的:

from sqlalchemy import lambda_stmt
def run_my_statement(connection, parameter):
    stmt = lambda_stmt(lambda: select(table))
    stmt += lambda s: s.where(table.c.col == parameter)
    stmt += lambda s: s.order_by(table.c.id)
    return connection.execute(stmt)
with engine.connect() as conn:
    result = run_my_statement(some_connection, "some parameter")

在上面的例子中,用于定义 SELECT 语句结构的三个 lambda 可调用对象仅被调用一次,并且生成的 SQL 字符串被缓存到引擎的编译缓存中。从那时起,run_my_statement() 函数可以被调用任意次数,而其中的 lambda 可调用对象将不会被调用,只会被用作缓存键来检索已经编译的 SQL。

注意

注意,当未使用 lambda 系统时,已经存在 SQL 缓存。lambda 系统只是在每个 SQL 语句调用时添加了一个额外的工作减少层,通过缓存 SQL 构造的构建以及使用一个更简单的缓存键。

Lambdas 的快速指南

最重要的是,在 lambda SQL 系统中,重点是确保生成的 lambda 的缓存键与它将产生的 SQL 字符串之间永远不会不匹配。LambdaElement 和相关对象将运行和分析给定的 lambda,以便计算应该在每次运行时如何缓存它,试图检测任何潜在问题。基本指南包括:

  • 支持任何类型的语句 - 虽然预期 select() 构造是 lambda_stmt() 的主要用例,但诸如 insert()update() 的 DML 语句同样可用:
def upd(id_, newname):
    stmt = lambda_stmt(lambda: users.update())
    stmt += lambda s: s.values(name=newname)
    stmt += lambda s: s.where(users.c.id == id_)
    return stmt
with engine.begin() as conn:
    conn.execute(upd(7, "foo"))
  • ORM 用例直接支持 - lambda_stmt() 完全可以容纳 ORM 功能,并直接与 Session.execute() 一起使用:
def select_user(session, name):
    stmt = lambda_stmt(lambda: select(User))
    stmt += lambda s: s.where(User.name == name)
    row = session.execute(stmt).first()
    return row
  • 绑定参数会自动适应 - 与 SQLAlchemy 以前的“烘焙查询”系统相比,lambda SQL 系统会自动适应成为 SQL 绑定参数的 Python 文字值。这意味着即使给定的 lambda 只运行一次,成为绑定参数的值也会在每次运行时从 lambda 的闭包中提取出来:
>>> def my_stmt(x, y):
...     stmt = lambda_stmt(lambda: select(func.max(x, y)))
...     return stmt
>>> engine = create_engine("sqlite://", echo=True)
>>> with engine.connect() as conn:
...     print(conn.scalar(my_stmt(5, 10)))
...     print(conn.scalar(my_stmt(12, 8)))
SELECT  max(?,  ?)  AS  max_1
[generated  in  0.00057s]  (5,  10)
10
SELECT  max(?,  ?)  AS  max_1
[cached  since  0.002059s  ago]  (12,  8)
12
  • 上面,StatementLambdaElement 从每次调用 my_stmt() 时生成的 lambda 的闭包中提取了 xy 的值;这些值被替换为参数的值,并缓存在 SQL 构造中。
  • lambda 最好在所有情况下生成相同的 SQL 结构 - 避免在 lambda 内部使用条件语句或自定义可调用对象,这些可能会基于输入生成不同的 SQL;如果函数可能会有条件地使用两个不同的 SQL 片段,请使用两个单独的 lambda:
# **Don't** do this:
def my_stmt(parameter, thing=False):
    stmt = lambda_stmt(lambda: select(table))
    stmt += lambda s: (
        s.where(table.c.x > parameter) if thing else s.where(table.c.y == parameter)
    )
    return stmt
# **Do** do this:
def my_stmt(parameter, thing=False):
    stmt = lambda_stmt(lambda: select(table))
    if thing:
        stmt += lambda s: s.where(table.c.x > parameter)
    else:
        stmt += lambda s: s.where(table.c.y == parameter)
    return stmt
  • 如果 lambda 表达式不能生成一致的 SQL 结构,可能会发生各种故障,其中一些目前并不容易检测到。
  • 不要在 lambda 内部使用函数生成绑定值 - 绑定值跟踪方法要求 SQL 语句中要使用的实际值在 lambda 的闭包中局部存在。如果值是从其他函数生成的,则这是不可能的,并且如果尝试这样做,LambdaElement 通常应该引发错误:
>>> def my_stmt(x, y):
...     def get_x():
...         return x
...
...     def get_y():
...         return y
...
...     stmt = lambda_stmt(lambda: select(func.max(get_x(), get_y())))
...     return stmt
>>> with engine.connect() as conn:
...     print(conn.scalar(my_stmt(5, 10)))
Traceback (most recent call last):
 # ...
sqlalchemy.exc.InvalidRequestError: Can't invoke Python callable get_x()
inside of lambda expression argument at
<code object <lambda> at 0x7fed15f350e0, file "<stdin>", line 6>;
lambda SQL constructs should not invoke functions from closure variables
to produce literal values since the lambda SQL system normally extracts
bound values without actually invoking the lambda or any functions within it.
  • 上面,如果必要,应该在 lambda 外部使用 get_x()get_y(),并将其分配给本地闭包变量:
>>> def my_stmt(x, y):
...     def get_x():
...         return x
...
...     def get_y():
...         return y
...
...     x_param, y_param = get_x(), get_y()
...     stmt = lambda_stmt(lambda: select(func.max(x_param, y_param)))
...     return stmt
  • 避免在 lambda 内部引用非 SQL 结构,因为它们默认情况下无法缓存 - 此问题涉及 LambdaElement 如何从语句中的其他闭包变量创建缓存键。为了提供对准确缓存键的最佳保证, lambda 中闭包中的所有对象都被认为是重要的,且默认情况下不会假设适用于缓存键。因此,以下示例也会引发相当详细的错误消息:
>>> class Foo:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
>>> def my_stmt(foo):
...     stmt = lambda_stmt(lambda: select(func.max(foo.x, foo.y)))
...     return stmt
>>> with engine.connect() as conn:
...     print(conn.scalar(my_stmt(Foo(5, 10))))
Traceback (most recent call last):
 # ...
sqlalchemy.exc.InvalidRequestError: Closure variable named 'foo' inside of
lambda callable <code object <lambda> at 0x7fed15f35450, file
"<stdin>", line 2> does not refer to a cacheable SQL element, and also
does not appear to be serving as a SQL literal bound value based on the
default SQL expression returned by the function.  This variable needs to
remain outside the scope of a SQL-generating lambda so that a proper cache
key may be generated from the lambda's state.  Evaluate this variable
outside of the lambda, set track_on=[<elements>] to explicitly select
closure elements to track, or set track_closure_variables=False to exclude
closure variables from being part of the cache key.
  • 上述错误表明 LambdaElement 不会假设传入的 Foo 对象在所有情况下都会保持相同的行为。它也不会假设默认情况下可以将 Foo 用作缓存键的一部分;如果将 Foo 对象用作缓存键的一部分,如果有许多不同的 Foo 对象,这将填满缓存并且还会对所有这些对象保持长时间的引用。
    解决上述情况的最佳方法是不在 Lambda 内部引用 foo,而是在外部引用它:
>>> def my_stmt(foo):
...     x_param, y_param = foo.x, foo.y
...     stmt = lambda_stmt(lambda: select(func.max(x_param, y_param)))
...     return stmt
  • 在某些情况下,如果 Lambda 的 SQL 结构保证不会根据输入改变,可以传递 track_closure_variables=False,这将禁用对除了用于绑定参数的变量之外的任何闭包变量的跟踪:
>>> def my_stmt(foo):
...     stmt = lambda_stmt(
...         lambda: select(func.max(foo.x, foo.y)), track_closure_variables=False
...     )
...     return stmt
  • 还有一个选项可以将对象添加到元素中,以明确形成缓存键的一部分,使用 track_on 参数;使用此参数允许特定值作为缓存键,并且还会阻止其他闭包变量被考虑。这对于 SQL 的一部分构造源自某种上下文对象且可能具有许多不同值的情况非常有用。在下面的示例中,SELECT 语句的第一个片段将禁用对 foo 变量的跟踪,而第二个片段将明确跟踪 self 作为缓存键的一部分:
>>> def my_stmt(self, foo):
...     stmt = lambda_stmt(
...         lambda: select(*self.column_expressions), track_closure_variables=False
...     )
...     stmt = stmt.add_criteria(lambda: self.where_criteria, track_on=[self])
...     return stmt
  • 使用 track_on 意味着给定对象将长期存储在 Lambda 的内部缓存中,并且只要缓存不清除这些对象(默认使用 1000 条记录的 LRU 方案),它们就会具有强引用。
缓存键生成

为了理解 Lambda SQL 构造中发生的一些选项和行为,了解缓存系统是有帮助的。

SQLAlchemy 的缓存系统通常通过生成一个表示构造内所有状态的结构来从给定的 SQL 表达式构造生成缓存键:

>>> from sqlalchemy import select, column
>>> stmt = select(column("q"))
>>> cache_key = stmt._generate_cache_key()
>>> print(cache_key)  # somewhat paraphrased
CacheKey(key=(
 '0',
 <class 'sqlalchemy.sql.selectable.Select'>,
 '_raw_columns',
 (
 (
 '1',
 <class 'sqlalchemy.sql.elements.ColumnClause'>,
 'name',
 'q',
 'type',
 (
 <class 'sqlalchemy.sql.sqltypes.NullType'>,
 ),
 ),
 ),
 # a few more elements are here, and many more for a more
 # complicated SELECT statement
),)

上述键存储在基本上是字典的缓存中,而值是一个构造,其中包括 SQL 语句的字符串形式,本例中是短语 “SELECT q”。我们可以观察到,即使是一个极短的查询,缓存键也非常冗长,因为它必须表示关于正在渲染和潜在执行的所有可能变化的内容。

相比之下,Lambda 构造系统创建了一种不同类型的缓存键:

>>> from sqlalchemy import lambda_stmt
>>> stmt = lambda_stmt(lambda: select(column("q")))
>>> cache_key = stmt._generate_cache_key()
>>> print(cache_key)
CacheKey(key=(
 <code object <lambda> at 0x7fed1617c710, file "<stdin>", line 1>,
 <class 'sqlalchemy.sql.lambdas.StatementLambdaElement'>,
),)

以上是一个缓存键,远比非 Lambda 语句的要短得多,而且此处生产 select(column("q")) 构造本身甚至都不是必要的;Python Lambda 本身包含一个名为 __code__ 的属性,指向一个在应用程序运行时不可变且永久存在的 Python 代码对象。

当 Lambda 也包含闭包变量时,在这些变量引用 SQL 构造(如列对象)的常规情况下,它们成为缓存键的一部分,或者如果它们引用将作为绑定参数的文字值,则将它们放在缓存键的一个单独元素中:

>>> def my_stmt(parameter):
...     col = column("q")
...     stmt = lambda_stmt(lambda: select(col))
...     stmt += lambda s: s.where(col == parameter)
...     return stmt

上述 StatementLambdaElement 包含两个 lambda,两者都引用了 col 闭包变量,因此缓存键将表示这两个段以及 column() 对象。

>>> stmt = my_stmt(5)
>>> key = stmt._generate_cache_key()
>>> print(key)
CacheKey(key=(
 <code object <lambda> at 0x7f07323c50e0, file "<stdin>", line 3>,
 (
 '0',
 <class 'sqlalchemy.sql.elements.ColumnClause'>,
 'name',
 'q',
 'type',
 (
 <class 'sqlalchemy.sql.sqltypes.NullType'>,
 ),
 ),
 <code object <lambda> at 0x7f07323c5190, file "<stdin>", line 4>,
 <class 'sqlalchemy.sql.lambdas.LinkedLambdaElement'>,
 (
 '0',
 <class 'sqlalchemy.sql.elements.ColumnClause'>,
 'name',
 'q',
 'type',
 (
 <class 'sqlalchemy.sql.sqltypes.NullType'>,
 ),
 ),
 (
 '0',
 <class 'sqlalchemy.sql.elements.ColumnClause'>,
 'name',
 'q',
 'type',
 (
 <class 'sqlalchemy.sql.sqltypes.NullType'>,
 ),
 ),
),)

缓存键的第二部分已检索出在调用语句时将使用的绑定参数:

>>> key.bindparams
[BindParameter('%(139668884281280 parameter)s', 5, type_=Integer())]

有关使用性能比较的“lambda”缓存的一系列示例,请参见性能 示例中的 “short_selects” 测试套件。


SqlAlchemy 2.0 中文文档(四十四)(9)https://developer.aliyun.com/article/1563076

相关实践学习
Github实时数据分析与可视化
基于Github Archive公开数据集,将项目、行为等20+种事件类型数据实时采集至Hologres进行分析,并搭建可视化大屏。
阿里云实时数仓实战 - 项目介绍及架构设计
课程简介 1)学习搭建一个数据仓库的过程,理解数据在整个数仓架构的从采集、存储、计算、输出、展示的整个业务流程。 2)整个数仓体系完全搭建在阿里云架构上,理解并学会运用各个服务组件,了解各个组件之间如何配合联动。 3&nbsp;)前置知识要求 &nbsp; 课程大纲 第一章&nbsp;了解数据仓库概念 初步了解数据仓库是干什么的 第二章&nbsp;按照企业开发的标准去搭建一个数据仓库 数据仓库的需求是什么 架构 怎么选型怎么购买服务器 第三章&nbsp;数据生成模块 用户形成数据的一个准备 按照企业的标准,准备了十一张用户行为表 方便使用 第四章&nbsp;采集模块的搭建 购买阿里云服务器 安装 JDK 安装 Flume 第五章&nbsp;用户行为数据仓库 严格按照企业的标准开发 第六章&nbsp;搭建业务数仓理论基础和对表的分类同步 第七章&nbsp;业务数仓的搭建&nbsp; 业务行为数仓效果图&nbsp;&nbsp;
相关文章
|
3月前
|
SQL 缓存 关系型数据库
SqlAlchemy 2.0 中文文档(四十四)(2)
SqlAlchemy 2.0 中文文档(四十四)
69 4
|
3月前
|
存储 缓存 数据库
SqlAlchemy 2.0 中文文档(四十四)(5)
SqlAlchemy 2.0 中文文档(四十四)
56 4
|
3月前
|
SQL 存储 API
SqlAlchemy 2.0 中文文档(四十四)(6)
SqlAlchemy 2.0 中文文档(四十四)
58 4
|
3月前
|
SQL 缓存 数据库连接
SqlAlchemy 2.0 中文文档(四十四)(4)
SqlAlchemy 2.0 中文文档(四十四)
44 3
|
3月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(四十四)(9)
SqlAlchemy 2.0 中文文档(四十四)
44 3
|
3月前
|
SQL 缓存 关系型数据库
SqlAlchemy 2.0 中文文档(四十四)(3)
SqlAlchemy 2.0 中文文档(四十四)
49 0
|
3月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(四十四)(7)
SqlAlchemy 2.0 中文文档(四十四)
38 0
|
3月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(四十四)(1)
SqlAlchemy 2.0 中文文档(四十四)
75 0
|
3月前
|
关系型数据库 MySQL API
SqlAlchemy 2.0 中文文档(五十三)(1)
SqlAlchemy 2.0 中文文档(五十三)
33 0
|
3月前
|
SQL 安全 关系型数据库
SqlAlchemy 2.0 中文文档(五十三)(4)
SqlAlchemy 2.0 中文文档(五十三)
27 0