SqlAlchemy 2.0 中文文档(七十六)(3)

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

SqlAlchemy 2.0 中文文档(七十六)(2)https://developer.aliyun.com/article/1561129


改进声明性混合类,@declared_attr和相关功能

declared_attr结合使用的声明性系统已经进行了全面改进,以支持新的功能。

现在,使用declared_attr装饰的函数仅在生成基于混合类的列副本之后才被调用。这意味着该函数可以调用基于混合类建立的列,并将接收到正确的Column对象的引用:

class HasFooBar(object):
    foobar = Column(Integer)
    @declared_attr
    def foobar_prop(cls):
        return column_property("foobar: " + cls.foobar)
class SomeClass(HasFooBar, Base):
    __tablename__ = "some_table"
    id = Column(Integer, primary_key=True)

在上面的例子中,SomeClass.foobar_prop 将针对 SomeClass 调用,而 SomeClass.foobar 将是最终要映射到 SomeClassColumn 对象,而不是直接存在于 HasFooBar 上的非复制对象,即使列尚未映射。

declared_attr 函数现在会基于每个类记忆返回的值,以便对相同属性的重复调用会返回相同的值。我们可以修改示例来说明这一点:

class HasFooBar(object):
    @declared_attr
    def foobar(cls):
        return Column(Integer)
    @declared_attr
    def foobar_prop(cls):
        return column_property("foobar: " + cls.foobar)
class SomeClass(HasFooBar, Base):
    __tablename__ = "some_table"
    id = Column(Integer, primary_key=True)

以前,SomeClass 将以一个特定的 foobar 列的副本进行映射,但通过第二次调用 foobar 来调用 foobar_prop 将产生一个不同的列。在声明式设置期间,SomeClass.foobar 的值现在被记忆,因此即使在属性由映射器映射之前,临时列值也将保持一致,无论 declared_attr 被调用多少次。

上述两种行为应该极大地帮助声明性定义许多类型的映射器属性,这些属性派生自其他属性,在类实际映射之前从其他 declared_attr 函数中调用。

对于一个非常罕见的边缘情况,其中希望建立一个在每个子类中建立不同列的声明性混合类,添加了一个新的修饰符 declared_attr.cascading。使用这个修饰符,装饰的函数将分别为映射的继承层次结构中的每个类调用。虽然这已经是诸如 __table_args____mapper_args__ 等特殊属性的行为,但是对于列和其他属性,默认情况下假定该属性仅附加到基类,并且仅从子类继承。使用 declared_attr.cascading,可以应用单独的行为:

class HasIdMixin(object):
    @declared_attr.cascading
    def id(cls):
        if has_inherited_table(cls):
            return Column(ForeignKey("myclass.id"), primary_key=True)
        else:
            return Column(Integer, primary_key=True)
class MyClass(HasIdMixin, Base):
    __tablename__ = "myclass"
    # ...
class MySubClass(MyClass):
  """ """
    # ...

另见

使用 _orm.declared_attr() 生成特定于表的继承列

最后,AbstractConcreteBase 类已经重新设计,以便在抽象基类上内联设置关系或其他映射器属性:

from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import (
    declarative_base,
    declared_attr,
    AbstractConcreteBase,
)
Base = declarative_base()
class Something(Base):
    __tablename__ = "something"
    id = Column(Integer, primary_key=True)
class Abstract(AbstractConcreteBase, Base):
    id = Column(Integer, primary_key=True)
    @declared_attr
    def something_id(cls):
        return Column(ForeignKey(Something.id))
    @declared_attr
    def something(cls):
        return relationship(Something)
class Concrete(Abstract):
    __tablename__ = "cca"
    __mapper_args__ = {"polymorphic_identity": "cca", "concrete": True}

上述映射将设置一个名为 cca 的表,其中包含 idsomething_id 列,而 Concrete 还将具有一个名为 something 的关系。新功能是 Abstract 也将有一个独立配置的关系 something,该关系构建在基类的多态联合上。

#3150 #2670 #3149 #2952 #3050

ORM 完整对象的提取速度提高了 25%

loading.py 模块的机制以及标识映射经历了几次内联、重构和修剪,因此现在原始行的加载速度比基于 ORM 的对象快约 25%。假设有一个包含 1 百万行的表,下面的脚本演示了最大程度改进的加载类型:

import time
from sqlalchemy import Integer, Column, create_engine, Table
from sqlalchemy.orm import Session
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Foo(Base):
    __table__ = Table(
        "foo",
        Base.metadata,
        Column("id", Integer, primary_key=True),
        Column("a", Integer(), nullable=False),
        Column("b", Integer(), nullable=False),
        Column("c", Integer(), nullable=False),
    )
engine = create_engine("mysql+mysqldb://scott:tiger@localhost/test", echo=True)
sess = Session(engine)
now = time.time()
# avoid using all() so that we don't have the overhead of building
# a large list of full objects in memory
for obj in sess.query(Foo).yield_per(100).limit(1000000):
    pass
print("Total time: %d" % (time.time() - now))

本地 MacBookPro 的结果从 0.9 秒降至 1.0 秒的时间为 19 秒降至 14 秒。在批量处理大量行时,Query.yield_per() 的调用总是一个好主意,因为它可以防止 Python 解释器一次性为所有对象及其仪器分配大量内存。没有 Query.yield_per(),在 MacBookPro 上,0.9 版本上的上述脚本需要 31 秒,1.0 版本上需要 26 秒,额外的时间用于设置非常大的内存缓冲区。

