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
将是最终要映射到 SomeClass
的 Column
对象,而不是直接存在于 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
的表,其中包含 id
和 something_id
列,而 Concrete
还将具有一个名为 something
的关系。新功能是 Abstract
也将有一个独立配置的关系 something
,该关系构建在基类的多态联合上。
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
结构内存使用方面的显著改进
通过更多内部对象的__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})
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()
.info 字典改进
InspectionAttr.info
集合现在可以在从 Mapper.all_orm_descriptors
集合中检索的任何类型的对象上使用。这包括 hybrid_property
和 association_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
对象(例如ForeignKey
、UniqueConstraint
等)的构造函数参数可用,以及剩余的 ORM 构造,如synonym()
。
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,使标签逻辑再次失败;这些问题也已经修复。
新功能和改进 - 核心
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_alter
和 ForeignKey.use_alter
标志仍然存在,并且继续具有相同的效果,在 CREATE/DROP 场景中建立需要 ALTER 的约束。
从版本 1.0.1 开始,针对 SQLite 的特殊逻辑接管了 ALTER 的情况,在 DROP 期间,如果给定的表存在不可解析的循环,将发出警告,并且这些表将以 无 排序的方式删除,这在 SQLite 上通常是可以接受的,除非启用了约束。为了解决警告并在 SQLite 数据库上至少进行部分排序,特别是在启用了约束的数据库上,请重新将“use_alter”标志应用于那些应该明确排除的 ForeignKey
和 ForeignKeyConstraint
对象。
另请参阅
通过 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
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
生成的约束的命名约定的组合,例如Boolean
或Enum
现在也将使用所有 CHECK 约束约定。
另请参阅
命名 CHECK 约束
配置布尔值、枚举和其他模式类型的命名
当约束引用未附加的列时,可以在其引用的列附加到表时自动附加约束
自版本 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 唯一约束和反射
新的系统以安全方式发出参数化警告
长期以来,存在着一个限制,即警告消息不能引用数据元素,因此特定函数可能会发出无限数量的唯一警告。这种情况最常见的地方是在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)
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_alter
和 ForeignKey.use_alter
标志保持不变,并且继续具有相同的效果,即在 CREATE/DROP 情景中需要 ALTER 来建立这些约束条件。
自版本 1.0.1 起,在 SQLite 的情况下,特殊逻辑会接管,SQLite 不支持 ALTER,在 DROP 过程中,如果给定的表存在无法解析的循环,则会发出警告,并且这些表将无序删除,这在 SQLite 上通常没问题,除非启用了约束条件。要解决警告并在 SQLite 数据库上至少实现部分排序,特别是在启用约束条件的数据库中,重新将 “use_alter” 标志应用于那些应该在排序中显式省略的 ForeignKey
和 ForeignKeyConstraint
对象。
另见
通过 ALTER 创建/删除外键约束 - 新行为的完整描述。
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
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
(如Boolean
或Enum
)产生的约束的组合现在也将使用所有 CHECK 约束约定。
另见
命名 CHECK 约束
为布尔值、枚举和其他模式类型配置命名
当其引用的列附加时,引用未附加的列的约束可以自动附加到表上
至少从版本 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
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 中毫无用处。
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 唯一约束和反射
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 唯一约束和反射
安全发出参数化警告的新系统
长期以来,存在一个限制,即警告消息不能引用数据元素,这样一个特定函数可能会发出无限数量的唯一警告。这种情况最常见的地方是在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)
SqlAlchemy 2.0 中文文档(七十六)(4)https://developer.aliyun.com/article/1561152