SqlAlchemy 2.0 中文文档(七十三)(4)

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 2核4GB
简介: SqlAlchemy 2.0 中文文档(七十三)

SqlAlchemy 2.0 中文文档(七十三)(3)https://developer.aliyun.com/article/1562219


关键行为变化 - ORM

Query.join() 更明确地处理决定“左”侧的歧义

从历史上看,给定以下查询:

u_alias = aliased(User)
session.query(User, u_alias).join(Address)

鉴于标准教程映射,查询将生成一个 FROM 子句,如下所示:

SELECT  ...
FROM  users  AS  users_1,  users  JOIN  addresses  ON  users.id  =  addresses.user_id

也就是说,JOIN 会隐式地针对第一个匹配的实体。新的行为是要求解决这种模糊性的异常:

sqlalchemy.exc.InvalidRequestError: Can't determine which FROM clause to
join from, there are multiple FROMS which can join to this entity.
Try adding an explicit ON clause to help resolve the ambiguity.

解决方案是提供一个 ON 子句,可以是一个表达式:

# join to User
session.query(User, u_alias).join(Address, Address.user_id == User.id)
# join to u_alias
session.query(User, u_alias).join(Address, Address.user_id == u_alias.id)

或者使用关系属性,如果可用的话:

# join to User
session.query(User, u_alias).join(Address, User.addresses)
# join to u_alias
session.query(User, u_alias).join(Address, u_alias.addresses)

这个变化包括,如果连接是非模糊的,那么现在连接可以正确地链接到不是列表中第一个元素的 FROM 子句:

session.query(func.current_timestamp(), User).join(Address)

在这项增强之前,上述查询将引发:

sqlalchemy.exc.InvalidRequestError: Don't know how to join from
CURRENT_TIMESTAMP; please use select_from() to establish the
left entity/selectable of this join

现在查询可以正常工作了:

SELECT  CURRENT_TIMESTAMP  AS  current_timestamp_1,  users.id  AS  users_id,
users.name  AS  users_name,  users.fullname  AS  users_fullname,
users.password  AS  users_password
FROM  users  JOIN  addresses  ON  users.id  =  addresses.user_id

总的来说,这种变化直接符合 Python 的“显式优于隐式”的哲学。

#4365 ### FOR UPDATE 子句在联合贪婪加载子查询中以及外部呈现

此更改特别适用于使用joinedload()加载策略与行限制查询相结合,例如使用Query.first()Query.limit(),以及使用Query.with_for_update()方法。

给定一个查询:

session.query(A).options(joinedload(A.b)).limit(5)

当联合贪婪加载与 LIMIT 结合时,Query对象呈现如下形式的 SELECT:

SELECT  subq.a_id,  subq.a_data,  b_alias.id,  b_alias.data  FROM  (
  SELECT  a.id  AS  a_id,  a.data  AS  a_data  FROM  a  LIMIT  5
)  AS  subq  LEFT  OUTER  JOIN  b  ON  subq.a_id=b.a_id

这样,对主要实体的行限制就会发生,而不会影响到关联项的贪婪加载。当上述查询与“SELECT…FOR UPDATE”结合时,行为是这样的:

SELECT  subq.a_id,  subq.a_data,  b_alias.id,  b_alias.data  FROM  (
  SELECT  a.id  AS  a_id,  a.data  AS  a_data  FROM  a  LIMIT  5
)  AS  subq  LEFT  OUTER  JOIN  b  ON  subq.a_id=b.a_id  FOR  UPDATE

然而,由于bugs.mysql.com/bug.php?id=90693,MySQL 不会锁定子查询中的行,不像 PostgreSQL 和其他数据库。因此,上述查询现在呈现为:

SELECT  subq.a_id,  subq.a_data,  b_alias.id,  b_alias.data  FROM  (
  SELECT  a.id  AS  a_id,  a.data  AS  a_data  FROM  a  LIMIT  5  FOR  UPDATE
)  AS  subq  LEFT  OUTER  JOIN  b  ON  subq.a_id=b.a_id  FOR  UPDATE

在 Oracle 方言中,内部的“FOR UPDATE”不会呈现,因为 Oracle 不支持这种语法,方言会跳过针对子查询的任何“FOR UPDATE”;在任何情况下都不是必要的,因为 Oracle 像 PostgreSQL 一样正确锁定返回行的所有元素。