新的 KeyedTuple 实现速度显著提高

我们研究了 KeyedTuple 实现,希望改进这样的查询:

rows = sess.query(Foo.a, Foo.b, Foo.c).all()

使用 KeyedTuple 类而不是 Python 的 collections.namedtuple(),因为后者具有一个非常复杂的类型创建程序,其性能比 KeyedTuple 慢得多。然而,当提取数十万行时,collections.namedtuple() 很快就会超过 KeyedTuple,随着实例调用次数的增加,KeyedTuple  的性能会急剧下降。怎么办?一个新类型,介于两者之间的方法。对于“大小”(返回的行数)和“num”(不同查询的数量)对所有三种类型进行测试,新的“轻量级键值元组”要么优于两者,要么略逊于更快的对象,具体取决于情况。在“甜蜜点”,我们既创建了大量新类型,又提取了大量行时,轻量级对象完全超越了  namedtuple 和 KeyedTuple:

-----------------
size=10 num=10000                 # few rows, lots of queries
namedtuple: 3.60302400589         # namedtuple falls over
keyedtuple: 0.255059957504        # KeyedTuple very fast
lw keyed tuple: 0.582715034485    # lw keyed trails right on KeyedTuple
-----------------
size=100 num=1000                 # <--- sweet spot
namedtuple: 0.365247011185
keyedtuple: 0.24896979332
lw keyed tuple: 0.0889317989349   # lw keyed blows both away!
-----------------
size=10000 num=100
namedtuple: 0.572599887848
keyedtuple: 2.54251694679
lw keyed tuple: 0.613876104355
-----------------
size=1000000 num=10               # few queries, lots of rows
namedtuple: 5.79669594765         # namedtuple very fast
keyedtuple: 28.856498003          # KeyedTuple falls over
lw keyed tuple: 6.74346804619     # lw keyed trails right on namedtuple

#3176

结构内存使用方面的显著改进

通过更多内部对象的__slots__的更显著使用改进了结构性内存使用。这种优化特别针对具有大量表和列的大型应用程序的基本内存大小,并减少了各种高容量对象的内存大小,包括事件监听内部、比较器对象以及 ORM 属性和加载器策略系统的部分。

一个利用 heapy 测量 Nova 启动大小的工作台展示了 SQLAlchemy 对象、相关字典以及弱引用在“nova.db.sqlalchemy.models”基本导入中占用的空间约减少了 3.7 兆字节,或 46%:

# reported by heapy, summation of SQLAlchemy objects +
# associated dicts + weakref-related objects with core of Nova imported:
    Before: total count 26477 total bytes 7975712
    After: total count 18181 total bytes 4236456
# reported for the Python module space overall with the
# core of Nova imported:
    Before: Partition of a set of 355558 objects. Total size = 61661760 bytes.
    After: Partition of a set of 346034 objects. Total size = 57808016 bytes.

UPDATE 语句现在在 flush 中与 executemany()批处理

现在可以将 UPDATE 语句批量处理到 ORM flush 中,以更高效的 executemany()调用,类似于 INSERT 语句可以批量处理;这将根据以下标准在 flush 中调用:

  • 连续两个或更多的 UPDATE 语句涉及相同的要修改的列集。
  • 该语句在 SET 子句中没有嵌入 SQL 表达式。
  • 映射不使用mapper.version_id_col,或者后端方言支持对 executemany()操作的“合理”行数计数;现在大多数 DBAPI 都正确支持这一点。

Session.get_bind()处理更广泛的继承场景

每当查询或工作单元 flush 过程试图定位与特定类对应的数据库引擎时,都会调用Session.get_bind()方法。该方法已经改进,以处理各种基于继承的场景,包括:

  • 绑定到 Mixin 或抽象类:
class MyClass(SomeMixin, Base):
    __tablename__ = "my_table"
    # ...
session = Session(binds={SomeMixin: some_engine})
  • 根据表单独绑定到继承的具体子类:
class BaseClass(Base):
    __tablename__ = "base"
    # ...
class ConcreteSubClass(BaseClass):
    __tablename__ = "concrete"
    # ...
    __mapper_args__ = {"concrete": True}
session = Session(binds={base_table: some_engine, concrete_table: some_other_engine})

#3035

Session.get_bind()将在所有相关的 Query 情况下接收到 Mapper

修复了一系列问题,其中Session.get_bind()不会接收到Query的主要Mapper,即使该映射器是 readily available 的(主要映射器是与Query对象关联的单个映射器,或者是第一个映射器)。

Mapper 对象传递给 Session.get_bind() 时,通常由使用 Session.binds 参数关联映射器与一系列引擎的会话使用,或更具体地实现一个用户定义的 Session.get_bind() 方法,根据映射器提供一种基于模式选择引擎的方式,例如水平分片或所谓的“路由”会话,将查询路由到不同的后端。

这些场景包括:

  • Query.count():
session.query(User).count()
  • Query.update()Query.delete(),都用于 UPDATE/DELETE 语句以及“fetch”策略中使用的 SELECT:
