SQLAlchemy 2.0 文档
变更和迁移
- SQLAlchemy 2.0 - 主要迁移指南
- SQLAlchemy 2.0 版本新特性?
- 2.0 更新日志
- 1.4 更新日志
- 1.3 更新日志
- 1.2 更新日志
- 1.1 更新日志
- 1.0 更新日志
- 0.9 更新日志
- 0.8 更新日志
- 0.7 更新日志
- 0.6 更新日志
- 0.5 更新日志
- 0.4 更新日志
- 0.3 更新日志
- 0.2 更新日志
- 0.1 更新日志
- SQLAlchemy 1.4 版本新特性?
- SQLAlchemy 1.3 版本新特性?¶
- 简介
- 概述
- 新特性和改进 - ORM
- 关键行为变更 - ORM
- 新特性和改进 - Core
- 关键变更 - Core
- 方言改进和变更 - PostgreSQL
- 方言改进和变更 - MySQL
- 方言改进和变更 - SQLite
- 方言改进和变更 - Oracle
- 方言改进和变更 - SQL Server
- 更改了 StatementError 格式 (换行符和 %s)
- SQLAlchemy 1.2 版本新特性?
- SQLAlchemy 1.1 版本新特性?
- SQLAlchemy 1.0 版本新特性?
- SQLAlchemy 0.9 版本新特性?
- SQLAlchemy 0.8 版本新特性?
- SQLAlchemy 0.7 版本新特性?
- SQLAlchemy 0.6 版本新特性?
- SQLAlchemy 0.5 版本新特性?
- SQLAlchemy 0.4 版本新特性?
项目版本
- 上一个: SQLAlchemy 1.4 版本新特性?
- 下一个: SQLAlchemy 1.2 版本新特性?
- 上级: 首页
- 本页目录
- SQLAlchemy 1.3 版本新特性?
- 简介
- 概述
- 新特性和改进 - ORM
- 关键行为变更 - ORM
- 新特性和改进 - Core
- 关键变更 - Core
- 方言改进和变更 - PostgreSQL
- 方言改进和变更 - MySQL
- 方言改进和变更 - SQLite
- 方言改进和变更 - Oracle
- 方言改进和变更 - SQL Server
- 更改了 StatementError 格式 (换行符和 %s)
SQLAlchemy 1.3 版本新特性?¶
关于本文档
本文档描述了 SQLAlchemy 版本 1.2 和 SQLAlchemy 版本 1.3 之间的变更。
简介¶
本指南介绍了 SQLAlchemy 版本 1.3 中的新特性,并记录了将应用程序从 SQLAlchemy 1.2 系列迁移到 1.3 的用户会受到的影响。
请仔细阅读关于行为变更的章节,了解潜在的向后不兼容的行为变更。
概述¶
所有已弃用的元素都会发出弃用警告;添加了新的弃用¶
1.3 版本确保所有已弃用的行为和 API,包括所有长期以来被列为 “遗留” 的行为和 API,都会发出 DeprecationWarning
警告。 这包括使用诸如 Session.weak_identity_map
等参数和诸如 MapperExtension
等类时。 虽然所有弃用都在文档中注明,但它们通常没有使用正确的 restructured text 指令,也没有包含它们被弃用的版本。 特定 API 功能是否实际发出弃用警告并不一致。 一般的态度是,大多数或所有这些弃用的功能都被视为长期遗留功能,没有计划删除它们。
此更改包括所有记录在案的弃用现在都在文档中使用正确的 restructured text 指令和版本号,明确表示该功能或用例将在未来版本中删除(例如,不再有永远的遗留用例),并且使用任何此类功能或用例肯定会发出 DeprecationWarning
,这在 Python 3 中以及使用 Pytest 等现代测试工具时,在标准错误流中更加明确。 目标是应开始完全删除这些长期弃用的功能(最早可追溯到 0.7 或 0.6 版本),而不是将它们作为 “遗留” 功能保留下来。 此外,自 1.3 版本起,还添加了一些重要的新弃用。 由于 SQLAlchemy 已被成千上万的开发人员实际使用了 14 年,因此可以指出一个单一的用例流,这些用例流可以很好地融合在一起,并剔除那些与这种单一工作方式相悖的功能和模式。
更大的背景是,SQLAlchemy 寻求适应即将到来的仅限 Python 3 的世界以及类型注解的世界,并且为了实现此目标, 初步 计划对 SQLAlchemy 进行重大重构,这将有望大大减少 API 的认知负担,并对 Core 和 ORM 之间在实现和使用方面的众多差异进行重大改进。 由于这两个系统在 SQLAlchemy 的首次发布后都发生了巨大变化,特别是 ORM 仍然保留了许多 “附加” 行为,这些行为使 Core 和 ORM 之间的分隔墙太高。 通过提前将 API 集中在每个受支持用例的单一模式上,最终迁移到经过重大更改的 API 的工作将变得更简单。
对于 1.3 中添加的最重要的弃用,请参阅下面的链接部分。
新特性和改进 - ORM¶
与 AliasedClass 的关系取代了对非主要映射器的需求¶
“非主要映射器” 是在 命令式映射 样式中创建的 Mapper
,它充当针对已映射类和不同类型的 selectable 的附加映射器。 非主要映射器的根源在于 SQLAlchemy 的 0.1、0.2 系列,当时预计 Mapper
对象将成为主要的查询构造接口,在 Query
对象存在之前。
随着 Query
和后来的 AliasedClass
构造的出现,非主要映射器的大多数用例都消失了。 这是一件好事,因为 SQLAlchemy 还在 0.5 系列左右完全放弃了 “经典” 映射,转而支持声明式系统。
当人们意识到,当使用备用 selectable 的非主要映射器作为映射目标时,而不是尝试构造 relationship.primaryjoin
来涵盖特定对象间关系的所有复杂性时,可以实现一些非常难以定义的 relationship()
配置时,仍然保留了一个非主要映射器的用例。
随着此用例变得越来越流行,其局限性也变得显而易见,包括非主要映射器难以针对添加新列的 selectable 进行配置,映射器不继承原始映射的关系,在非主要映射器上显式配置的关系无法与加载器选项良好地配合使用,并且非主要映射器也不提供可用于查询的基于列的属性的完全功能命名空间(同样,在旧的 0.1 - 0.4 时代,人们会直接将 Table
对象与 ORM 一起使用)。
缺失的部分是允许 relationship()
直接引用 AliasedClass
。 AliasedClass
已经完成了我们希望非主要映射器执行的所有操作; 它允许从备用 selectable 加载现有的映射类,它继承了现有映射器的所有属性和关系,它与加载器选项配合得非常好,并且它提供了一个类对象,该对象可以像类本身一样混合到查询中。 随着此更改,以前在 配置关系连接 中用于非主要映射器的配方已更改为别名类。
在 与别名类的关系 中,原始的非主要映射器如下所示
j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)
B_viacd = mapper(
B,
j,
non_primary=True,
primary_key=[j.c.b_id],
properties={
"id": j.c.b_id, # so that 'id' looks the same as before
"c_id": j.c.c_id, # needed for disambiguation
"d_c_id": j.c.d_c_id, # needed for disambiguation
"b_id": [j.c.b_id, j.c.d_b_id],
"d_id": j.c.d_id,
},
)
A.b = relationship(B_viacd, primaryjoin=A.b_id == B_viacd.c.b_id)
这些属性是必要的,以便重新映射附加列,使其不与映射到 B
的现有列冲突,并且有必要定义新的主键。
使用新方法,所有这些冗长都消失了,并且在建立关系时直接引用了附加列
j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id)
B_viacd = aliased(B, j, flat=True)
A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id)
非主要映射器现在已弃用,最终目标是完全取消经典映射作为一项功能。 声明式 API 将成为唯一的映射方式,这有望实现内部改进和简化,并提供更清晰的文档说明。
selectin 加载不再对简单的一对多关系使用 JOIN¶
1.2 版本中添加的 “selectin” 加载功能引入了一种极其高效的新方法来迫切加载集合,在许多情况下比 “子查询” 迫切加载快得多,因为它不依赖于重新声明原始 SELECT 查询,而是使用简单的 IN 子句。 但是,“selectin” 加载仍然依赖于在父表和相关表之间呈现 JOIN,因为它需要在行中包含父主键值才能匹配行。 在 1.3 中,添加了一项新的优化,它将在最常见的情况下省略此 JOIN,即简单的一对多加载,其中相关行已在其外键列中包含父行的主键。 这再次提供了显着的性能提升,因为 ORM 现在可以在一个查询中加载大量集合,而根本不使用 JOIN 或子查询。
给定一个映射
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
bs = relationship("B", lazy="selectin")
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
在 1.2 版本的 “selectin” 加载中,从 A 到 B 的加载如下所示
SELECT a.id AS a_id FROM a
SELECT a_1.id AS a_1_id, b.id AS b_id, b.a_id AS b_a_id
FROM a AS a_1 JOIN b ON a_1.id = b.a_id
WHERE a_1.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY a_1.id
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
使用新行为,加载如下所示
SELECT a.id AS a_id FROM a
SELECT b.a_id AS b_a_id, b.id AS b_id FROM b
WHERE b.a_id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY b.a_id
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
此行为作为自动行为发布,使用类似于延迟加载的启发式方法来确定是否可以直接从身份映射中获取相关实体。 但是,与大多数查询功能一样,该功能的实现由于关于多态加载的高级场景而变得更加复杂。 如果遇到问题,用户应报告错误,但是此更改还包括一个标志 relationship.omit_join
,可以在 relationship()
上将其设置为 False
以禁用优化。
改进了多对一查询表达式的行为¶
当构建将多对一关系与对象值进行比较的查询时,例如
u1 = session.query(User).get(5)
query = session.query(Address).filter(Address.user == u1)
上面的表达式 Address.user == u1
,最终编译为通常基于 User
对象的主键列的 SQL 表达式,如 "address.user_id = 5"
,使用延迟可调用对象,以便尽可能晚地在绑定表达式中检索值 5
。 这是为了适应 Address.user == u1
表达式可能针对尚未刷新的 User
对象(该对象依赖于服务器生成的主键值)的用例,以及即使 u1
的主键值自表达式创建以来已更改,表达式始终返回正确结果的用例。
但是,此行为的副作用是,如果 u1
在表达式求值时最终过期,则会导致额外的 SELECT 语句,并且如果 u1
也从 Session
中分离,则会引发错误
u1 = session.query(User).get(5)
query = session.query(Address).filter(Address.user == u1)
session.expire(u1)
session.expunge(u1)
query.all() # <-- would raise DetachedInstanceError
当 Session
被提交并且 u1
实例超出范围时,对象的过期/清除可能会隐式发生,因为 Address.user == u1
表达式不会强引用对象本身,而仅强引用其 InstanceState
。
修复方法是允许 Address.user == u1
表达式根据在表达式编译时尝试正常检索或加载值(如现在所做的那样)来求值 5
值,但是如果对象已分离并且已过期,则会从 InstanceState
上的新机制中检索该值,该机制将在属性过期时 memoize 该状态上特定属性的最后已知值。 仅当表达式功能需要时,此机制才对特定属性 / InstanceState
启用,以节省性能/内存开销。
最初,尝试了更简单的方法,例如立即求值表达式,并进行了各种安排以在稍后尝试加载值(如果不存在),但是困难的边缘情况是列属性(通常是自然主键)的值正在更改的情况。 为了确保像 Address.user == u1
这样的表达式始终为 u1
的当前状态返回正确的答案,它将返回持久对象的当前数据库持久化值,并在必要时通过 SELECT 查询取消过期;对于分离的对象,它将返回最近的已知值,无论对象何时过期,都使用 InstanceState
中的新功能来跟踪列属性的最后已知值,只要属性即将过期。
现代属性 API 功能用于在无法求值该值时指示特定错误消息,这两种情况是列属性从未设置过,以及在首次求值时对象已过期且现在已分离。 在所有情况下,都不再引发 DetachedInstanceError
。
多对一替换不会为 “raiseload” 引发异常,也不会为 “旧” 对象引发 detached 异常¶
给定一个延迟加载将在多对一关系上进行以加载 “旧” 值的情况,如果关系未指定 relationship.active_history
标志,则不会为分离的对象引发断言
a1 = session.query(Address).filter_by(id=5).one()
session.expunge(a1)
a1.user = some_user
上面,当在分离的 a1
对象上替换 .user
属性时,将引发 DetachedInstanceError
,因为该属性正在尝试从身份映射中检索 .user
的先前值。 更改是操作现在继续进行,而无需加载旧值。
相同的更改也适用于 lazy="raise"
加载器策略
class Address(Base):
# ...
user = relationship("User", ..., lazy="raise")
以前,由于属性尝试检索先前的值,a1.user
的关联将调用 “raiseload” 异常。 现在,在加载 “旧” 值的情况下,将跳过此断言。
为 ORM 属性实现了 “del” 操作¶
Python del
操作对于映射属性(标量列或对象引用)来说并不是真正可用的。 已添加对此操作的正确支持,其中 del
操作大致等效于将属性设置为 None
值
some_object = session.query(SomeObject).get(5)
del some_object.some_attribute # from a SQL perspective, works like "= None"
info 字典已添加到 InstanceState¶
将 .info
字典添加到 InstanceState
类,该类是从对映射对象调用 inspect()
获取的对象。 这允许自定义配方添加关于对象的其他信息,这些信息将随该对象的完整生命周期在内存中携带
from sqlalchemy import inspect
u1 = User(id=7, name="ed")
inspect(u1).info["user_info"] = "7|ed"
水平分片扩展支持批量更新和删除方法¶
ShardedQuery
扩展对象支持 Query.update()
和 Query.delete()
批量更新/删除方法。 调用 query_chooser
可调用对象时会咨询它,以便根据给定的条件在多个分片上运行更新/删除操作。
关联代理的改进¶
虽然没有任何特别的原因,但关联代理扩展在本周期进行了许多改进。
关联代理具有新的 cascade_scalar_deletes 标志¶
给定一个映射,如下所示
class A(Base):
__tablename__ = "test_a"
id = Column(Integer, primary_key=True)
ab = relationship("AB", backref="a", uselist=False)
b = association_proxy(
"ab", "b", creator=lambda b: AB(b=b), cascade_scalar_deletes=True
)
class B(Base):
__tablename__ = "test_b"
id = Column(Integer, primary_key=True)
ab = relationship("AB", backref="b", cascade="all, delete-orphan")
class AB(Base):
__tablename__ = "test_ab"
a_id = Column(Integer, ForeignKey(A.id), primary_key=True)
b_id = Column(Integer, ForeignKey(B.id), primary_key=True)
对 A.b
的赋值将生成一个 AB
对象
a.b = B()
A.b
关联是标量,并且包含一个新的标志 AssociationProxy.cascade_scalar_deletes
。 设置此标志后,将 A.b
设置为 None
也会删除 A.ab
。 默认行为仍然是保持 a.ab
不变
a.b = None
assert a.ab is None
虽然乍一看,此逻辑似乎应该只查看现有关系的 “cascade” 属性,但仅从这一点尚不清楚是否应删除代理对象,因此该行为作为显式选项提供。
此外,del
现在对标量的作用类似于设置为 None
del a.b
assert a.ab is None
AssociationProxy 在每个类的基础上存储特定于类的状态¶
AssociationProxy
对象根据与其关联的父映射类做出许多决策。 虽然 AssociationProxy
历史上最初是一个相对简单的 “getter”,但很快就变得明显,它还需要就其引用的属性类型(例如标量或集合、映射对象或简单值等)做出决策。 为了实现这一点,它需要检查从其父类引用的映射属性或其他引用描述符或属性。 但是,在 Python 描述符机制中,描述符仅在其父类的上下文中访问时才了解其 “父” 类,例如调用 MyClass.some_descriptor
,这将调用 __get__()
方法,该方法传入类。 因此,AssociationProxy
对象将存储特定于该类的状态,但仅在调用此方法后才存储; 尝试预先检查此状态而不先将 AssociationProxy
作为描述符访问将引发错误。 此外,它会假设 __get__()
看到的第一个类将是它需要了解的唯一父类。 尽管即使存在此缺点,关联代理仍然会在其当前行为中取得很大进展,但它仍然在某些情况下以及确定最佳 “所有者” 类的复杂问题中留下了缺点。
这些问题现在已得到解决,因为 AssociationProxy
在调用 __get__()
时不再修改其自身的内部状态; 相反,每个类都会生成一个新对象,称为 AssociationProxyInstance
,它处理特定映射父类特有的所有状态(当父类未映射时,不生成 AssociationProxyInstance
)。 关联代理的单个 “所有者类” 的概念(尽管在 1.1 中得到了改进)基本上已被一种方法取代,在这种方法中,AP 现在可以平等地对待任意数量的 “所有者” 类。
为了适应想要检查 AssociationProxy
的此状态而无需调用 __get__()
的应用程序,添加了一个新方法 AssociationProxy.for_class()
,该方法提供对特定于类的 AssociationProxyInstance
的直接访问,如下所示
class User(Base):
# ...
keywords = association_proxy("kws", "keyword")
proxy_state = inspect(User).all_orm_descriptors["keywords"].for_class(User)
一旦我们有了 AssociationProxyInstance
对象,在上面的示例中存储在 proxy_state
变量中,我们就可以查看特定于 User.keywords
代理的属性,例如 target_class
>>> proxy_state.target_class
Keyword
AssociationProxy 现在为面向列的目标提供标准列运算符¶
给定一个 AssociationProxy
,其目标是一个数据库列,并且不是对象引用或另一个关联代理
class User(Base):
# ...
elements = relationship("Element")
# column-based association proxy
values = association_proxy("elements", "value")
class Element(Base):
# ...
value = Column(String)
User.values
关联代理指向 Element.value
列。现在可以使用标准列操作,例如 like
>>> print(s.query(User).filter(User.values.like("%foo%")))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value LIKE :value_1)
等于
:
>>> print(s.query(User).filter(User.values == "foo"))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value = :value_1)
当与 None
比较时,IS NULL
表达式会增加一个相关行根本不存在的测试;这与之前的行为相同
>>> print(s.query(User).filter(User.values == None))
SELECT "user".id AS user_id
FROM "user"
WHERE (EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND element.value IS NULL)) OR NOT (EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id))
请注意,ColumnOperators.contains()
运算符实际上是一个字符串比较运算符;这是一个行为上的改变,因为以前,关联代理仅将 .contains
用作列表包含运算符。对于面向列的比较,它现在的行为类似于 “like”
>>> print(s.query(User).filter(User.values.contains("foo")))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM element
WHERE "user".id = element.user_id AND (element.value LIKE '%' || :value_1 || '%'))
为了测试 User.values
集合是否简单地包含值 "foo"
,应使用等于运算符(例如 User.values == 'foo'
);这在以前的版本中也有效。
当将基于对象的关联代理与集合一起使用时,其行为与以前相同,即测试集合成员资格,例如,给定一个映射
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
user_elements = relationship("UserElement")
# object-based association proxy
elements = association_proxy("user_elements", "element")
class UserElement(Base):
__tablename__ = "user_element"
id = Column(Integer, primary_key=True)
user_id = Column(ForeignKey("user.id"))
element_id = Column(ForeignKey("element.id"))
element = relationship("Element")
class Element(Base):
__tablename__ = "element"
id = Column(Integer, primary_key=True)
value = Column(String)
.contains()
方法产生与以前相同的表达式,测试 User.elements
列表中是否存在 Element
对象
>>> print(s.query(User).filter(User.elements.contains(Element(id=1))))
SELECT "user".id AS user_id
FROM "user"
WHERE EXISTS (SELECT 1
FROM user_element
WHERE "user".id = user_element.user_id AND :param_1 = user_element.element_id)
总的来说,此更改是基于 AssociationProxy 在每个类的基础上存储特定于类的状态 的架构更改而启用的;由于代理现在在生成表达式时会分离出额外的状态,因此 AssociationProxyInstance
类既有面向对象的版本,也有面向列的版本。
Association Proxy 现在强引用父对象¶
关联代理集合长期以来仅维护对父对象的弱引用的行为已被撤销;现在,只要代理集合本身也在内存中,代理将维护对父对象的强引用,从而消除 “stale association proxy” 错误。 正在以实验性质进行此更改,以查看是否会出现任何导致副作用的用例。
例如,给定一个带有关联代理的映射
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
bs = relationship("B")
b_data = association_proxy("bs", "data")
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
data = Column(String)
a1 = A(bs=[B(data="b1"), B(data="b2")])
b_data = a1.b_data
以前,如果 a1
超出作用域而被删除
del a1
在 a1
从作用域中删除后尝试迭代 b_data
集合会引发错误 "stale association proxy, parent object has gone out of scope"
。 这是因为关联代理需要访问实际的 a1.bs
集合才能生成视图,并且在此更改之前,它仅维护对 a1
的弱引用。 特别是,用户在执行内联操作时经常会遇到此错误,例如
collection = session.query(A).filter_by(id=1).first().b_data
上面,因为 A
对象会在 b_data
集合实际使用之前被垃圾回收。
更改是 b_data
集合现在维护对 a1
对象的强引用,以便它保持存在
assert b_data == ["b1", "b2"]
此更改引入的副作用是,如果应用程序像上面那样传递集合,则在集合也被丢弃之前,父对象将不会被垃圾回收。 与往常一样,如果 a1
在特定的 Session
中是持久的,它将一直保留在该会话的状态中,直到被垃圾回收。
请注意,如果此更改导致问题,则可能会对其进行修订。
为使用 AssociationProxy 的集合、字典实现批量替换¶
将集合或字典赋值给关联代理集合现在应该可以正确工作,而以前它会为现有键重新创建关联代理成员,从而导致由于同一对象的删除+插入而可能导致刷新失败的问题,现在它应该只在适当的情况下创建新的关联对象
class A(Base):
__tablename__ = "test_a"
id = Column(Integer, primary_key=True)
b_rel = relationship(
"B",
collection_class=set,
cascade="all, delete-orphan",
)
b = association_proxy("b_rel", "value", creator=lambda x: B(value=x))
class B(Base):
__tablename__ = "test_b"
__table_args__ = (UniqueConstraint("a_id", "value"),)
id = Column(Integer, primary_key=True)
a_id = Column(Integer, ForeignKey("test_a.id"), nullable=False)
value = Column(String)
# ...
s = Session(e)
a = A(b={"x", "y", "z"})
s.add(a)
s.commit()
# re-assign where one B should be deleted, one B added, two
# B's maintained
a.b = {"x", "z", "q"}
# only 'q' was added, so only one new B object. previously
# all three would have been re-created leading to flush conflicts
# against the deleted ones.
assert len(s.new) == 1
多对一反向引用在删除操作期间检查集合重复项¶
当 ORM 映射的集合作为 Python 序列存在时,通常是 Python list
,这是 relationship()
的默认值,包含重复项,并且对象从其一个位置删除但未从其他位置删除时,多对一反向引用会将其属性设置为 None
,即使一对多端仍然表示该对象存在。 即使一对多集合在关系模型中不能有重复项,但使用序列集合的 ORM 映射 relationship()
可以在内存中包含重复项,但限制是此重复状态既不能持久化也不能从数据库中检索。 特别是,在列表中临时存在重复项是 Python “交换” 操作的内在组成部分。 给定一个标准的一对多/多对一设置
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
bs = relationship("B", backref="a")
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
如果我们有一个带有两个 B
成员的 A
对象,并执行交换
a1 = A(bs=[B(), B()])
a1.bs[0], a1.bs[1] = a1.bs[1], a1.bs[0]
在上述操作期间,拦截标准 Python __setitem__
__delitem__
方法会传递一个临时状态,其中第二个 B()
对象在集合中出现两次。 当 B()
对象从其中一个位置删除时,B.a
反向引用会将引用设置为 None
,从而导致在刷新期间删除 A
对象和 B
对象之间的链接。 使用普通重复项也可以证明相同的问题
>>> a1 = A()
>>> b1 = B()
>>> a1.bs.append(b1)
>>> a1.bs.append(b1) # append the same b1 object twice
>>> del a1.bs[1]
>>> a1.bs # collection is unaffected so far...
[<__main__.B object at 0x7f047af5fb70>]
>>> b1.a # however b1.a is None
>>>
>>> session.add(a1)
>>> session.commit() # so upon flush + expire....
>>> a1.bs # the value is gone
[]
此修复确保在反向引用触发时(在集合被修改之前),集合会检查目标项是否正好有一个或零个实例,然后再取消设置多对一端,使用线性搜索,目前线性搜索使用 list.search
和 list.__contains__
。
最初认为需要在集合内部使用基于事件的引用计数方案,以便可以在集合的整个生命周期内跟踪所有重复实例,这将对所有集合操作(包括加载和附加的非常频繁的操作)增加性能/内存/复杂性影响。 相反,所采用的方法将额外的开销限制在不太常见的集合删除和批量替换操作上,并且观察到的线性扫描开销可以忽略不计; 单元工作内部以及批量替换集合时,已经在工作单元内使用了关系绑定集合的线性扫描。
关键行为变更 - 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 的 “显式优于隐式” 哲学。
FOR UPDATE 子句在连接的预加载子查询以及外部呈现¶
此更改专门适用于结合行限制查询(例如,使用 Query.first()
或 Query.limit()
)以及使用 Query.with_for_update()
方法的 joinedload()
加载策略。
给定如下查询
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
但是,由于 https://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”; 在任何情况下,它都不是必需的,因为像 PostgreSQL 一样,Oracle 正确地锁定了返回行的所有元素。
当使用 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 仍然高度特定于所使用的目标数据库,并且不能轻易地推广到更复杂的查询。
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
此修复现在包括,根据 passive_deletes="all"
,address.user_id
保持不变。 这种方法对于构建自定义 “版本表” 方案以及行被存档而不是删除的情况很有用。
新功能和改进 - Core¶
新的多列命名约定标记,长名称截断¶
为了适应 MetaData
命名约定需要在多列约束之间消除歧义并希望在生成的约束名称中使用所有列的情况,添加了一系列新的命名约定标记,包括 column_0N_name
、column_0_N_name
、column_0N_key
、column_0_N_key
、referred_column_0N_name
、referred_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 字符哈希。 例如,在标识符长度不能超过 63 个字符的 PostgreSQL 上,通常会从下面的表定义生成长约束名称
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
(如果这两个值不同)。
SQL 函数的二进制比较解释¶
此增强功能在 Core 级别实现,但主要适用于 ORM。
比较两个元素的 SQL 函数现在可以用作 “比较” 对象,适用于 ORM relationship()
中的用法,方法是首先使用 func
工厂像往常一样创建函数,然后在函数完成后调用 FunctionElement.as_comparison()
修饰符以生成具有 “left” 和 “right” 侧的 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 懒加载等功能生成如下 SQL
SELECT venue.id AS venue_id, venue.name AS venue_name
FROM venue
WHERE instr(venue.name, (? || ?)) = ? ORDER BY venue.name
('parent1', '/', 1)
以及连接加载,例如
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 连接的 ON 子句用 SQL 函数表示的任何情况。
Expanding IN 功能现在支持空列表¶
版本 1.2 中引入的 “expanding IN” 功能,在 后期扩展的 IN 参数集允许使用缓存语句的 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)
TypeEngine 方法 bind_expression、column_expression 与 Variant、特定于类型的类型一起工作¶
当 TypeEngine.bind_expression()
和 TypeEngine.column_expression()
方法出现在特定数据类型的 “impl” 上时,它们现在可以工作,从而允许方言以及 TypeDecorator
和 Variant
用例使用这些方法。
以下示例说明了将 SQL 时间转换函数应用于 LargeBinary
的 TypeDecorator
。 为了使此类型在 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 新的 “binary prefix” 要求以及用于转换 MySQL 的十进制绑定值。
QueuePool 的新后进先出策略¶
create_engine()
通常使用的连接池称为 QueuePool
。 此池使用与 Python 内置 Queue
类等效的对象来存储等待使用的数据库连接。 Queue
具有先进先出行为,旨在提供对持久存在于池中的数据库连接的循环使用。 但是,这样做的一个潜在缺点是,当池的利用率较低时,按顺序重用每个连接意味着尝试减少未使用连接的服务器端超时策略无法关闭这些连接。 为了适应这种情况,添加了一个新的标志 create_engine.pool_use_lifo
,它反转了 Queue
的 .get()
方法,以从队列的开头而不是结尾拉取连接,实际上将 “队列” 变成了 “堆栈” (考虑添加一个全新的名为 StackPool
的池,但这太冗长了)。
另请参阅
关键更改 - Core¶
完全删除将字符串 SQL 片段强制转换为 text()¶
版本 1.0 中首次添加的警告(在 将完整 SQL 片段强制转换为 text() 时发出警告 中描述)现在已转换为异常。 对于传递给诸如 Query.filter()
和 Select.order_by()
之类的方法的字符串片段被自动强制转换为 text()
构造,即使这已发出警告,但仍持续引发关注。 在 Select.order_by()
、Query.order_by()
、Select.group_by()
和 Query.group_by()
的情况下,字符串标签或列名仍然会解析为相应的表达式构造,但是如果解析失败,则会引发 CompileError
,从而阻止直接呈现原始 SQL 文本。
“threadlocal” 引擎策略已弃用¶
“threadlocal 引擎策略” 是在 SQLAlchemy 0.2 左右添加的,作为解决 SQLAlchemy 0.1 中标准操作方式(可以概括为 “threadlocal 一切”)被发现不足的问题的方案。 回顾一下,似乎相当荒谬的是,在 SQLAlchemy 的第一个版本(在各个方面都是 “alpha” 版本)中,人们担心已经有太多用户习惯了现有的 API 而无法简单地更改它。
SQLAlchemy 的原始使用模型如下所示
engine.begin()
table.insert().execute(parameters)
result = table.select().execute()
table.update().execute(parameters)
engine.commit()
在实际使用几个月后,很明显,试图假装“连接”或“事务”是一个隐藏的实现细节是一个糟糕的主意,特别是当有人需要一次处理多个数据库连接时。因此,我们今天看到的用法范例被引入了,只是缺少了上下文管理器,因为它们在 Python 中还不存在。
conn = engine.connect()
try:
trans = conn.begin()
conn.execute(table.insert(), parameters)
result = conn.execute(table.select())
conn.execute(table.update(), parameters)
trans.commit()
except:
trans.rollback()
raise
finally:
conn.close()
上述范例是人们需要的,但由于它仍然有点冗长(因为没有上下文管理器),旧的工作方式也被保留下来,并成为了 threadlocal 引擎策略。
今天,使用 Core 更加简洁,甚至比最初的模式更加简洁,这要归功于上下文管理器。
with engine.begin() as conn:
conn.execute(table.insert(), parameters)
result = conn.execute(table.select())
conn.execute(table.update(), parameters)
在这一点上,任何仍然依赖“threadlocal”风格的剩余代码都将通过此弃用声明被鼓励进行现代化改造 - 该功能应在 SQLAlchemy 的下一个主要版本系列(例如 1.4)中完全移除。连接池参数 Pool.use_threadlocal
也已被弃用,因为它在大多数情况下实际上没有任何效果,Engine.contextual_connect()
方法也是如此,该方法通常与 Engine.connect()
方法同义,除非在使用 threadlocal 引擎的情况下。
convert_unicode 参数已弃用¶
参数 String.convert_unicode
和 create_engine.convert_unicode
已被弃用。这些参数的目的是指示 SQLAlchemy 确保在 Python 2 下,传入的 Python Unicode 对象在传递到数据库之前被编码为字节串,并期望从数据库返回的字节串被转换回 Python Unicode 对象。在 Python 3 之前的时代,要做到这一点非常困难,因为几乎所有 Python DBAPI 默认情况下都没有启用 Unicode 支持,并且大多数在它们提供的 Unicode 扩展方面存在重大问题。最终,SQLAlchemy 添加了 C 扩展,这些扩展的主要目的之一是加速结果集中 Unicode 解码过程。
一旦 Python 3 被引入,DBAPI 开始更全面地支持 Unicode,更重要的是,默认情况下就支持。然而,特定 DBAPI 是否会从结果中返回 Unicode 数据,以及是否接受 Python Unicode 值作为参数的条件仍然极其复杂。“convert_unicode”标志的过时性由此开始,因为它们不再足以作为确保仅在需要时而不是在不需要时才进行编码/解码的手段。相反,“convert_unicode”开始由方言自动检测。这部分可以在引擎首次连接时发出的“SELECT ‘test plain returns’”和“SELECT ‘test_unicode_returns’” SQL 中看到;方言正在测试当前的 DBAPI 及其当前设置和后端数据库连接是否默认返回 Unicode。
最终结果是,最终用户在任何情况下都不再需要使用“convert_unicode”标志,如果需要,SQLAlchemy 项目需要知道这些情况是什么以及原因。目前,数百个 Unicode 往返测试在所有主要数据库中都通过了,而没有使用此标志,因此有相当高的置信度认为它们不再需要,除非在有争议的非用例中,例如访问来自遗留数据库的错误编码数据,这种情况更适合使用自定义类型。
方言改进和变更 - PostgreSQL¶
为 PostgreSQL 分区表添加了基本反射支持¶
SQLAlchemy 可以使用 1.2.6 版本中添加的标志 postgresql_partition_by
在 PostgreSQL CREATE TABLE 语句中呈现“PARTITION BY”序列。然而,'p'
类型直到现在才成为反射查询的一部分。
给定如下模式
dv = Table(
"data_values",
metadata_obj,
Column("modulus", Integer, nullable=False),
Column("data", String(30)),
postgresql_partition_by="range(modulus)",
)
sa.event.listen(
dv,
"after_create",
sa.DDL(
"CREATE TABLE data_values_4_10 PARTITION OF data_values "
"FOR VALUES FROM (4) TO (10)"
),
)
两个表名 'data_values'
和 'data_values_4_10'
将从 Inspector.get_table_names()
返回,此外,列也将从 Inspector.get_columns('data_values')
以及 Inspector.get_columns('data_values_4_10')
返回。这也扩展到将 Table(..., autoload=True)
与这些表一起使用。
方言改进和变更 - MySQL¶
协议级 ping 现在用于预 ping¶
包括 mysqlclient、python-mysql、PyMySQL 和 mysql-connector-python 在内的 MySQL 方言现在使用 connection.ping()
方法用于池预 ping 功能,详见 断开连接处理 - 悲观。这比以前在连接上发出“SELECT 1”的方法轻量得多。
控制 ON DUPLICATE KEY UPDATE 中参数的顺序¶
ON DUPLICATE KEY UPDATE
子句中 UPDATE 参数的顺序现在可以通过传递 2 元组列表来显式排序
from sqlalchemy.dialects.mysql import insert
insert_stmt = insert(my_table).values(id="some_existing_id", data="inserted value")
on_duplicate_key_stmt = insert_stmt.on_duplicate_key_update(
[
("data", "some data"),
("updated_at", func.current_timestamp()),
],
)
方言改进和变更 - SQLite¶
添加了对 SQLite JSON 的支持¶
添加了一个新的数据类型 JSON
,它代表 JSON
基本数据类型实现了 SQLite 的 json 成员访问函数。SQLite JSON_EXTRACT
和 JSON_QUOTE
函数被该实现用于提供基本的 JSON 支持。
请注意,数据类型本身在数据库中呈现的名称是“JSON”。这将创建一个具有“numeric”亲和性的 SQLite 数据类型,这通常不应该是一个问题,除非 JSON 值由单个整数值组成。然而,根据 SQLite 自身文档 https://www.sqlite.org/json1.html 中的示例,名称 JSON 因其熟悉性而被使用。
添加了对约束中 SQLite ON CONFLICT 的支持¶
SQLite 支持一个非标准的 ON CONFLICT 子句,该子句可以为独立约束以及某些列内联约束(如 NOT NULL)指定。通过添加到 UniqueConstraint
等对象的 sqlite_on_conflict
关键字以及几个 Column
特定的变体,已经添加了对这些子句的支持
some_table = Table(
"some_table",
metadata_obj,
Column("id", Integer, primary_key=True, sqlite_on_conflict_primary_key="FAIL"),
Column("data", Integer),
UniqueConstraint("id", "data", sqlite_on_conflict="IGNORE"),
)
上面的表将在 CREATE TABLE 语句中呈现为
CREATE TABLE some_table (
id INTEGER NOT NULL,
data INTEGER,
PRIMARY KEY (id) ON CONFLICT FAIL,
UNIQUE (id, data) ON CONFLICT IGNORE
)
另请参阅
方言改进和变更 - Oracle¶
国家字符数据类型对于通用 Unicode 不再强调,使用选项重新启用¶
默认情况下,Unicode
和 UnicodeText
数据类型现在对应于 Oracle 上的 VARCHAR2
和 CLOB
数据类型,而不是 NVARCHAR2
和 NCLOB
(也称为“国家”字符集类型)。这将在诸如它们如何在 CREATE TABLE
语句中呈现的行为中看到,以及当使用 Unicode
或 UnicodeText
的绑定参数时,不会有类型对象传递给 setinputsizes()
;cx_Oracle 本机处理字符串值。此更改基于 cx_Oracle 维护者的建议,即 Oracle 中的“国家”数据类型在很大程度上已经过时,并且性能不佳。它们还会干扰某些情况,例如应用于 trunc()
等函数的格式说明符时。
可能需要 NVARCHAR2
和相关类型的一种情况是数据库未使用符合 Unicode 标准的字符集。在这种情况下,可以将标志 use_nchar_for_unicode
传递给 create_engine()
以重新启用旧的行为。
与往常一样,显式使用 NVARCHAR2
和 NCLOB
数据类型将继续使用 NVARCHAR2
和 NCLOB
,包括在 DDL 中以及在使用 cx_Oracle 的 setinputsizes()
处理绑定参数时。
在读取方面,已将 Python 2 下的自动 Unicode 转换添加到 CHAR/VARCHAR/CLOB 结果行,以匹配 Python 3 下 cx_Oracle 的行为。为了减轻 cx_Oracle 方言之前在 Python 2 下使用此行为时的性能损失,SQLAlchemy 非常高性能(当构建 C 扩展时)的本机 Unicode 处理程序在 Python 2 下被使用。可以通过将 coerce_to_unicode
标志设置为 False 来禁用自动 unicode 强制。此标志现在默认为 True,并应用于结果集中返回的所有字符串数据,这些数据并非显式地在 Unicode
或 Oracle 的 NVARCHAR2/NCHAR/NCLOB 数据类型下。
cx_Oracle 连接参数现代化,已删除已弃用的参数¶
对 cx_oracle 方言以及 URL 字符串接受的参数进行了一系列现代化改造
已删除已弃用的参数
auto_setinputsizes
、allow_twophase
、exclude_setinputsizes
。threaded
参数的值(对于 SQLAlchemy 方言,该值始终默认为 True)不再默认生成。SQLAlchemyConnection
对象本身不被认为是线程安全的,因此无需传递此标志。将
threaded
传递给create_engine()
本身已被弃用。要将threaded
的值设置为True
,请将其传递给create_engine.connect_args
字典或使用查询字符串,例如oracle+cx_oracle://...?threaded=true
。URL 查询字符串上所有未被特殊消耗的参数现在都传递给 cx_Oracle.connect() 函数。其中一部分也被强制转换为 cx_Oracle 常量或布尔值,包括
mode
、purity
、events
和threaded
。与早期情况一样,所有 cx_Oracle
.connect()
参数都通过create_engine.connect_args
字典接受,关于此的文档是不准确的。
方言改进和变更 - SQL Server¶
支持 pyodbc fast_executemany¶
Pyodbc 最近添加的“fast_executemany”模式(在使用 Microsoft ODBC 驱动程序时可用)现在是 pyodbc / mssql 方言的一个选项。通过 create_engine()
传递它
engine = create_engine(
"mssql+pyodbc://scott:tiger@mssql2017:1433/test?driver=ODBC+Driver+13+for+SQL+Server",
fast_executemany=True,
)
另请参阅
影响 IDENTITY 启动值和增量的新参数,不建议使用 Sequence¶
SQL Server 从 SQL Server 2012 开始,现在支持具有真实 CREATE SEQUENCE
语法的序列。在 #4235 中,SQLAlchemy 将添加对这些序列的支持,使用 Sequence
,方式与其他方言相同。然而,目前的情况是,Sequence
在 SQL Server 上被专门重新用于影响主键列上 IDENTITY
规范的“start”和“increment”参数。为了实现向也可用普通序列的过渡,在整个 1.3 系列中使用 Sequence
将发出弃用警告。为了影响“start”和“increment”,请在 Column
上使用新的 mssql_identity_start
和 mssql_identity_increment
参数
test = Table(
"test",
metadata_obj,
Column(
"id",
Integer,
primary_key=True,
mssql_identity_start=100,
mssql_identity_increment=10,
),
Column("name", String(20)),
)
为了在非主键列上发出 IDENTITY
(这是一个很少使用但有效的 SQL Server 用例),请使用 Column.autoincrement
标志,在目标列上将其设置为 True
,在任何整数主键列上设置为 False
test = Table(
"test",
metadata_obj,
Column("id", Integer, primary_key=True, autoincrement=False),
Column("number", Integer, autoincrement=True),
)
另请参阅
更改了 StatementError 格式(换行符和 %s)¶
对 StatementError
的字符串表示形式引入了两项更改。“detail”和“SQL”部分现在用换行符分隔,并且保留原始 SQL 语句中存在的换行符。目标是提高可读性,同时仍然将原始错误消息保持在一行以用于日志记录。
这意味着以前看起来像这样的错误消息
sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError) A value is
required for bind parameter 'id' [SQL: 'select * from reviews\nwhere id = ?']
(Background on this error at: https://sqlalche.me/e/cd3x)
现在看起来像这样
sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError) A value is required for bind parameter 'id'
[SQL: select * from reviews
where id = ?]
(Background on this error at: https://sqlalche.me/e/cd3x)
此更改的主要影响是,使用者不能再假设完整的异常消息在单行上,但是从 DBAPI 驱动程序或 SQLAlchemy 内部生成的原始“error”部分仍将位于第一行。
flambé! 龙和 The Alchemist 图像设计由 Rotem Yaari 创建并慷慨捐赠。
使用 Sphinx 7.2.6 创建。文档最后生成时间:Tue 11 Mar 2025 02:40:17 PM EDT