当在Query.with_for_update.of修饰符上使用时,通常在 PostgreSQL 上,外部的“FOR UPDATE”被省略,OF 现在在内部呈现;以前,OF 目标不会被转换以正确适应子查询。因此,考虑到:

session.query(A).options(joinedload(A.b)).with_for_update(of=A).limit(5)

现在查询将呈现为:

SELECT  subq.a_id,  subq.a_data,  b_alias.id,  b_alias.data  FROM  (
  SELECT  a.id  AS  a_id,  a.data  AS  a_data  FROM  a  LIMIT  5  FOR  UPDATE  OF  a
)  AS  subq  LEFT  OUTER  JOIN  b  ON  subq.a_id=b.a_id

上述形式应该对 PostgreSQL 有所帮助,因为 PostgreSQL 不允许在 LEFT OUTER JOIN 目标之后呈现 FOR UPDATE 子句。

总的来说,对于正在使用的目标数据库,FOR UPDATE 仍然非常具体,不容易推广到更复杂的查询。

#4246 ### passive_deletes=’all’将使 FK 对于从集合中移除的对象保持不变

relationship.passive_deletes选项接受值"all",表示在刷新对象时不应修改任何外键属性,即使关系的集合/引用已被移除。以前,在以下情况下,这种情况并不适用于一对多或一对一关系:

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    addresses = relationship("Address", passive_deletes="all")
class Address(Base):
    __tablename__ = "addresses"
    id = Column(Integer, primary_key=True)
    email = Column(String)
    user_id = Column(Integer, ForeignKey("users.id"))
    user = relationship("User")
u1 = session.query(User).first()
address = u1.addresses[0]
u1.addresses.remove(address)
session.commit()
# would fail and be set to None
assert address.user_id == u1.id

修复现在包括address.user_id根据passive_deletes="all"保持不变。这种情况对于构建自定义“版本表”方案等非常有用,其中行被归档而不是被删除。

#3844 ### Query.join()更明确地处理决定“左”侧的模棱两可性

历史上,给定如下查询:

u_alias = aliased(User)
session.query(User, u_alias).join(Address)

鉴于标准教程映射,查询将生成一个 FROM 子句:

SELECT  ...
FROM  users  AS  users_1,  users  JOIN  addresses  ON  users.id  =  addresses.user_id

这意味着 JOIN 将隐式地针对第一个匹配的实体。新行为是,异常请求解决这种模棱两可的情况:

sqlalchemy.exc.InvalidRequestError: Can't determine which FROM clause to
join from, there are multiple FROMS which can join to this entity.
Try adding an explicit ON clause to help resolve the ambiguity.

解决方案是提供一个 ON 子句,可以是一个表达式:

# join to User
session.query(User, u_alias).join(Address, Address.user_id == User.id)
# join to u_alias
session.query(User, u_alias).join(Address, Address.user_id == u_alias.id)

或者使用关系属性,如果可用的话:

# join to User
session.query(User, u_alias).join(Address, User.addresses)
# join to u_alias
session.query(User, u_alias).join(Address, u_alias.addresses)

更改包括现在 JOIN 可以正确地链接到不是列表中第一个元素的 FROM 子句,如果 JOIN 是非模棱两可的话:

session.query(func.current_timestamp(), User).join(Address)

在此增强之前,上述查询将引发:

sqlalchemy.exc.InvalidRequestError: Don't know how to join from
CURRENT_TIMESTAMP; please use select_from() to establish the
left entity/selectable of this join

现在查询正常工作:

SELECT  CURRENT_TIMESTAMP  AS  current_timestamp_1,  users.id  AS  users_id,
users.name  AS  users_name,  users.fullname  AS  users_fullname,
users.password  AS  users_password
FROM  users  JOIN  addresses  ON  users.id  =  addresses.user_id

总的来说,这种变化直接符合 Python 的“显式优于隐式”的哲学。

#4365

FOR UPDATE 子句在连接贪婪加载子查询内部以及外部都被渲染

此更改特别适用于使用joinedload()加载策略与行限制查询结合使用时,例如使用Query.first()Query.limit(),以及使用Query.with_for_update()方法。

给定一个查询如下:

session.query(A).options(joinedload(A.b)).limit(5)