session.query(User).filter(User.id == 15).update(
    {"name": "foob"}, synchronize_session="fetch"
)
session.query(User).filter(User.id == 15).delete(synchronize_session="fetch")
  • 针对单个列的查询:
session.query(User.id, User.name).all()
  • 针对间接映射的 SQL 函数和其他表达式,例如 column_property
class User(Base):
    ...
    score = column_property(func.coalesce(self.tables.users.c.name, None))
session.query(func.max(User.score)).scalar()

#3227 #3242 #1326

.info 字典改进

InspectionAttr.info 集合现在可以在从 Mapper.all_orm_descriptors 集合中检索的任何类型的对象上使用。这包括 hybrid_propertyassociation_proxy()。然而,由于这些对象是类绑定描述符,必须单独从附加到的类中访问它们,以便访问属性。下面使用 Mapper.all_orm_descriptors 命名空间进行说明:

class SomeObject(Base):
    # ...
    @hybrid_property
    def some_prop(self):
        return self.value + 5
inspect(SomeObject).all_orm_descriptors.some_prop.info["foo"] = "bar"

它也作为所有SchemaItem对象(例如ForeignKeyUniqueConstraint等)的构造函数参数可用,以及剩余的 ORM 构造,如synonym()

#2971

#2963

ColumnProperty 构造与别名、order_by 更好地配合

解决了关于column_property()的各种问题,特别是关于 0.9 版本引入的关于“order by label”逻辑的问题(参见 Label constructs can now render as their name alone in an ORDER BY)。

给定以下映射:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))
A.b = column_property(select([func.max(B.id)]).where(B.a_id == A.id).correlate(A))

一个简单的场景,包含两次“A.b”将无法正确呈现:

print(sess.query(A, a1).order_by(a1.b))

这将按错误的列排序:

SELECT  a.id  AS  a_id,  (SELECT  max(b.id)  AS  max_1  FROM  b
WHERE  b.a_id  =  a.id)  AS  anon_1,  a_1.id  AS  a_1_id,
(SELECT  max(b.id)  AS  max_2
FROM  b  WHERE  b.a_id  =  a_1.id)  AS  anon_2
FROM  a,  a  AS  a_1  ORDER  BY  anon_1

新输出:

SELECT  a.id  AS  a_id,  (SELECT  max(b.id)  AS  max_1
FROM  b  WHERE  b.a_id  =  a.id)  AS  anon_1,  a_1.id  AS  a_1_id,
(SELECT  max(b.id)  AS  max_2
FROM  b  WHERE  b.a_id  =  a_1.id)  AS  anon_2
FROM  a,  a  AS  a_1  ORDER  BY  anon_2

在许多情况下,“order by”逻辑无法按标签排序,例如如果映射是“多态”的:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    type = Column(String)
    __mapper_args__ = {"polymorphic_on": type, "with_polymorphic": "*"}

由于多态加载,order_by 将无法使用标签,因为它将被匿名化:

SELECT  a.id  AS  a_id,  a.type  AS  a_type,  (SELECT  max(b.id)  AS  max_1
FROM  b  WHERE  b.a_id  =  a.id)  AS  anon_1
FROM  a  ORDER  BY  (SELECT  max(b.id)  AS  max_2
FROM  b  WHERE  b.a_id  =  a.id)

现在,由于 order by 标签跟踪了匿名化标签,这现在可以工作:

SELECT  a.id  AS  a_id,  a.type  AS  a_type,  (SELECT  max(b.id)  AS  max_1
FROM  b  WHERE  b.a_id  =  a.id)  AS  anon_1
FROM  a  ORDER  BY  anon_1

这些修复中包括了一系列可能会破坏aliased()构造状态的 heisenbugs,使标签逻辑再次失败;这些问题也已经修复。

#3148 #3188

新功能和改进 - 核心

Select/Query LIMIT / OFFSET 可以指定为任意 SQL 表达式

Select.limit()Select.offset()方法现在接受任何 SQL 表达式作为参数,而不仅仅是整数值。ORM Query对象也将任何表达式传递给底层的Select对象。通常用于允许传递绑定参数,稍后可以用值替换:

sel = select([table]).limit(bindparam("mylimit")).offset(bindparam("myoffset"))

不支持非整数 LIMIT 或 OFFSET 表达式的方言可能继续不支持此行为;第三方方言可能还需要修改以利用新行为。当前使用 ._limit._offset 属性的方言将继续为那些限制/偏移指定为简单整数值的情况下运行。然而,当指定 SQL 表达式时,这两个属性将在访问时引发 CompileError。希望支持新功能的第三方方言现在应调用 ._limit_clause._offset_clause 属性以接收完整的 SQL 表达式,而不是整数值。### ForeignKeyConstraint 上的 use_alter 标志(通常)不再需要。

MetaData.create_all()MetaData.drop_all() 方法现在将使用一个系统,自动为涉及表之间相互依赖循环的外键约束渲染 ALTER 语句,无需指定 ForeignKeyConstraint.use_alter。此外,外键约束现在不再需要名称即可通过  ALTER 创建;只有 DROP 操作需要名称。在 DROP 的情况下,该功能将确保只有具有显式名称的约束实际上包含在 ALTER 语句中。在  DROP 中存在无法解决的循环的情况下,如果无法继续执行 DROP,系统现在会发出简洁明了的错误消息。