当连接贪婪加载与 LIMIT 结合时,Query对象会渲染以下形式的 SELECT:

SELECT  subq.a_id,  subq.a_data,  b_alias.id,  b_alias.data  FROM  (
  SELECT  a.id  AS  a_id,  a.data  AS  a_data  FROM  a  LIMIT  5
)  AS  subq  LEFT  OUTER  JOIN  b  ON  subq.a_id=b.a_id

这样做是为了使主实体的行限制生效,而不影响相关项目的连接贪婪加载。当上述查询与“SELECT…FOR UPDATE”结合时,行为如下:

SELECT  subq.a_id,  subq.a_data,  b_alias.id,  b_alias.data  FROM  (
  SELECT  a.id  AS  a_id,  a.data  AS  a_data  FROM  a  LIMIT  5
)  AS  subq  LEFT  OUTER  JOIN  b  ON  subq.a_id=b.a_id  FOR  UPDATE

然而,由于bugs.mysql.com/bug.php?id=90693,MySQL 不会锁定子查询内的行,不像 PostgreSQL 和其他数据库。因此,上述查询现在会渲染为:

SELECT  subq.a_id,  subq.a_data,  b_alias.id,  b_alias.data  FROM  (
  SELECT  a.id  AS  a_id,  a.data  AS  a_data  FROM  a  LIMIT  5  FOR  UPDATE
)  AS  subq  LEFT  OUTER  JOIN  b  ON  subq.a_id=b.a_id  FOR  UPDATE

在 Oracle 方言中,内部的“FOR UPDATE”不会被渲染,因为 Oracle 不支持这种语法,方言会跳过任何针对子查询的“FOR  UPDATE”;在任何情况下都不是必要的,因为 Oracle 像 PostgreSQL 一样正确地锁定了返回行的所有元素。

当在Query.with_for_update.of修饰符上使用时,通常在 PostgreSQL 上,外部的“FOR UPDATE”会被省略,OF 现在会在内部被渲染;以前,OF 目标不会被转换以适应子查询。所以给定:

session.query(A).options(joinedload(A.b)).with_for_update(of=A).limit(5)

查询现在会渲染为:

SELECT  subq.a_id,  subq.a_data,  b_alias.id,  b_alias.data  FROM  (
  SELECT  a.id  AS  a_id,  a.data  AS  a_data  FROM  a  LIMIT  5  FOR  UPDATE  OF  a
)  AS  subq  LEFT  OUTER  JOIN  b  ON  subq.a_id=b.a_id

以上形式在 PostgreSQL 上也应该有所帮助,因为 PostgreSQL 不允许在 LEFT OUTER JOIN 目标之后渲染 FOR UPDATE 子句。

总的来说,FOR UPDATE 对于正在使用的目标数据库非常具体,不能轻易地推广到更复杂的查询。

#4246

passive_deletes='all’将使从集合中移除的对象的 FK 保持不变

relationship.passive_deletes选项接受值"all",表示在刷新对象时不应修改任何外键属性,即使关系的集合/引用已被移除。以前,在以下情况下,这不会发生在一对多或一对一关系中:

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    addresses = relationship("Address", passive_deletes="all")
class Address(Base):
    __tablename__ = "addresses"
    id = Column(Integer, primary_key=True)
    email = Column(String)
    user_id = Column(Integer, ForeignKey("users.id"))
    user = relationship("User")
u1 = session.query(User).first()
address = u1.addresses[0]
u1.addresses.remove(address)
session.commit()
# would fail and be set to None
assert address.user_id == u1.id

修复现在包括address.user_id按照passive_deletes="all"不变。这种情况对于构建自定义“版本表”方案等非常有用,其中行被归档而不是删除。

#3844

新功能和改进 - 核心

新的多列命名约定标记,长名称截断

为了适应MetaData命名约定需要区分多列约束并希望在生成的约束名称中使用所有列的情况,添加了一系列新的命名约定标记,包括column_0N_namecolumn_0_N_namecolumn_0N_keycolumn_0_N_keyreferred_column_0N_namereferred_column_0_N_name等,它们将所有列的列名(或键或标签)连接在一起,没有分隔符或使用下划线分隔符。下面我们定义一个约定,将UniqueConstraint约束命名为连接所有列名称的名称:

metadata_obj = MetaData(
    naming_convention={"uq": "uq_%(table_name)s_%(column_0_N_name)s"}
)
table = Table(
    "info",
    metadata_obj,
    Column("a", Integer),
    Column("b", Integer),
    Column("c", Integer),
    UniqueConstraint("a", "b", "c"),
)

上述表的 CREATE TABLE 将呈现为:

CREATE  TABLE  info  (
  a  INTEGER,
  b  INTEGER,
  c  INTEGER,
  CONSTRAINT  uq_info_a_b_c  UNIQUE  (a,  b,  c)
)

此外,现在将长名称截断逻辑应用于命名约定生成的名称,特别是为了适应可能产生非常长名称的多列标签。这种逻辑与用于截断 SELECT  语句中的长标签名称的逻辑相同,用一个确定性生成的 4 字符哈希替换超过目标数据库标识符长度限制的多余字符。例如,在 PostgreSQL  上,标识符不能超过 63 个字符,长约束名称通常从下面的表定义中生成:

long_names = Table(
    "long_names",
    metadata_obj,
    Column("information_channel_code", Integer, key="a"),
    Column("billing_convention_name", Integer, key="b"),
    Column("product_identifier", Integer, key="c"),
    UniqueConstraint("a", "b", "c"),
)

截断逻辑将确保不会为 UNIQUE 约束生成过长的名称:

CREATE  TABLE  long_names  (
  information_channel_code  INTEGER,
  billing_convention_name  INTEGER,
  product_identifier  INTEGER,
  CONSTRAINT  uq_long_names_information_channel_code_billing_conventi_a79e
  UNIQUE  (information_channel_code,  billing_convention_name,  product_identifier)
)

上述后缀a79e基于长名称的 md5 哈希,并且每次生成相同的值,以产生给定模式的一致名称。

注意,当约束名在给定方言中显式过大时,截断逻辑也会引发IdentifierError。这已经是Index对象的行为很长时间了,但现在也适用于其他类型的约束:

from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import Table
from sqlalchemy import UniqueConstraint
from sqlalchemy.dialects import postgresql
from sqlalchemy.schema import AddConstraint
m = MetaData()
t = Table("t", m, Column("x", Integer))
uq = UniqueConstraint(
    t.c.x,
    name="this_is_too_long_of_a_name_for_any_database_backend_even_postgresql",
)
print(AddConstraint(uq).compile(dialect=postgresql.dialect()))

将输出:

sqlalchemy.exc.IdentifierError: Identifier
'this_is_too_long_of_a_name_for_any_database_backend_even_postgresql'
exceeds maximum length of 63 characters

异常引发阻止了由数据库后端截断的非确定性约束名称的生成,这些名称后来与数据库迁移不兼容。

要将 SQLAlchemy 端的截断规则应用于上述标识符,请使用conv()构造:

uq = UniqueConstraint(
    t.c.x,
    name=conv("this_is_too_long_of_a_name_for_any_database_backend_even_postgresql"),
)

这将再次输出确定性截断的 SQL,如下所示:

ALTER  TABLE  t  ADD  CONSTRAINT  this_is_too_long_of_a_name_for_any_database_backend_eve_ac05  UNIQUE  (x)

目前还没有选项可以使名称传递以允许数据库端截断。这对于Index名称已经有一段时间了,而且并没有提出问题。

此更改还修复了另外两个问题。一个是column_0_key标记虽然被记录,但却无法使用,另一个是如果这两个值不同,referred_column_0_name标记会错误地呈现.key而不是.name列。

另请参阅

配置约束命名约定

MetaData.naming_convention

#3989 ### 用于 SQL 函数的二进制比较解释

这个增强功能是在核心层实现的,但主要适用于 ORM。

现在可以将比较两个元素的 SQL 函数用作“比较”对象,适用于 ORM relationship()的用法,首先像往常一样使用func工厂创建函数,然后在函数完成时调用FunctionElement.as_comparison()修饰符来生成一个具有“左”和“右”两侧的BinaryExpression

class Venue(Base):
    __tablename__ = "venue"
    id = Column(Integer, primary_key=True)
    name = Column(String)
    descendants = relationship(
        "Venue",
        primaryjoin=func.instr(remote(foreign(name)), name + "/").as_comparison(1, 2)
        == 1,
        viewonly=True,
        order_by=name,
    )