ForeignKeyConstraint.use_alterForeignKey.use_alter 标志仍然存在,并且继续具有相同的效果,在 CREATE/DROP 场景中建立需要 ALTER 的约束。

从版本 1.0.1 开始,针对 SQLite 的特殊逻辑接管了 ALTER 的情况,在 DROP 期间,如果给定的表存在不可解析的循环,将发出警告,并且这些表将以 排序的方式删除,这在 SQLite 上通常是可以接受的,除非启用了约束。为了解决警告并在 SQLite 数据库上至少进行部分排序,特别是在启用了约束的数据库上,请重新将“use_alter”标志应用于那些应该明确排除的 ForeignKeyForeignKeyConstraint 对象。

另请参阅

通过 ALTER 创建/删除外键约束 - 新行为的完整描述。

#3282 ### ResultProxy “auto close” 现在是 “soft” close

在许多版本中,ResultProxy 对象总是在获取所有结果行后自动关闭。这是为了允许在不需要显式调用 ResultProxy.close() 的情况下使用该对象;由于所有的 DBAPI 资源都已被释放,因此可以安全地丢弃该对象。但是,该对象保持了严格的“关闭”行为,这意味着对 ResultProxy.fetchone()ResultProxy.fetchmany()ResultProxy.fetchall() 的任何后续调用现在都将引发 ResourceClosedError

>>> result = connection.execute(stmt)
>>> result.fetchone()
(1, 'x')
>>> result.fetchone()
None  # indicates no more rows
>>> result.fetchone()
exception: ResourceClosedError

这种行为与 pep-249 规定的不一致,即使结果耗尽后仍然可以重复调用 fetch 方法。这也会干扰某些实现结果代理的行为,例如某些数据类型的 cx_oracle 方言所使用的 BufferedColumnResultProxy

为了解决这个问题,ResultProxy的“closed”状态被分解为两个状态;一个“软关闭”执行了“关闭”的大部分功能,释放了 DBAPI 游标,并且在“带有结果的关闭”对象的情况下还会释放连接,并且一个“关闭”状态包括“软关闭”所包含的一切以及将提取方法设为“关闭”。ResultProxy.close() 方法现在不会被隐式调用,只会调用 ResultProxy._soft_close() 方法,该方法是非公开的:

>>> result = connection.execute(stmt)
>>> result.fetchone()
(1, 'x')
>>> result.fetchone()
None  # indicates no more rows
>>> result.fetchone()
None  # still None
>>> result.fetchall()
[]
>>> result.close()
>>> result.fetchone()
exception: ResourceClosedError  # *now* it raises

#3330 #3329

CHECK 约束现在支持命名约定中的 %(column_0_name)s 占位符。

%(column_0_name)s 将派生自 CheckConstraint 表达式中找到的第一列:

metadata = MetaData(naming_convention={"ck": "ck_%(table_name)s_%(column_0_name)s"})
foo = Table("foo", metadata, Column("value", Integer))
CheckConstraint(foo.c.value > 5)

将呈现:

CREATE  TABLE  foo  (
  value  INTEGER,
  CONSTRAINT  ck_foo_value  CHECK  (value  >  5)
)

与由SchemaType生成的约束的命名约定的组合,例如BooleanEnum现在也将使用所有 CHECK 约束约定。

另请参阅

命名 CHECK 约束

配置布尔值、枚举和其他模式类型的命名

#3299

当约束引用未附加的列时,可以在其引用的列附加到表时自动附加约束

自版本 0.8 起,Constraint至少具有根据传递的与表附加的列“自动附加”到Table的能力:

from sqlalchemy import Table, Column, MetaData, Integer, UniqueConstraint
m = MetaData()
t = Table("t", m, Column("a", Integer), Column("b", Integer))
uq = UniqueConstraint(t.c.a, t.c.b)  # will auto-attach to Table
assert uq in t.constraints

为了帮助一些在声明时经常出现的情况,即使Column对象尚未与Table关联,此自动附加逻辑现在也可以起作用;建立了额外的事件,以便当这些Column对象关联时,也会添加Constraint:

from sqlalchemy import Table, Column, MetaData, Integer, UniqueConstraint
m = MetaData()
a = Column("a", Integer)
b = Column("b", Integer)
uq = UniqueConstraint(a, b)
t = Table("t", m, a, b)
assert uq in t.constraints  # constraint auto-attached

以上功能是版本 1.0.0b3 之后的晚期添加的。从版本 1.0.4 开始修复了#3411,以确保如果Constraint引用混合了Column对象和字符串列名,则不会发生此逻辑;因为我们尚未跟踪将名称添加到Table的情况:

from sqlalchemy import Table, Column, MetaData, Integer, UniqueConstraint
m = MetaData()
a = Column("a", Integer)
b = Column("b", Integer)
uq = UniqueConstraint(a, "b")
t = Table("t", m, a, b)
# constraint *not* auto-attached, as we do not have tracking
# to locate when a name 'b' becomes available on the table
assert uq not in t.constraints

在上面,对于列“a”到表“t”的附加事件将在列“b”被附加之前触发(因为“a”在Table构造函数中在“b”之前声明),如果尝试进行附加,则约束将无法定位“b”。为了保持一致,如果约束涉及任何字符串名称,则会跳过自动在列附加时附加的逻辑。