上面,“后代”关系的relationship.primaryjoin将基于传递给instr()的第一个和第二个参数产生一个“左”和一个“右”表达式。这使得 ORM 懒加载等功能能够产生 SQL,例如:

SELECT  venue.id  AS  venue_id,  venue.name  AS  venue_name
FROM  venue
WHERE  instr(venue.name,  (?  ||  ?))  =  ?  ORDER  BY  venue.name
('parent1',  '/',  1)

以及一个 joinedload,例如:

v1 = (
    s.query(Venue)
    .filter_by(name="parent1")
    .options(joinedload(Venue.descendants))
    .one()
)

工作原理如下:

SELECT  venue.id  AS  venue_id,  venue.name  AS  venue_name,
  venue_1.id  AS  venue_1_id,  venue_1.name  AS  venue_1_name
FROM  venue  LEFT  OUTER  JOIN  venue  AS  venue_1
  ON  instr(venue_1.name,  (venue.name  ||  ?))  =  ?
WHERE  venue.name  =  ?  ORDER  BY  venue_1.name
('/',  1,  'parent1')

该功能预期将有助于处理诸如在关系连接条件中使用几何函数或任何通过 SQL 函数来表达 SQL 连接的 ON 子句等情况。

#3831 ### 扩展 IN 特性现在支持空列表

版本 1.2 中引入的“扩展 IN”功能现在支持传递给ColumnOperators.in_()运算符的空列表。对于空列表的实现将产生一个特定于目标后端的“空集”表达式,例如对于  PostgreSQL,“SELECT CAST(NULL AS INTEGER) WHERE 1!=1”,对于 MySQL,“SELECT 1  FROM (SELECT 1) as _empty_set WHERE 1!=1”:

>>> from sqlalchemy import create_engine
>>> from sqlalchemy import select, literal_column, bindparam
>>> e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
>>> with e.connect() as conn:
...     conn.execute(
...         select([literal_column("1")]).where(
...             literal_column("1").in_(bindparam("q", expanding=True))
...         ),
...         q=[],
...     )
{exexsql}SELECT 1 WHERE 1 IN (SELECT CAST(NULL AS INTEGER) WHERE 1!=1)

该功能还适用于基于元组的 IN 语句,其中“空 IN”表达式将被扩展以支持元组中给定的元素,例如在 PostgreSQL 上:

>>> from sqlalchemy import create_engine
>>> from sqlalchemy import select, literal_column, tuple_, bindparam
>>> e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
>>> with e.connect() as conn:
...     conn.execute(
...         select([literal_column("1")]).where(
...             tuple_(50, "somestring").in_(bindparam("q", expanding=True))
...         ),
...         q=[],
...     )
{exexsql}SELECT 1 WHERE (%(param_1)s, %(param_2)s)
IN (SELECT CAST(NULL AS INTEGER), CAST(NULL AS VARCHAR) WHERE 1!=1)

#4271 ### TypeEngine 方法 bind_expression、column_expression 与 Variant、特定类型一起工作

TypeEngine.bind_expression()TypeEngine.column_expression()方法存在于特定数据类型的“impl”上时,这些方法现在可以被方言使用,也可以用于TypeDecoratorVariant的用例。

以下示例说明了一个TypeDecorator,它将 SQL 时间转换函数应用于LargeBinary。为了使此类型在Variant的上下文中工作,编译器需要深入“impl”变体表达式以定位这些方法:

from sqlalchemy import TypeDecorator, LargeBinary, func
class CompressedLargeBinary(TypeDecorator):
    impl = LargeBinary
    def bind_expression(self, bindvalue):
        return func.compress(bindvalue, type_=self)
    def column_expression(self, col):
        return func.uncompress(col, type_=self)
MyLargeBinary = LargeBinary().with_variant(CompressedLargeBinary(), "sqlite")

上述表达式仅在 SQLite 上使用时会呈现为 SQL 中的函数:

from sqlalchemy import select, column
from sqlalchemy.dialects import sqlite
print(select([column("x", CompressedLargeBinary)]).compile(dialect=sqlite.dialect()))

将呈现:

SELECT  uncompress(x)  AS  x