Constraint 构造时,如果 Table 已经包含所有目标 Column 对象,则原始的自动附加逻辑仍然存在:

from sqlalchemy import Table, Column, MetaData, Integer, UniqueConstraint
m = MetaData()
a = Column("a", Integer)
b = Column("b", Integer)
t = Table("t", m, a, b)
uq = UniqueConstraint(a, "b")
# constraint auto-attached normally as in older versions
assert uq in t.constraints

#3341 #3411 ### INSERT FROM SELECT 现在包括 Python 和 SQL 表达式默认值

如果未另行指定,Insert.from_select() 现在包括 Python 和 SQL 表达式默认值;解除了非服务器列默认值不包括在 INSERT FROM SELECT 中的限制,这些表达式被渲染为常量插入到 SELECT 语句中:

from sqlalchemy import Table, Column, MetaData, Integer, select, func
m = MetaData()
t = Table(
    "t", m, Column("x", Integer), Column("y", Integer, default=func.somefunction())
)
stmt = select([t.c.x])
print(t.insert().from_select(["x"], stmt))

将呈现:

INSERT  INTO  t  (x,  y)  SELECT  t.x,  somefunction()  AS  somefunction_1
FROM  t

可以使用 Insert.from_select.include_defaults 来禁用此功能。 ### Column 服务器默认值现在呈现为字面值

当由 Column.server_default 设置的 DefaultClause 存在作为要编译的 SQL 表达式时,“literal binds” 编译器标志会被打开。这允许嵌入在 SQL 中的字面值正确渲染,例如:

from sqlalchemy import Table, Column, MetaData, Text
from sqlalchemy.schema import CreateTable
from sqlalchemy.dialects.postgresql import ARRAY, array
from sqlalchemy.dialects import postgresql
metadata = MetaData()
tbl = Table(
    "derp",
    metadata,
    Column("arr", ARRAY(Text), server_default=array(["foo", "bar", "baz"])),
)
print(CreateTable(tbl).compile(dialect=postgresql.dialect()))

现在呈现为:

CREATE  TABLE  derp  (
  arr  TEXT[]  DEFAULT  ARRAY['foo',  'bar',  'baz']
)

以前,字面值 "foo", "bar", "baz" 会被渲染为绑定参数,在 DDL 中毫无用处。

#3087 ### UniqueConstraint 现在是表反射过程的一部分

使用 autoload=True 填充的 Table 对象现在也会包括 UniqueConstraint 构造以及 Index 构造。这个逻辑在 PostgreSQL 和 MySQL 中有一些注意事项:

PostgreSQL

PostgreSQL 在创建唯一约束时会隐式创建对应的唯一索引。Inspector.get_indexes()Inspector.get_unique_constraints()方法将继续分别返回这些条目,其中Inspector.get_indexes()现在在索引条目中包含一个duplicates_constraint标记,指示检测到的相应约束。然而,在使用Table(..., autoload=True)进行完整表反射时,检测到Index构造与UniqueConstraint相关联,并且会出现在Table.indexes集合中;只有UniqueConstraint会出现在Table.constraints集合中。这种去重逻辑通过在查询pg_index时连接到pg_constraint表来查看这两个构造是否相关联。

MySQL

MySQL 没有唯一索引和唯一约束的单独概念。虽然它在创建表和索引时都支持两种语法,但在存储时没有任何区别。Inspector.get_indexes()Inspector.get_unique_constraints()方法将同时返回 MySQL 中唯一索引的条目,其中Inspector.get_unique_constraints()在约束条目中包含一个新的标记 duplicates_index,表示这是与该索引对应的重复条目。但是,在使用Table(..., autoload=True)执行完整表反射时,UniqueConstraint构造会在任何情况下成为完全反射的Table构造的一部分;该构造始终由Index表示,并且在Table.indexes集合中存在unique=True设置。

另请参阅

PostgreSQL 索引反射

MySQL / MariaDB 唯一约束和反射

#3184

新的系统以安全方式发出参数化警告

长期以来,存在着一个限制,即警告消息不能引用数据元素,因此特定函数可能会发出无限数量的唯一警告。这种情况最常见的地方是在Unicode 类型接收到非 Unicode 绑定参数值警告中。将数据值放入此消息中意味着该模块的 Python __warningregistry__,或在某些情况下是 Python 全局的 warnings.onceregistry,会无限增长,因为在大多数警告情况下,这两个集合中的一个会填充每个不同的警告消息。

此处的更改是通过使用一个特殊的string类型,故意更改字符串的哈希方式,我们可以控制大量参数化消息仅在一小组可能的哈希值上进行哈希,使得像Unicode 类型接收到非 Unicode 绑定参数值这样的警告可以被定制为仅发出特定次数;超出此次数,Python 警告注册表将开始记录它们作为重复项。

举例来说,以下测试脚本将仅对一千个参数集中的十个发出警告:

from sqlalchemy import create_engine, Unicode, select, cast
import random
import warnings
e = create_engine("sqlite://")
# Use the "once" filter (which is also the default for Python
# warnings).  Exactly ten of these warnings will
# be emitted; beyond that, the Python warnings registry will accumulate
# new values as dupes of one of the ten existing.
warnings.filterwarnings("once")
for i in range(1000):
    e.execute(
        select([cast(("foo_%d" % random.randint(0, 1000000)).encode("ascii"), Unicode)])
    )

这里的警告格式为:

/path/lib/sqlalchemy/sql/sqltypes.py:186: SAWarning: Unicode type received
  non-unicode bind param value 'foo_4852'. (this warning may be
  suppressed after 10 occurrences)

#3178

Select/Query LIMIT / OFFSET 可以指定为任意 SQL 表达式

Select.limit()Select.offset() 方法现在接受任何 SQL 表达式作为参数,而不仅仅是整数值。ORM Query 对象也会将任何表达式传递给底层的 Select 对象。通常用于允许传递绑定参数,稍后可以用值替换:

sel = select([table]).limit(bindparam("mylimit")).offset(bindparam("myoffset"))

不支持非整数 LIMIT 或 OFFSET 表达式的方言可能继续不支持此行为;第三方方言可能还需要修改以利用新行为。当前使用 ._limit._offset 属性的方言将继续对指定为简单整数值的限制/偏移量的情况进行处理。但是,当指定 SQL 表达式时,这两个属性将在访问时引发 CompileError。希望支持新功能的第三方方言现在应调用 ._limit_clause._offset_clause 属性以接收完整的 SQL 表达式,而不是整数值。

ForeignKeyConstraint 上的 use_alter 标志(通常)不再需要

MetaData.create_all()MetaData.drop_all() 方法现在将使用一个系统,自动为涉及表之间相互依赖循环的外键约束渲染 ALTER 语句,无需指定 ForeignKeyConstraint.use_alter。此外,外键约束现在不再需要名称即可通过  ALTER 创建;仅在 DROP 操作时需要名称。在 DROP 的情况下,该功能将确保只有具有显式名称的约束实际上包含在 ALTER  语句中。在 DROP 中存在无法解决的循环的情况下,如果无法继续进行 DROP,系统现在会发出简洁明了的错误消息。

ForeignKeyConstraint.use_alterForeignKey.use_alter 标志保持不变,并且继续具有相同的效果,即在 CREATE/DROP 情景中需要 ALTER 来建立这些约束条件。

自版本 1.0.1 起,在 SQLite 的情况下,特殊逻辑会接管,SQLite 不支持 ALTER,在 DROP  过程中,如果给定的表存在无法解析的循环,则会发出警告,并且这些表将无序删除,这在 SQLite 上通常没问题,除非启用了约束条件。要解决警告并在  SQLite 数据库上至少实现部分排序,特别是在启用约束条件的数据库中,重新将 “use_alter” 标志应用于那些应该在排序中显式省略的 ForeignKeyForeignKeyConstraint 对象。

另见

通过 ALTER 创建/删除外键约束 - 新行为的完整描述。

#3282

ResultProxy “auto close” 现在是 “soft” close

在很多版本中,ResultProxy 对象一直会在获取所有结果行后自动关闭。这是为了允许在不需要显式调用 ResultProxy.close() 的情况下使用该对象;由于所有的 DBAPI 资源都已被释放,因此可以安全地丢弃该对象。然而,该对象仍保持严格的“closed”行为,这意味着任何后续对 ResultProxy.fetchone()ResultProxy.fetchmany()ResultProxy.fetchall() 的调用都会引发 ResourceClosedError

>>> result = connection.execute(stmt)
>>> result.fetchone()
(1, 'x')
>>> result.fetchone()
None  # indicates no more rows
>>> result.fetchone()
exception: ResourceClosedError

此行为与 pep-249 的规定不一致,pep-249 规定即使结果已经耗尽,仍然可以重复调用 fetch 方法。这也会影响到某些结果代理的实现行为,比如 cx_oracle 方言中某些数据类型使用的 BufferedColumnResultProxy

为了解决这个问题,ResultProxy的“closed”状态已被分为两个状态;“软关闭”执行了“关闭”的大部分操作,即释放了  DBAPI 游标,并且在 “close with result” 对象的情况下还会释放连接,并且“closed”状态包括了“soft  close”中的所有内容,同时还将 fetch 方法设为“closed”。现在永远不会隐式调用 ResultProxy.close() 方法,只会调用非公开的 ResultProxy._soft_close() 方法:

>>> result = connection.execute(stmt)
>>> result.fetchone()
(1, 'x')
>>> result.fetchone()
None  # indicates no more rows
>>> result.fetchone()
None  # still None
>>> result.fetchall()
[]
>>> result.close()
>>> result.fetchone()
exception: ResourceClosedError  # *now* it raises

#3330 #3329

CHECK Constraints 现在支持命名约定中的%(column_0_name)s标记

%(column_0_name)s将从CheckConstraint的表达式中找到的第一列派生:

metadata = MetaData(naming_convention={"ck": "ck_%(table_name)s_%(column_0_name)s"})
foo = Table("foo", metadata, Column("value", Integer))
CheckConstraint(foo.c.value > 5)

将呈现为:

CREATE  TABLE  foo  (
  value  INTEGER,
  CONSTRAINT  ck_foo_value  CHECK  (value  >  5)
)

命名约定与由SchemaType(如BooleanEnum)产生的约束的组合现在也将使用所有 CHECK 约束约定。

另见

命名 CHECK 约束