此更改还包括方言可以在方言级别的实现类型上实现TypeEngine.bind_expression()TypeEngine.column_expression(),它们现在将被使用;特别是这将用于 MySQL 的新“二进制前缀”要求以及用于将 MySQL 的十进制绑定值转换为浮点数。

#3981 ### QueuePool 的新后进先出策略

通常由 create_engine() 使用的连接池称为 QueuePool。此连接池使用类似于 Python 内置的 Queue 类的对象来存储等待使用的数据库连接。Queue 具有先进先出的行为,旨在提供对持续在池中的数据库连接的循环使用。然而,这样做的一个潜在缺点是,当池的利用率低时,池中的每个连接的串行重复使用意味着试图减少未使用连接的服务器端超时策略被阻止关闭这些连接。为了适应这种情况,添加了一个新的标志 create_engine.pool_use_lifo ,它将 Queue.get() 方法反转,以从队列的开头而不是末尾获取连接,从而实质上将“队列”变为“栈”(考虑到这样做会增加一个称为 StackPool 的全新连接池,但这太啰嗦了)。

另见

使用 FIFO vs. LIFO ### 新的多列命名约定标记,长名称截断

为了适应需要通过 MetaData 命名约定消除多列约束的歧义,并希望在生成的约束名称中使用所有列的情况,添加了一系列新的命名约定标记,包括 column_0N_namecolumn_0_N_namecolumn_0N_keycolumn_0_N_keyreferred_column_0N_namereferred_column_0_N_name 等,这些标记将约束中的所有列的列名(或键或标签)连接在一起,可以是没有分隔符或带有下划线分隔符。下面我们定义一个约定,将会以将所有列的名称连接在一起的方式命名 UniqueConstraint 约束:

metadata_obj = MetaData(
    naming_convention={"uq": "uq_%(table_name)s_%(column_0_N_name)s"}
)
table = Table(
    "info",
    metadata_obj,
    Column("a", Integer),
    Column("b", Integer),
    Column("c", Integer),
    UniqueConstraint("a", "b", "c"),
)

上述表的 CREATE TABLE 将呈现为:

CREATE  TABLE  info  (
  a  INTEGER,
  b  INTEGER,
  c  INTEGER,
  CONSTRAINT  uq_info_a_b_c  UNIQUE  (a,  b,  c)
)

此外,现在对通过命名约定生成的名称应用长名称截断逻辑,特别是为了适应可能产生非常长名称的多列标签。这个逻辑与在 SELECT  语句中截断长标签名称所使用的逻辑相同,它用一个确定性生成的 4 字符哈希替换了超过目标数据库标识符长度限制的多余字符。例如,在  PostgreSQL 中,标识符不能超过 63 个字符,长约束名通常是从下面的表定义生成的:

long_names = Table(
    "long_names",
    metadata_obj,
    Column("information_channel_code", Integer, key="a"),
    Column("billing_convention_name", Integer, key="b"),
    Column("product_identifier", Integer, key="c"),
    UniqueConstraint("a", "b", "c"),
)

截断逻辑将确保不会为 UNIQUE 约束生成过长的名称:

CREATE  TABLE  long_names  (
  information_channel_code  INTEGER,
  billing_convention_name  INTEGER,
  product_identifier  INTEGER,
  CONSTRAINT  uq_long_names_information_channel_code_billing_conventi_a79e
  UNIQUE  (information_channel_code,  billing_convention_name,  product_identifier)
)

上述后缀a79e基于长名称的 md5 哈希,并且每次生成相同的值,以为给定的模式生成一致的名称。

请注意,当约束名称显式过大时,截断逻辑还会引发IdentifierError。这已经是Index对象的行为很长时间了,但现在也适用于其他类型的约束:

from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import Table
from sqlalchemy import UniqueConstraint
from sqlalchemy.dialects import postgresql
from sqlalchemy.schema import AddConstraint
m = MetaData()
t = Table("t", m, Column("x", Integer))
uq = UniqueConstraint(
    t.c.x,
    name="this_is_too_long_of_a_name_for_any_database_backend_even_postgresql",
)
print(AddConstraint(uq).compile(dialect=postgresql.dialect()))

输出将是:

sqlalchemy.exc.IdentifierError: Identifier
'this_is_too_long_of_a_name_for_any_database_backend_even_postgresql'
exceeds maximum length of 63 characters