为布尔值、枚举和其他模式类型配置命名

#3299

当其引用的列附加时,引用未附加的列的约束可以自动附加到表上

至少从版本 0.8 开始,Constraint已经能够根据传递的表附加列自动“附加”到Table上:

from sqlalchemy import Table, Column, MetaData, Integer, UniqueConstraint
m = MetaData()
t = Table("t", m, Column("a", Integer), Column("b", Integer))
uq = UniqueConstraint(t.c.a, t.c.b)  # will auto-attach to Table
assert uq in t.constraints

为了帮助处理一些在声明时经常出现的情况,即使Column对象尚未与Table关联,此相同的自动附加逻辑现在也可以起作用;额外的事件被建立,以便当这些Column对象关联时,也添加了Constraint

from sqlalchemy import Table, Column, MetaData, Integer, UniqueConstraint
m = MetaData()
a = Column("a", Integer)
b = Column("b", Integer)
uq = UniqueConstraint(a, b)
t = Table("t", m, a, b)
assert uq in t.constraints  # constraint auto-attached

上述功能是截至版本 1.0.0b3 的最后添加的。从版本 1.0.4 开始对#3411的修复确保如果Constraint引用了Column对象和字符串列名称的混合;因为我们尚未跟踪将名称添加到Table中:

from sqlalchemy import Table, Column, MetaData, Integer, UniqueConstraint
m = MetaData()
a = Column("a", Integer)
b = Column("b", Integer)
uq = UniqueConstraint(a, "b")
t = Table("t", m, a, b)
# constraint *not* auto-attached, as we do not have tracking
# to locate when a name 'b' becomes available on the table
assert uq not in t.constraints

在上面的示例中,将列“a”附加到表“t”的附加事件将在附加列“b”之前触发(因为“a”在构造Table时在“b”之前声明),如果约束尝试附加时无法找到“b”,约束将失败。为了保持一致性,如果约束引用任何字符串名称,则跳过在列附加时自动附加的逻辑。

Constraint构造时,如果Table已经包含所有目标Column对象,则原始的自动附加逻辑当然仍然存在:

from sqlalchemy import Table, Column, MetaData, Integer, UniqueConstraint
m = MetaData()
a = Column("a", Integer)
b = Column("b", Integer)
t = Table("t", m, a, b)
uq = UniqueConstraint(a, "b")
# constraint auto-attached normally as in older versions
assert uq in t.constraints

#3341 #3411

INSERT FROM SELECT 现在包括 Python 和 SQL 表达式默认值

如果未另行指定,则Insert.from_select()现在将包括 Python 和 SQL 表达式默认值;现在解除了非服务器列默认值不包括在 INSERT FROM SELECT 中的限制,并将这些表达式作为常量呈现到 SELECT 语句中:

from sqlalchemy import Table, Column, MetaData, Integer, select, func
m = MetaData()
t = Table(
    "t", m, Column("x", Integer), Column("y", Integer, default=func.somefunction())
)
stmt = select([t.c.x])
print(t.insert().from_select(["x"], stmt))

将呈现:

INSERT  INTO  t  (x,  y)  SELECT  t.x,  somefunction()  AS  somefunction_1
FROM  t

可以使用Insert.from_select.include_defaults来禁用此功能。

Column 服务器默认值现在呈现为字面值

当由Column.server_default设置的DefaultClause作为要编译的 SQL 表达式存在时,“字面绑定”编译器标志将被打开。这允许嵌入在 SQL 中的字面值正确呈现,例如:

from sqlalchemy import Table, Column, MetaData, Text
from sqlalchemy.schema import CreateTable
from sqlalchemy.dialects.postgresql import ARRAY, array
from sqlalchemy.dialects import postgresql
metadata = MetaData()
tbl = Table(
    "derp",
    metadata,
    Column("arr", ARRAY(Text), server_default=array(["foo", "bar", "baz"])),
)
print(CreateTable(tbl).compile(dialect=postgresql.dialect()))

现在呈现:

CREATE  TABLE  derp  (
  arr  TEXT[]  DEFAULT  ARRAY['foo',  'bar',  'baz']
)

以前,字面值"foo", "bar", "baz"会呈现为绑定参数,在 DDL 中毫无用处。

#3087

UniqueConstraint 现在是 Table 反射过程的一部分

使用autoload=True填充的Table对象现在将包括UniqueConstraint构造以及Index构造。对于 PostgreSQL 和 MySQL,这种逻辑有一些注意事项:

PostgreSQL

PostgreSQL 的行为是,当创建一个唯一约束时,它会隐式地创建一个对应该约束的唯一索引。Inspector.get_indexes()Inspector.get_unique_constraints() 方法将继续分别返回这些条目,其中 Inspector.get_indexes() 现在在索引条目中包含一个 duplicates_constraint 标记,指示检测到的相应约束。然而,在使用 Table(..., autoload=True) 进行完整表反射时,检测到 Index 构造与 UniqueConstraint 相关联,并且出现在 Table.indexes 集合中;只有 UniqueConstraint 将出现在 Table.constraints 集合中。这种去重逻辑通过在查询 pg_index 时连接到 pg_constraint 表来查看这两个构造是否关联。

MySQL