异常抛出可防止由数据库后端截断的不确定性约束名称的生成,这些名称随后与数据库迁移不兼容。

要将 SQLAlchemy 端的截断规则应用于上述标识符,请使用conv()构造:

uq = UniqueConstraint(
    t.c.x,
    name=conv("this_is_too_long_of_a_name_for_any_database_backend_even_postgresql"),
)

这将再次输出确定性截断的 SQL,如下所示:

ALTER  TABLE  t  ADD  CONSTRAINT  this_is_too_long_of_a_name_for_any_database_backend_eve_ac05  UNIQUE  (x)

目前尚无选项可使名称通过以允许数据库端截断。这在一段时间内已经适用于Index名称,并且并未引起问题。

这一变更还修复了其他两个问题。其中一个是column_0_key令牌虽然已记录,但却不可用,另一个是如果这两个值不同,referred_column_0_name令牌会意外地呈现.key而不是.name

另请参阅

配置约束命名约定

MetaData.naming_convention

#3989

SQL 函数的二进制比较解释

此增强功能是在核心级别实现的,但主要适用于 ORM。

现在可以将比较两个元素的 SQL 函数用作适用于 ORM relationship()中的“比较”对象,首先使用func工厂通常创建该函数,然后当函数完成时调用FunctionElement.as_comparison()修饰符,以生成具有“左”和“右”两侧的BinaryExpression

class Venue(Base):
    __tablename__ = "venue"
    id = Column(Integer, primary_key=True)
    name = Column(String)
    descendants = relationship(
        "Venue",
        primaryjoin=func.instr(remote(foreign(name)), name + "/").as_comparison(1, 2)
        == 1,
        viewonly=True,
        order_by=name,
    )

上面,“descendants”关系的relationship.primaryjoin将基于传递给instr()的第一个和第二个参数产生一个“left”和一个“right”表达式。这允许 ORM lazyload 等功能生成类似以下的 SQL:

SELECT  venue.id  AS  venue_id,  venue.name  AS  venue_name
FROM  venue
WHERE  instr(venue.name,  (?  ||  ?))  =  ?  ORDER  BY  venue.name
('parent1',  '/',  1)

以及一个 joinedload,例如:

v1 = (
    s.query(Venue)
    .filter_by(name="parent1")
    .options(joinedload(Venue.descendants))
    .one()
)

作为:

SELECT  venue.id  AS  venue_id,  venue.name  AS  venue_name,
  venue_1.id  AS  venue_1_id,  venue_1.name  AS  venue_1_name
FROM  venue  LEFT  OUTER  JOIN  venue  AS  venue_1
  ON  instr(venue_1.name,  (venue.name  ||  ?))  =  ?
WHERE  venue.name  =  ?  ORDER  BY  venue_1.name
('/',  1,  'parent1')

这个功能预计将有助于处理诸如在关系连接条件中使用几何函数或任何 ON 子句以 SQL 函数形式表达的情况。

#3831

扩展 IN 功能现在支持空列表

在版本 1.2 中引入的“扩展 IN”功能现在支持传递给ColumnOperators.in_()运算符的空列表。对于空列表的实现将产生一个特定于目标后端的“空集合”表达式,例如对于  PostgreSQL,“SELECT CAST(NULL AS INTEGER) WHERE 1!=1”,对于 MySQL,“SELECT 1  FROM (SELECT 1) as _empty_set WHERE 1!=1”:

>>> from sqlalchemy import create_engine
>>> from sqlalchemy import select, literal_column, bindparam
>>> e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
>>> with e.connect() as conn:
...     conn.execute(
...         select([literal_column("1")]).where(
...             literal_column("1").in_(bindparam("q", expanding=True))
...         ),
...         q=[],
...     )
{exexsql}SELECT 1 WHERE 1 IN (SELECT CAST(NULL AS INTEGER) WHERE 1!=1)

该功能还适用于基于元组的 IN 语句,其中“空 IN”表达式将被扩展以支持元组中给定的元素,例如在 PostgreSQL 上:

>>> from sqlalchemy import create_engine
>>> from sqlalchemy import select, literal_column, tuple_, bindparam
>>> e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
>>> with e.connect() as conn:
...     conn.execute(
...         select([literal_column("1")]).where(
...             tuple_(50, "somestring").in_(bindparam("q", expanding=True))
...         ),
...         q=[],
...     )
{exexsql}SELECT 1 WHERE (%(param_1)s, %(param_2)s)
IN (SELECT CAST(NULL AS INTEGER), CAST(NULL AS VARCHAR) WHERE 1!=1)

#4271

TypeEngine 方法 bind_expression、column_expression 与 Variant、特定类型一起工作

TypeEngine.bind_expression()TypeEngine.column_expression()方法现在在特定数据类型的“impl”上存在时也能工作,允许这些方法被方言以及TypeDecoratorVariant用例使用。

以下示例说明了一个TypeDecorator,它将 SQL 时间转换函数应用于LargeBinary。为了使这种类型在Variant的上下文中工作,编译器需要深入“impl”变体表达式以定位这些方法:

from sqlalchemy import TypeDecorator, LargeBinary, func
class CompressedLargeBinary(TypeDecorator):
    impl = LargeBinary
    def bind_expression(self, bindvalue):
        return func.compress(bindvalue, type_=self)
    def column_expression(self, col):
        return func.uncompress(col, type_=self)
MyLargeBinary = LargeBinary().with_variant(CompressedLargeBinary(), "sqlite")

上述表达式仅在 SQLite 上使用时会在 SQL 中呈现一个函数:

from sqlalchemy import select, column
from sqlalchemy.dialects import sqlite
print(select([column("x", CompressedLargeBinary)]).compile(dialect=sqlite.dialect()))

将呈现:

SELECT  uncompress(x)  AS  x

此更改还包括方言可以在方言级别的实现类型上实现TypeEngine.bind_expression()TypeEngine.column_expression(),在那里它们现在将被使用;特别是这将用于 MySQL 的新“二进制前缀”要求以及用于将 MySQL 的十进制绑定值转换的情况。

#3981

QueuePool 的新后进先出策略

通常由create_engine()使用的连接池被称为QueuePool。此池使用一个类似于 Python 内置的Queue类的对象来存储等待使用的数据库连接。Queue具有先进先出的行为,旨在提供对持久在池中的数据库连接的循环使用。然而,这种方法的一个潜在缺点是,当池的利用率较低时,池中每个连接的串行重复使用意味着试图减少未使用连接的服务器端超时策略被阻止关闭这些连接。为了适应这种用例,添加了一个新标志create_engine.pool_use_lifo,它将Queue.get()方法反转,从队列的开头而不是末尾获取连接,从本质上将“队列”变成“栈”(考虑到添加一个名为StackPool的全新池,但这太啰嗦了)。

另请参阅

使用 FIFO vs. LIFO


SqlAlchemy 2.0 中文文档(七十三)(5)https://developer.aliyun.com/article/1562231

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
4月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(七十四)(5)
SqlAlchemy 2.0 中文文档(七十四)
46 6
|
4月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(七十四)(3)
SqlAlchemy 2.0 中文文档(七十四)
46 1
|
4月前
|
存储 Java 测试技术
SqlAlchemy 2.0 中文文档(七十三)(3)
SqlAlchemy 2.0 中文文档(七十三)
34 8
|
4月前
|
SQL 关系型数据库 测试技术
SqlAlchemy 2.0 中文文档(七十三)(2)
SqlAlchemy 2.0 中文文档(七十三)
44 4
|
4月前
|
SQL Python
SqlAlchemy 2.0 中文文档(七十四)(4)
SqlAlchemy 2.0 中文文档(七十四)
30 6
|
4月前
|
SQL JSON 关系型数据库
SqlAlchemy 2.0 中文文档(七十三)(5)
SqlAlchemy 2.0 中文文档(七十三)
43 1
|
4月前
|
SQL 关系型数据库 测试技术
SqlAlchemy 2.0 中文文档(七十三)(1)
SqlAlchemy 2.0 中文文档(七十三)
42 1
|
4月前
|
SQL 缓存 关系型数据库
SqlAlchemy 2.0 中文文档(七十四)(2)
SqlAlchemy 2.0 中文文档(七十四)
34 1
|
4月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(七十四)(1)
SqlAlchemy 2.0 中文文档(七十四)
64 1
|
4月前
|
SQL 关系型数据库 API
SqlAlchemy 2.0 中文文档(三十六)(4)
SqlAlchemy 2.0 中文文档(三十六)
33 0