MySQL 没有单独的概念来区分唯一索引和唯一约束。虽然在创建表和索引时都支持两种语法,但在存储时并没有任何区别。Inspector.get_indexes()Inspector.get_unique_constraints()方法将继续同时返回 MySQL 中唯一索引的条目,其中Inspector.get_unique_constraints()在约束条目中使用新标记duplicates_index指示这是对应于该索引的重复条目。然而,在使用Table(..., autoload=True)执行完整表反射时,UniqueConstraint构造在任何情况下都是完全反映的Table构造的一部分;这个构造始终由在Table.indexes集合中存在unique=True设置的Index表示。

另请参阅

PostgreSQL 索引反射

MySQL / MariaDB 唯一约束和反射

#3184

PostgreSQL

当创建唯一约束时,PostgreSQL 的行为是隐式创建与该约束对应的唯一索引。Inspector.get_indexes()Inspector.get_unique_constraints() 方法将继续分别返回这些条目,其中 Inspector.get_indexes() 现在在索引条目中特征化了一个 duplicates_constraint 标记,表示当检测到相应约束时。然而,在使用 Table(..., autoload=True) 进行完整表反射时,Index 结构被检测为与 UniqueConstraint 相关联,并且会出现在 Table.indexes 集合中;只有 UniqueConstraint 会出现在 Table.constraints 集合中。这个去重逻辑通过在查询 pg_index 时连接到 pg_constraint 表来查看这两个结构是否相关联。

MySQL

MySQL 没有单独的概念来区分唯一索引唯一约束。虽然在创建表和索引时支持两种语法,但在存储时并没有任何区别。Inspector.get_indexes()Inspector.get_unique_constraints()方法将继续同时返回 MySQL 中唯一索引的条目,其中Inspector.get_unique_constraints()在约束条目中具有一个新的标记duplicates_index,表示这是对应该索引的重复条目。然而,在使用Table(..., autoload=True)执行完整表反射时,UniqueConstraint构造在任何情况下都是完全反映的Table构造的一部分;这个构造始终由在Table.indexes集合中存在unique=True设置的Index表示。

另请参阅

PostgreSQL 索引反射

MySQL / MariaDB 唯一约束和反射

#3184

安全发出参数化警告的新系统

长期以来,存在一个限制,即警告消息不能引用数据元素,这样一个特定函数可能会发出无限数量的唯一警告。这种情况最常见的地方是在Unicode type received non-unicode bind param value警告中。将数据值放入此消息中意味着该模块的 Python __warningregistry__,或在某些情况下是 Python 全局的warnings.onceregistry,将无限增长,因为在大多数警告场景中,这两个集合中的一个会填充每个不同的警告消息。

这里的变化是通过使用一种特殊的string类型,故意改变字符串的哈希方式,我们可以控制大量参数化消息仅在一小组可能的哈希值上进行哈希,这样一个警告,比如Unicode type received non-unicode bind param value,可以被定制为仅发出特定次数;在那之后,Python 警告注册表将开始记录它们作为重复项。

为了说明,以下测试脚本将仅显示对于 1000 个参数集中的十个参数集发出的十个警告:

from sqlalchemy import create_engine, Unicode, select, cast
import random
import warnings
e = create_engine("sqlite://")
# Use the "once" filter (which is also the default for Python
# warnings).  Exactly ten of these warnings will
# be emitted; beyond that, the Python warnings registry will accumulate
# new values as dupes of one of the ten existing.
warnings.filterwarnings("once")
for i in range(1000):
    e.execute(
        select([cast(("foo_%d" % random.randint(0, 1000000)).encode("ascii"), Unicode)])
    )

这里的警告格式是:

/path/lib/sqlalchemy/sql/sqltypes.py:186: SAWarning: Unicode type received
  non-unicode bind param value 'foo_4852'. (this warning may be
  suppressed after 10 occurrences)

#3178


SqlAlchemy 2.0 中文文档(七十六)(4)https://developer.aliyun.com/article/1561152

相关文章
|
5月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(七十四)(5)
SqlAlchemy 2.0 中文文档(七十四)
54 6
|
5月前
|
SQL JSON 测试技术
SqlAlchemy 2.0 中文文档(七十五)(2)
SqlAlchemy 2.0 中文文档(七十五)
50 3
|
5月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(七十四)(3)
SqlAlchemy 2.0 中文文档(七十四)
53 1
|
5月前
|
SQL JSON 关系型数据库
SqlAlchemy 2.0 中文文档(七十五)(4)
SqlAlchemy 2.0 中文文档(七十五)
71 1
|
5月前
|
SQL Python
SqlAlchemy 2.0 中文文档(七十四)(4)
SqlAlchemy 2.0 中文文档(七十四)
35 6
|
5月前
|
SQL JSON 关系型数据库
SqlAlchemy 2.0 中文文档(七十五)(1)
SqlAlchemy 2.0 中文文档(七十五)
89 4
|
5月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(七十六)(1)
SqlAlchemy 2.0 中文文档(七十六)
43 2
|
5月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(七十六)(2)
SqlAlchemy 2.0 中文文档(七十六)
80 2
|
5月前
|
SQL 存储 数据库
SqlAlchemy 2.0 中文文档(一)(4)
SqlAlchemy 2.0 中文文档(一)
80 1
|
5月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(七十四)(1)
SqlAlchemy 2.0 中文文档(七十四)
69 1