SqlAlchemy 2.0 中文文档(七十二)(4)https://developer.aliyun.com/article/1561033
行为变化 - ORM
查询返回的“KeyedTuple”对象已被Row
替换
如在 RowProxy 不再是“代理”;现在称为 Row 并且行为类似于增强的命名元组中讨论的,Core 的RowProxy
对象现在被一个名为Row
的类替换。 基本的Row
对象现在更像一个命名元组,因此现在用作由Query
对象返回的类似元组的结果的基础,而不是以前的“KeyedTuple”类。
这样做的理由是,到达 SQLAlchemy 2.0,Core 和 ORM SELECT 语句将使用行为类似于命名元组的相同Row
对象返回结果行。 通过Row
的Row._mapping
属性可以获得类似字典的功能。 在此期间,Core 结果集将使用一个Row
子类LegacyRow
,该子类保留了以前的字典/元组混合行为,以确保向后兼容性,而Row
类将直接用于由Query
对象返回的 ORM 元组结果。
已经努力使得Row
的大部分功能集在 ORM 中可用,这意味着可以通过字符串名称以及实体/列进行访问:
row = s.query(User, Address).join(User.addresses).first() row._mapping[User] # same as row[0] row._mapping[Address] # same as row[1] row._mapping["User"] # same as row[0] row._mapping["Address"] # same as row[1] u1 = aliased(User) row = s.query(u1).only_return_tuples(True).first() row._mapping[u1] # same as row[0] row = s.query(User.id, Address.email_address).join(User.addresses).first() row._mapping[User.id] # same as row[0] row._mapping["id"] # same as row[0] row._mapping[users.c.id] # same as row[0]
另请参见
RowProxy 不再是“代理”;现在称为 Row,并且行为类似于增强的命名元组
#4710. ### 会话功能新的“autobegin”行为
以前,在其默认模式 autocommit=False
下,Session
在构造时会立即开始一个 SessionTransaction
对象,并且在每次调用 Session.rollback()
或 Session.commit()
后还会创建一个新的对象。
新行为是,这个 SessionTransaction
对象现在只在需要时才创建,当调用 Session.add()
或 Session.execute()
等方法时。但现在也可以显式调用 Session.begin()
来开始事务,即使在 autocommit=False
模式下,从而与未来风格的 _base.Connection
的行为相匹配。
这表明的行为变化是:
- 现在,即使在
autocommit=False
模式下,Session
也可以处于未开始事务的状态。以前,这种状态仅在“自动提交”模式下可用。 - 在这种状态下,
Session.commit()
和Session.rollback()
方法不起作用。依赖这些方法来使所有对象过期的代码应明确使用Session.begin()
或Session.expire_all()
来适应其用例。 - 当
Session
创建时,或在Session.rollback()
或Session.commit()
完成后,不会立即触发SessionEvents.after_transaction_create()
事件钩子。 Session.close()
方法也不意味着隐式开始新的SessionTransaction
。
另请参阅
自动开始
理由
Session
对象的默认行为autocommit=False
在历史上意味着始终存在一个与Session
相关联的SessionTransaction
对象,通过Session.transaction
属性关联。当给定的SessionTransaction
完成时,由于提交、回滚或关闭,它会立即被新的替代。SessionTransaction
本身并不意味着使用任何连接相关资源,因此这种长期存在的行为具有特定的优雅之处,即Session.transaction
的状态始终可预测为非 None。
然而,作为#5056倡议的一部分,大大减少引用循环,这一假设意味着调用Session.close()
会导致一个仍然存在引用循环且更昂贵的Session
对象需要清理,更不用说构造SessionTransaction
对象的一点开销,这意味着对于例如调用Session.commit()
然后Session.close()
的Session
会创建不必要的开销。
因此,决定Session.close()
应该将self.transaction
的内部状态,现在在内部称为self._transaction
,保留为 None,并且只有在需要时才会创建新的SessionTransaction
。为了一致性和代码覆盖率,此行为还扩展到了所有“自动开始”预期的点,而不仅仅是在调用Session.close()
时。
特别是,这对订阅SessionEvents.after_transaction_create()
事件钩子的应用程序造成了行为上的变化;以前,当首次构建Session
时,此事件将被触发,以及对关闭先前事务的大多数操作,并将触发SessionEvents.after_transaction_end()
。新行为是,当Session
尚未创建新的SessionTransaction
对象并且映射对象通过Session.add()
和Session.delete()
等方法与Session
关联时,当调用Session.transaction
属性时,当Session.flush()
方法有任务要完成时等,SessionEvents.after_transaction_create()
会按需触发。
此外,依赖于Session.commit()
或Session.rollback()
方法无条件使所有对象过期的代码将不再能够这样做。当没有发生任何更改时需要使所有对象过期的代码应该针对此情况调用Session.expire_all()
。
除了更改SessionEvents.after_transaction_create()
事件发出的时间以及Session.commit()
或Session.rollback()
的无操作性质外,此更改不应对Session
对象的行为产生其他用户可见的影响;Session
在调用Session.close()
后仍然保持可用于新操作的行为,并且Session
与Engine
和数据库本身的交互顺序也应保持不受影响,因为这些操作已经以按需方式运行。
#5074 ### 只读视图关系不同步回引
在 1.3.14 中的#5149中,SQLAlchemy 开始在目标关系上同时使用relationship.backref
或relationship.back_populates
关键字时发出警告,同时使用relationship.viewonly
标志。这是因为“只读”关系实际上不会保存对其所做的更改,这可能导致一些误导行为发生。然而,在#5237中,我们试图优化这种行为,因为在只读关系上设置回引是有合法用例的,包括回填属性有时由关系懒加载器用于确定在另一个方向上不需要额外的急加载,以及回填可以用于映射器内省和backref()
可以是设置双向关系的便捷方式。
当时的解决方案是使从反向引用发生的“变化”成为可选的事情,使用 relationship.sync_backref
标志。在 1.4 版本中,对于也设置了 relationship.viewonly
的关系目标,默认情况下 relationship.sync_backref
的值为 False。这表明对于具有 viewonly 的关系所做的任何更改都不会影响另一侧或 Session
的状态:
class User(Base): # ... addresses = relationship(Address, backref=backref("user", viewonly=True)) class Address(Base): ... u1 = session.query(User).filter_by(name="x").first() a1 = Address() a1.user = u1
上面,a1
对象将不会被添加到 u1.addresses
集合中,也不会将 a1
对象添加到会话中。之前,这两件事情都是正确的。当 relationship.viewonly
为 False
时,不再发出应将 relationship.sync_backref
设置为 False
的警告,因为这现在是默认行为。
#5237 ### 在 2.0 版本中将删除的 cascade_backrefs 行为
SQLAlchemy 长期以来一直有一个根据反向引用分配将对象级联到 Session
的行为。假设 User
已经在 Session
中,将其分配给 Address
对象的 Address.user
属性,假设已经建立了双向关系,这意味着此时 Address
也会被放入 Session
中:
u1 = User() session.add(u1) a1 = Address() a1.user = u1 # <--- adds "a1" to the Session
上述行为是反向引用行为的意外副作用,因为由于 a1.user
暗示着 u1.addresses.append(a1)
,所以 a1
将被级联到 Session
中。这仍然是整个 1.4 版本的默认行为。在某些时候,新增了一个标志 relationship.cascade_backrefs
来禁用上述行为,以及 backref.cascade_backrefs
来设置此标志,当关系由 relationship.backref
指定时,因为这可能会令人惊讶,并且还会妨碍某些操作,其中对象会过早地放入 Session
中并过早地刷新。
在 2.0 版本中,默认行为将是“cascade_backrefs”为 False,另外也不会有“True”行为,因为这通常不是一种理想的行为。当启用 2.0 版本的弃用警告时,当“反向引用级联”实际发生时,将发出警告。要获取新行为,可以在任何目标关系上将 relationship.cascade_backrefs
和 backref.cascade_backrefs
设置为 False
,就像在 1.3 版本和更早版本中已经支持的那样,或者可以使用 Session.future
标志进入 2.0 样式 模式:
Session = sessionmaker(engine, future=True) with Session() as session: u1 = User() session.add(u1) a1 = Address() a1.user = u1 # <--- will not add "a1" to the Session
#5150 ### 急切加载器在取消过期操作期间发出
长期以来,人们一直期待的行为是,当访问过期对象时,配置的急切加载器将运行,以便在刷新或其他情况下未过期时急切加载过期对象上的关系。现已添加了此行为,以便 joinedloaders 将像往常一样添加内联 JOIN,并且当未过期对象被刷新或对象被刷新时,selectin/subquery 加载器将为给定关系运行“immediateload”操作:
>>> a1 = session.query(A).options(joinedload(A.bs)).first() >>> a1.data = "new data" >>> session.commit()
上述 A
对象是使用与其关联的 joinedload()
选项加载的,以便急切加载 bs
集合。在 session.commit()
后,对象的状态已过期。在访问 .data
列属性时,对象将被刷新,现在这将包括 joinedload 操作:
>>> a1.data SELECT a.id AS a_id, a.data AS a_data, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id FROM a LEFT OUTER JOIN b AS b_1 ON a.id = b_1.a_id WHERE a.id = ?
该行为适用于直接应用于relationship()
的加载策略,以及与Query.options()
一起使用的选项,前提是对象最初是由该查询加载的。
对于“次要”急加载器“selectinload”和“subqueryload”,这些加载器的 SQL 策略在单个对象上急加载属性时并不是必要的;因此,在刷新场景中,它们将调用“immediateload”策略,类似于“lazyload”发出的查询,作为额外的查询:
>>> a1 = session.query(A).options(selectinload(A.bs)).first() >>> a1.data = "new data" >>> session.commit() >>> a1.data SELECT a.id AS a_id, a.data AS a_data FROM a WHERE a.id = ? (1,) SELECT b.id AS b_id, b.a_id AS b_a_id FROM b WHERE ? = b.a_id (1,)
请注意,加载器选项不适用于以不同方式引入到Session
中的对象。也就是说,如果a1
对象只是在此Session
中持久化,或者在应用急加载选项之前使用不同的查询加载了该对象,则该对象不具有与之关联的急加载选项。这并不是一个新概念,但是寻找刷新行为上的急加载的用户可能会发现这更加明显。
#1763 ### 列加载器如deferred()
、with_expression()
仅在外部、完整实体查询中指示时才生效
注意
本更改说明在本文档的早期版本中不存在,但对于所有 SQLAlchemy 1.4 版本都是相关的。
1.3 版本和之前版本从未支持的行为,但仍然会产生特定效果,即重新利用列加载器选项,如defer()
和with_expression()
在子查询中,以控制每个子查询的列子句中的 SQL 表达式。一个典型的例子是构造 UNION 查询,例如:
q1 = session.query(User).options(with_expression(User.expr, literal("u1"))) q2 = session.query(User).options(with_expression(User.expr, literal("u2"))) q1.union_all(q2).all()
在 1.3 版本中,with_expression()
选项会对 UNION 的每个元素生效,例如:
SELECT anon_1.anon_2 AS anon_1_anon_2, anon_1.user_account_id AS anon_1_user_account_id, anon_1.user_account_name AS anon_1_user_account_name FROM ( SELECT ? AS anon_2, user_account.id AS user_account_id, user_account.name AS user_account_name FROM user_account UNION ALL SELECT ? AS anon_3, user_account.id AS user_account_id, user_account.name AS user_account_name FROM user_account ) AS anon_1 ('u1', 'u2')
SQLAlchemy 1.4 对加载器选项的概念变得更加严格,因此仅应用于查询的最外层部分,即用于填充实际要返回的 ORM 实体的 SELECT;在 1.4 中,上述查询将产生:
SELECT ? AS anon_1, anon_2.user_account_id AS anon_2_user_account_id, anon_2.user_account_name AS anon_2_user_account_name FROM ( SELECT user_account.id AS user_account_id, user_account.name AS user_account_name FROM user_account UNION ALL SELECT user_account.id AS user_account_id, user_account.name AS user_account_name FROM user_account ) AS anon_2 ('u1',)
即,Query
的选项是从 UNION 的第一个元素中获取的,因为所有加载器选项只能在最顶层。第二个查询的选项被忽略了。
理由
这种行为现在更加接近于其他种类的加载器选项,例如所有 SQLAlchemy 版本中的关系加载器选项,如joinedload()
,其中在 UNION 情况下已经复制到查询的最顶层,并且仅从 UNION 的第一个元素中获取,舍弃查询的其他部分的任何选项。
这种隐式复制和选择性忽略选项的行为,如上所示,是相当任意的,是Query
的遗留行为,也是Query
以及其应用Query.union_all()
方式存在不足的一个特定示例,因为如何将单个 SELECT 转换为其自身和另一个查询的 UNION 以及如何应用加载器选项到新语句上都是模糊的。
对于使用defer()
的更常见情况,可以演示 SQLAlchemy 1.4 的行为通常优于 1.3。以下查询:
q1 = session.query(User).options(defer(User.name)) q2 = session.query(User).options(defer(User.name)) q1.union_all(q2).all()
在 1.3 中,会笨拙地向内部查询添加 NULL,然后选择它:
SELECT anon_1.anon_2 AS anon_1_anon_2, anon_1.user_account_id AS anon_1_user_account_id FROM ( SELECT NULL AS anon_2, user_account.id AS user_account_id FROM user_account UNION ALL SELECT NULL AS anon_2, user_account.id AS user_account_id FROM user_account ) AS anon_1
如果所有查询都没有设置相同的选项,上述场景将由于无法形成正确的 UNION 而引发错误。
而在 1.4 中,该选项仅应用于顶层,省略了对User.name
的提取,并且避免了这种复杂性:
SELECT anon_1.user_account_id AS anon_1_user_account_id FROM ( SELECT user_account.id AS user_account_id, user_account.name AS user_account_name FROM user_account UNION ALL SELECT user_account.id AS user_account_id, user_account.name AS user_account_name FROM user_account ) AS anon_1
正确的方法
使用 2.0-style 查询时,目前不会发出警告,但是嵌套的with_expression()
选项一致被忽略,因为它们不适用于正在加载的实体,并且不会被隐式复制到任何地方。以下查询不会为with_expression()
调用产生输出:
s1 = select(User).options(with_expression(User.expr, literal("u1"))) s2 = select(User).options(with_expression(User.expr, literal("u2"))) stmt = union_all(s1, s2) session.scalars(select(User).from_statement(stmt)).all()
产生 SQL:
SELECT user_account.id, user_account.name FROM user_account UNION ALL SELECT user_account.id, user_account.name FROM user_account
要将with_expression()
正确应用于User
实体,应将其应用于查询的最外层级,使用每个 SELECT 的列子句中的普通 SQL 表达式:
s1 = select(User, literal("u1").label("some_literal")) s2 = select(User, literal("u2").label("some_literal")) stmt = union_all(s1, s2) session.scalars( select(User) .from_statement(stmt) .options(with_expression(User.expr, stmt.selected_columns.some_literal)) ).all()
这将产生预期的 SQL:
SELECT user_account.id, user_account.name, ? AS some_literal FROM user_account UNION ALL SELECT user_account.id, user_account.name, ? AS some_literal FROM user_account
User
对象本身将在其内容中包含此表达式,在User.expr
下方。
SQLAlchemy 一直以来的行为是,在新创建的对象上访问映射属性会返回一个隐式生成的值,而不是引发AttributeError
,例如标量属性为None
或列表关系为[]
:
>>> u1 = User() >>> u1.name None >>> u1.addresses []
上述行为的理由最初是为了使 ORM 对象更易于使用。由于 ORM 对象在首次创建时代表一个空行而没有任何状态,因此直观地认为其未访问的属性会解析为标量的None
(或 SQL NULL),对于关系则解析为空集合。特别是,这使得一种极其常见的模式成为可能,即能够在不手动创建和分配空集合的情况下改变新集合:
>>> u1 = User() >>> u1.addresses.append(Address()) # no need to assign u1.addresses = []
直到 SQLAlchemy 的 1.0 版本,对于标量属性以及集合的初始化系统的行为是,None
或空集合会被填充到对象的状态中,例如__dict__
。这意味着以下两个操作是等效的:
>>> u1 = User() >>> u1.name = None # explicit assignment >>> u2 = User() >>> u2.name # implicit assignment just by accessing it None
在上述情况下,u1
和u2
都会在name
属性的值中填充None
。由于这是一个 SQL NULL,ORM 会跳过将这些值包含在 INSERT 中,以便 SQL 级别的默认值生效,如果有的话,否则该值默认为数据库端的 NULL。
在 1.0 版本中作为关于没有预先存在值的属性的属性事件和其他操作的更改的一部分,这种行为被改进,以便None
值不再填充到__dict__
中,只是返回。除了消除 getter 操作的变异副作用外,这种改变还使得可以通过实际分配None
来将具有服务器默认值的列设置为 NULL 值,这现在与仅仅读取它有所区别。
然而,这种变化并没有考虑到集合,其中返回一个未分配的空集合意味着这个可变集合每次都会不同,并且也无法正确地适应变异操作(例如追加,添加等)调用它。虽然这种行为通常不会妨碍任何人,但最终在#4519中识别出了一个边缘情况,其中这个空集合可能是有害的,即当对象合并到会话中时:
>>> u1 = User(id=1) # create an empty User to merge with id=1 in the database >>> merged1 = session.merge( ... u1 ... ) # value of merged1.addresses is unchanged from that of the DB >>> u2 = User(id=2) # create an empty User to merge with id=2 in the database >>> u2.addresses [] >>> merged2 = session.merge(u2) # value of merged2.addresses has been emptied in the DB
在上述情况下,merged1
上的.addresses
集合将包含已经在数据库中的所有Address()
对象。merged2
不会;因为它有一个隐式分配的空列表,.addresses
集合将被擦除。这是一个例子,说明这种变异副作用实际上可以改变数据库本身。
虽然考虑过属性系统是否应开始使用严格的“纯 Python”行为,在所有情况下为非存在属性的非持久对象引发AttributeError
,并要求所有集合都必须显式分配,但这样的更改可能对多年来依赖此行为的大量应用程序来说过于极端,导致复杂的发布/向后兼容性问题,以及恢复旧行为的解决方法可能会变得普遍,从而使整个更改在任何情况下都变得无效。
然后,更改是保持默认的生成行为,但最终使标量的非变异行为对集合也成为现实,通过在集合系统中添加额外的机制。访问空属性时,将创建新集合并与状态关联,但直到实际发生变异才添加到__dict__
中:
>>> u1 = User() >>> l1 = u1.addresses # new list is created, associated with the state >>> assert u1.addresses is l1 # you get the same list each time you access it >>> assert ( ... "addresses" not in u1.__dict__ ... ) # but it won't go into __dict__ until it's mutated >>> from sqlalchemy import inspect >>> inspect(u1).attrs.addresses.history History(added=None, unchanged=None, deleted=None)
当列表发生更改时,它将成为要持久化到数据库的已跟踪更改的一部分:
>>> l1.append(Address()) >>> assert "addresses" in u1.__dict__ >>> inspect(u1).attrs.addresses.history History(added=[<__main__.Address object at 0x7f49b725eda0>], unchanged=[], deleted=[])
预计这种更改几乎不会对现有应用程序产生任何影响,除了观察到一些应用程序可能依赖于对该集合的隐式分配,例如根据其__dict__
断定对象包含某些值:
>>> u1 = User() >>> u1.addresses [] # this will now fail, would pass before >>> assert {k: v for k, v in u1.__dict__.items() if not k.startswith("_")} == { ... "addresses": [] ... }
或者确保集合不需要进行延迟加载以继续进行,下面(尽管有些笨拙)的代码现在也会失败:
>>> u1 = User() >>> u1.addresses [] >>> s.add(u1) >>> s.flush() >>> s.close() >>> u1.addresses # <-- will fail, .addresses is not loaded and object is detached
依赖于集合的隐式变异行为的应用程序将需要更改,以便显式分配所需的集合:
>>> u1.addresses = []
#4519 ### “新实例与现有标识冲突”错误现在是一个警告
SQLAlchemy 一直具有逻辑来检测要插入的Session
中的对象是否具有与已存在对象相同的主键:
class Product(Base): __tablename__ = "product" id = Column(Integer, primary_key=True) session = Session(engine) # add Product with primary key 1 session.add(Product(id=1)) session.flush() # add another Product with same primary key session.add(Product(id=1)) s.commit() # <-- will raise FlushError
更改是将FlushError
更改为仅作为警告:
sqlalchemy/orm/persistence.py:408: SAWarning: New instance <Product at 0x7f1ff65e0ba8> with identity key (<class '__main__.Product'>, (1,), None) conflicts with persistent instance <Product at 0x7f1ff60a4550>
在此之后,该条件将尝试将行插入数据库,这将引发IntegrityError
,这是与Session
中尚不存在主键标识时引发的相同错误:
sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: product.id
为了允许使用IntegrityError
的代码捕获重复项而无需考虑Session
的现有状态,通常使用保存点来实现:
# add another Product with same primary key try: with session.begin_nested(): session.add(Product(id=1)) except exc.IntegrityError: print("row already exists")
以前的逻辑并不完全可行,因为如果具有现有标识的Product
对象已经在Session
中,代码还必须捕获FlushError
,此外,该错误也没有针对完整性问题的特定条件进行过滤。通过更改,上述块的行为与发出警告的异常一致。
由于涉及主键的逻辑,所有数据库在插入时发生主键冲突时都会发出完整性错误。不会引发错误的情况是极为罕见的,即定义了一个在映射的可选择项上定义了一个比实际配置的数据库模式更严格的主键的映射,例如在映射到表的连接或在定义附加列作为复合主键的一部分时,这些列实际上在数据库模式中没有约束。然而,这些情况也更一致地工作,即使现有标识仍然存在于数据库中,插入理论上也会继续进行。警告也可以使用 Python 警告过滤器配置为引发异常。
#4662 ### 持久性相关级联操作不允许与 viewonly=True
当使用relationship.viewonly
标志将relationship()
设置为viewonly=True
时,表示此关系仅用于从数据库加载数据,不应进行变异或参与持久化操作。为了确保此约定成功运行,关系不能再指定在“viewonly”方面毫无意义的relationship.cascade
设置。
这里的主要目标是“delete, delete-orphan”级联,即使viewonly
为 True,通过 1.3 仍会影响持久性,这是一个错误;即使viewonly
为 True,如果删除父对象或分离对象,对象仍会级联这两个操作到相关对象。而不是修改级联操作以检查viewonly
,这两者的配置被简单地禁止在一起:
class User(Base): # ... # this is now an error addresses = relationship("Address", viewonly=True, cascade="all, delete-orphan")
上述将引发:
sqlalchemy.exc.ArgumentError: Cascade settings "delete, delete-orphan, merge, save-update" apply to persistence operations and should not be combined with a viewonly=True relationship.
从 SQLAlchemy 1.3.12 开始,存在此问题的应用程序应发出警告,对于上述错误,解决方案是删除视图关系的级联设置。
#4993 #4994 ### 使用自定义查询查询继承映射时更严格的行为
此更改适用于查询已完成的 SELECT 子查询以选择的连接或单表继承子类实体的情况。如果给定的子查询返回的行不对应于请求的多态标识或标识,则会引发错误。在以前的情况下,这种条件在连接表继承下会悄悄通过,返回一个无效的子类,在单表继承下,Query
会添加额外的条件来限制结果,这可能会不当地干扰查询的意图。
给定Employee
,Engineer(Employee)
,Manager(Employee)
的映射示例,在 1.3 系列中,如果我们针对连接继承映射发出以下查询:
s = Session(e) s.add_all([Engineer(), Manager()]) s.commit() print(s.query(Manager).select_entity_from(s.query(Employee).subquery()).all())
子查询选择了Engineer
和Manager
行,即使外部查询针对Manager
,我们也会得到一个非Manager
对象:
SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id FROM (SELECT employee.type AS type, employee.id AS id FROM employee) AS anon_1 2020-01-29 18:04:13,524 INFO sqlalchemy.engine.base.Engine () [<__main__.Engineer object at 0x7f7f5b9a9810>, <__main__.Manager object at 0x7f7f5b9a9750>]
新的行为是这种情况会引发错误:
sqlalchemy.exc.InvalidRequestError: Row with identity key (<class '__main__.Employee'>, (1,), None) can't be loaded into an object; the polymorphic discriminator column '%(140205120401296 anon)s.type' refers to mapped class Engineer->engineer, which is not a sub-mapper of the requested mapped class Manager->manager
仅当该实体的主键列为非 NULL 时,才会引发上述错误。如果一行中没有给定实体的主键,则不会尝试构造实体。
在单继承映射的情况下,行为的变化稍微更加复杂;如果上面的Engineer
和Manager
被映射为单表继承,那么在 1.3 版本中将会发出以下查询,并且只返回一个Manager
对象:
SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id FROM (SELECT employee.type AS type, employee.id AS id FROM employee) AS anon_1 WHERE anon_1.type IN (?) 2020-01-29 18:08:32,975 INFO sqlalchemy.engine.base.Engine ('manager',) [<__main__.Manager object at 0x7ff1b0200d50>]
Query
向子查询添加了“单表继承”条件,对其最初设置的意图进行了评论。这种行为是在 1.0 版本中添加的#3891,在“连接”和“单”表继承之间创建了行为不一致,并且修改了给定查询的意图,可能意图返回额外的行,其中对应于继承实体的列为 NULL,这是一个有效的用例。现在的行为等同于连接表继承的行为,假定子查询返回正确的行,如果遇到意外的多态标识,则会引发错误:
SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id FROM (SELECT employee.type AS type, employee.id AS id FROM employee) AS anon_1 2020-01-29 18:13:10,554 INFO sqlalchemy.engine.base.Engine () Traceback (most recent call last): # ... sqlalchemy.exc.InvalidRequestError: Row with identity key (<class '__main__.Employee'>, (1,), None) can't be loaded into an object; the polymorphic discriminator column '%(140700085268432 anon)s.type' refers to mapped class Engineer->employee, which is not a sub-mapper of the requested mapped class Manager->employee
对于上述情况的正确调整是在 1.3 上运行的,调整给定的子查询以根据鉴别器列正确过滤行:
print( s.query(Manager) .select_entity_from( s.query(Employee).filter(Employee.discriminator == "manager").subquery() ) .all() )
SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id FROM (SELECT employee.type AS type, employee.id AS id FROM employee WHERE employee.type = ?) AS anon_1 2020-01-29 18:14:49,770 INFO sqlalchemy.engine.base.Engine ('manager',) [<__main__.Manager object at 0x7f70e13fca90>]
#5122 ### 查询返回的“KeyedTuple”对象被“Row”替换
如在 RowProxy 不再是“代理”;现在被称为 Row,并且表现得像一个增强的命名元组中所讨论的,Core RowProxy
对象现在被一个名为Row
的类所取代。基本的Row
对象现在更像一个命名元组,因此它现在被用作由Query
对象返回的类似元组的结果的基础,而不是以前的“KeyedTuple”类。
这样做的理由是到 SQLAlchemy 2.0,Core 和 ORM SELECT 语句将使用行为类似命名元组的相同Row
对象返回结果行。通过Row
的Row._mapping
属性可以获得类似字典的功能。在此期间,Core 结果集将使用一个Row
子类 LegacyRow
,它保持了以前的字典/元组混合行为以实现向后兼容性,而Row
类将直接用于由Query
对象返回的 ORM 元组结果。
已经努力使Row
的大部分功能集在 ORM 中可用,这意味着可以通过字符串名称以及实体/列进行访问:
row = s.query(User, Address).join(User.addresses).first() row._mapping[User] # same as row[0] row._mapping[Address] # same as row[1] row._mapping["User"] # same as row[0] row._mapping["Address"] # same as row[1] u1 = aliased(User) row = s.query(u1).only_return_tuples(True).first() row._mapping[u1] # same as row[0] row = s.query(User.id, Address.email_address).join(User.addresses).first() row._mapping[User.id] # same as row[0] row._mapping["id"] # same as row[0] row._mapping[users.c.id] # same as row[0]
另请参阅
RowProxy 不再是“代理”;现在被称为 Row,并且表现得像一个增强的命名元组
Session 新的“autobegin”行为特性
以前,在其默认模式autocommit=False
下,Session
会在构造时立即内部开始一个SessionTransaction
对象,并且在每次调用Session.rollback()
或Session.commit()
后还会创建一个新的对象。
新的行为是,只有在调用诸如Session.add()
或Session.execute()
等方法时,才会按需创建此SessionTransaction
对象。然而,现在也可以显式调用Session.begin()
来开始事务,即使在autocommit=False
模式下,从而与未来风格的_base.Connection
的行为相匹配。
这些变化所指示的行为变化是:
- 现在,
Session
可以处于未开始任何事务的状态,即使在autocommit=False
模式下。以前,这种状态仅在“自动提交”模式下可用。 - 在这种状态下,
Session.commit()
和Session.rollback()
方法是无操作的。依赖这些方法使所有对象过期的代码应明确使用Session.begin()
或Session.expire_all()
来适应其用例。 - 当
Session
被创建时,或在Session.rollback()
或Session.commit()
完成后,SessionEvents.after_transaction_create()
事件钩子不会立即触发。 Session.close()
方法也不意味着隐式开始新的SessionTransaction
。
另请参阅
自动开始
理由
Session
对象的默认行为autocommit=False
历史上意味着始终有一个与Session
相关联的SessionTransaction
对象,通过Session.transaction
属性关联。当给定的SessionTransaction
完成时,由于提交、回滚或关闭,它会立即被新的替换。SessionTransaction
本身并不意味着使用任何连接相关资源,因此这种长期存在的行为具有特定的优雅之处,即Session.transaction
的状态始终可预测为非 None。
但是,作为减少引用循环的倡议的一部分#5056,这意味着调用Session.close()
会导致一个仍然存在引用循环且更昂贵的Session
对象,清理起来更加费力,更不用说构造SessionTransaction
对象时会有一些额外开销,这意味着对于一个例如调用了Session.commit()
然后又调用Session.close()
的Session
会产生不必要的开销。
因此,决定Session.close()
应该将self.transaction
的内部状态,现在在内部称为self._transaction
,保留为 None,并且只有在需要时才创建新的SessionTransaction
。为了保持一致性和代码覆盖率,这种行为也扩展到了所有“autobegin”预期的点,而不仅仅是调用Session.close()
时。
特别是,这对订阅SessionEvents.after_transaction_create()
事件钩子的应用程序造成了行为上的改变;以前,当首次构建Session
时,此事件将被触发,以及对关闭先前事务的大多数操作,并将触发SessionEvents.after_transaction_end()
。新行为是,当Session
尚未创建新的SessionTransaction
对象并且映射对象通过Session.add()
和Session.delete()
等方法与Session
关联时,当调用Session.transaction
属性时,当Session.flush()
方法有任务要完成时等,SessionEvents.after_transaction_create()
会按需触发。
此外,依赖于Session.commit()
或Session.rollback()
方法无条件使所有对象过期的代码将不再能够这样做。当没有发生任何更改时需要使所有对象过期的代码应该在这种情况下调用Session.expire_all()
。
除了SessionEvents.after_transaction_create()
事件发出的时间变化以及Session.commit()
或Session.rollback()
的无操作性质之外,这种变化对Session
对象的行为应该没有其他用户可见的影响;Session
在调用Session.close()
后仍然保持可用于新操作的行为,并且Session
与Engine
以及数据库本身的交互顺序也应该保持不受影响,因为这些操作已经以按需方式运行。
理由
Session
对象的默认行为autocommit=False
在历史上意味着始终存在一个与Session
相关联的SessionTransaction
对象,通过Session.transaction
属性关联。当给定的SessionTransaction
完成时,由于提交、回滚或关闭,它会立即被新的替代。SessionTransaction
本身并不意味着使用任何连接相关资源,因此这种长期存在的行为具有特定的优雅之处,即Session.transaction
的状态始终可预测为非 None。
但是,作为在#5056中极大减少引用循环的倡议的一部分,这意味着调用Session.close()
会导致一个仍然存在引用循环且更昂贵清理的Session
对象,更不用说构造SessionTransaction
对象时会有一些小的开销,这意味着对于一个例如调用了Session.commit()
然后调用Session.close()
的Session
会产生不必要的开销。
因此,决定让Session.close()
将self.transaction
的内部状态,现在在内部称为self._transaction
,保留为 None,并且只在需要时创建一个新的SessionTransaction
。为了一致性和代码覆盖率,这种行为也扩展到所有“自动开始”预期的点,不仅仅是在调用Session.close()
时。
特别是,这会对订阅 SessionEvents.after_transaction_create()
事件钩子的应用程序造成行为上的改变;以前,此事件将在首次构造 Session
时被触发,以及对关闭上一个事务的大多数动作进行操作,并会发出 SessionEvents.after_transaction_end()
。新行为是,当 Session
尚未创建新的 SessionTransaction
对象,并且映射对象通过 Session.add()
和 Session.delete()
等方法与 Session
关联时,以及调用 Session.transaction
属性时,当 Session.flush()
方法有任务需要完成等情况下,将按需触发 SessionEvents.after_transaction_create()
。
此外,依赖于Session.commit()
或 Session.rollback()
方法来无条件使所有对象过期的代码将不再能够这样做。当没有发生变化时需要使所有对象过期的代码应该调用Session.expire_all()
。
除了SessionEvents.after_transaction_create()
事件的触发时间发生变化以及Session.commit()
或Session.rollback()
的无操作性质外,这一变化不应对Session
对象的行为产生其他用户可见的影响;Session
在调用Session.close()
后仍然保持可用于新操作的行为,并且Session
与Engine
以及数据库本身的交互顺序也应保持不受影响,因为这些操作已经以按需方式运行。
只读视图关系不同步反向引用
在 1.3.14 中的#5149中,SQLAlchemy 开始在目标关系上同时使用relationship.backref
或relationship.back_populates
关键字时发出警告,与relationship.viewonly
标志一起使用。这是因为“只读”关系实际上不会持久保存对其所做的更改,这可能导致一些误导性行为发生。然而,在#5237中,我们试图优化这种行为,因为在只读关系上设置反向引用是有合法用例的,包括反向填充属性有时被关系懒加载器用来确定在另一个方向上不需要额外的急加载,以及反向填充可以用于映射器内省,backref()
也可以是设置双向关系的便捷方式。
那时的解决方案是使从反向引用发生的“变化”成为可选的事情,使用relationship.sync_backref
标志。在 1.4 中,对于还设置了relationship.viewonly
的关系目标,默认情况下relationship.sync_backref
的值为 False。这表示对于具有 viewonly 的关系所做的任何更改都不会影响另一侧或Session
的状态:
class User(Base): # ... addresses = relationship(Address, backref=backref("user", viewonly=True)) class Address(Base): ... u1 = session.query(User).filter_by(name="x").first() a1 = Address() a1.user = u1
上面,a1
对象不会被添加到u1.addresses
集合中,也不会将a1
对象添加到会话中。以前,这两件事情都是正确的。当relationship.viewonly
为False
时,不再发出警告,即relationship.sync_backref
应设置为False
,因为这现在是默认行为。
在 2.0 版本中将删除 cascade_backrefs 行为
SQLAlchemy 长期以来一直有一个行为,根据反向引用赋值将对象级联到Session
中。给定下面的User
已经在Session
中,将其分配给Address
对象的Address.user
属性,假设已建立双向关系,这意味着在那一点上Address
也会被放入Session
中:
u1 = User() session.add(u1) a1 = Address() a1.user = u1 # <--- adds "a1" to the Session
上述行为是反向引用行为的一个意外副作用,因为a1.user
意味着u1.addresses.append(a1)
,a1
会被级联到Session
中。这在 1.4 版本中仍然是默认行为。在某个时候,添加了一个新标志relationship.cascade_backrefs
来禁用上述行为,以及backref.cascade_backrefs
来在通过relationship.backref
指定关系时设置此行为,因为这可能会令人惊讶,也会妨碍一些操作,其中对象会过早地放置在Session
中并提前刷新。
在 2.0 版本中,默认行为将是“cascade_backrefs”为 False,并且另外不会有“True”行为,因为这通常不是一种理想的行为。当启用 2.0 版本的弃用警告时,当“backref cascade”实际发生时将发出警告。要获得新行为,可以在任何目标关系上将relationship.cascade_backrefs
和backref.cascade_backrefs
设置为False
,就像在 1.3 版本和更早版本中已经支持的那样,或者使用Session.future
标志进入 2.0 风格模式:
Session = sessionmaker(engine, future=True) with Session() as session: u1 = User() session.add(u1) a1 = Address() a1.user = u1 # <--- will not add "a1" to the Session
在取消过期操作期间急切加载器发出
长期以来一直寻求的行为是,当访问一个过期对象时,配置的急切加载器将运行,以便在对象被刷新或其他情况下取消过期时急切加载过期对象上的关系。现在已经添加了这种行为,因此 joinedloaders 将像往常一样添加内联 JOIN,而 selectin/subquery loaders 将在过期对象被取消过期或对象被刷新时运行“immediateload”操作:
>>> a1 = session.query(A).options(joinedload(A.bs)).first() >>> a1.data = "new data" >>> session.commit()
在上面的例子中,A
对象使用了joinedload()
选项加载,以便急切加载bs
集合。在session.commit()
后,对象的状态会过期。访问.data
列属性时,对象会被刷新,现在这将包括 joinedload 操作:
>>> a1.data SELECT a.id AS a_id, a.data AS a_data, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id FROM a LEFT OUTER JOIN b AS b_1 ON a.id = b_1.a_id WHERE a.id = ?
该行为适用于直接应用于relationship()
的加载策略,以及与Query.options()
一起使用的选项,前提是对象最初是由该查询��载的。
对于“secondary”急加载器“selectinload”和“subqueryload”,这些加载器的 SQL 策略并不是必要的,以便在单个对象上急加载属性;因此它们将在刷新场景中调用“immediateload”策略,这类似于“lazyload”发出的查询,作为额外的查询:
>>> a1 = session.query(A).options(selectinload(A.bs)).first() >>> a1.data = "new data" >>> session.commit() >>> a1.data SELECT a.id AS a_id, a.data AS a_data FROM a WHERE a.id = ? (1,) SELECT b.id AS b_id, b.a_id AS b_a_id FROM b WHERE ? = b.a_id (1,)
请注意,加载器选项不适用于以不同方式引入到Session
中的对象。也就是说,如果a1
对象只是在这个Session
中被持久化,或者在应用急加载选项之前用不同的查询加载了该对象,那么该对象就没有与之关联的急加载选项。这并不是一个新概念,但是寻找刷新行为上的急加载的用户可能会发现这更加明显。
列加载器如deferred()
、with_expression()
只在最外层、完整的实体查询中指定时才生效
注意
这个变更说明在此文档的早期版本中并不存在,但对于所有 SQLAlchemy 1.4 版本都是相关的。
一个在 1.3 版本和之前版本中从未支持过的行为,但仍然会产生特定效果的是重新利用列加载器选项,比如defer()
和with_expression()
在子查询中,以控制哪些 SQL 表达式将出现在每个子查询的列子句中。一个典型的例子是构造 UNION 查询,例如:
q1 = session.query(User).options(with_expression(User.expr, literal("u1"))) q2 = session.query(User).options(with_expression(User.expr, literal("u2"))) q1.union_all(q2).all()
在 1.3 版本中,with_expression()
选项会对 UNION 的每个元素生效,例如:
SELECT anon_1.anon_2 AS anon_1_anon_2, anon_1.user_account_id AS anon_1_user_account_id, anon_1.user_account_name AS anon_1_user_account_name FROM ( SELECT ? AS anon_2, user_account.id AS user_account_id, user_account.name AS user_account_name FROM user_account UNION ALL SELECT ? AS anon_3, user_account.id AS user_account_id, user_account.name AS user_account_name FROM user_account ) AS anon_1 ('u1', 'u2')
SQLAlchemy 1.4 对加载器选项的概念变得更加严格,因此仅应用于查询的最外层部分,即用于填充实际要返回的 ORM 实体的 SELECT;在 1.4 中上述查询将产生:
SELECT ? AS anon_1, anon_2.user_account_id AS anon_2_user_account_id, anon_2.user_account_name AS anon_2_user_account_name FROM ( SELECT user_account.id AS user_account_id, user_account.name AS user_account_name FROM user_account UNION ALL SELECT user_account.id AS user_account_id, user_account.name AS user_account_name FROM user_account ) AS anon_2 ('u1',)
也就是说,Query
的选项是从 UNION 的第一个元素中获取的,因为所有加载器选项只能在最顶层。第二个查询的选项被忽略。
理由
这种行为现在更接近于其他类型的加载器选项,比如在所有 SQLAlchemy 版本中,1.3 版本及更早版本中的关系加载器选项joinedload()
,在 UNION 情况下已经被复制到查询的最顶层,并且仅从 UNION 的第一个元素中获取,丢弃查询其他部分的任何选项。
上面演示的这种隐式复制和选择性忽略选项的行为,是一个仅在Query
中存在的遗留行为,也是一个特定的例子,说明了Query
及其应用Query.union_all()
的方式存在缺陷,因为如何将单个 SELECT 转换为自身和另一个查询的 UNION 以及如何应用加载器选项到该新语句是模棱两可的。
SQLAlchemy 1.4 的行为可以被证明在更常见的使用defer()
的情况下,比 1.3 更为优越。以下查询:
q1 = session.query(User).options(defer(User.name)) q2 = session.query(User).options(defer(User.name)) q1.union_all(q2).all()
在 1.3 版本中会尴尬地向内部查询中添加 NULL,然后再 SELECT 它:
SELECT anon_1.anon_2 AS anon_1_anon_2, anon_1.user_account_id AS anon_1_user_account_id FROM ( SELECT NULL AS anon_2, user_account.id AS user_account_id FROM user_account UNION ALL SELECT NULL AS anon_2, user_account.id AS user_account_id FROM user_account ) AS anon_1
如果所有查询没有设置相同的选项,上述情况将由于无法形成正确的 UNION 而引发错误。
而在 1.4 版本中,该选项仅应用于顶层,省略了对 User.name
的提取,避免了这种复杂性:
SELECT anon_1.user_account_id AS anon_1_user_account_id FROM ( SELECT user_account.id AS user_account_id, user_account.name AS user_account_name FROM user_account UNION ALL SELECT user_account.id AS user_account_id, user_account.name AS user_account_name FROM user_account ) AS anon_1
正确的方法
使用 2.0 风格查询,目前不会发出警告,但是嵌套的with_expression()
选项始终被忽略,因为它们不适用于正在加载的实体,并且不会被隐式复制到任何地方。下面的查询对with_expression()
调用不会产生任何输出:
s1 = select(User).options(with_expression(User.expr, literal("u1"))) s2 = select(User).options(with_expression(User.expr, literal("u2"))) stmt = union_all(s1, s2) session.scalars(select(User).from_statement(stmt)).all()
生成 SQL:
SELECT user_account.id, user_account.name FROM user_account UNION ALL SELECT user_account.id, user_account.name FROM user_account
要正确应用with_expression()
到 User
实体,应该将其应用于查询的最外层,使用普通的 SQL 表达式在每个 SELECT 的列子句中:
s1 = select(User, literal("u1").label("some_literal")) s2 = select(User, literal("u2").label("some_literal")) stmt = union_all(s1, s2) session.scalars( select(User) .from_statement(stmt) .options(with_expression(User.expr, stmt.selected_columns.some_literal)) ).all()
这将产生预期的 SQL:
SELECT user_account.id, user_account.name, ? AS some_literal FROM user_account UNION ALL SELECT user_account.id, user_account.name, ? AS some_literal FROM user_account
User
对象本身将在 User.expr
下的内容中包含此表达式。
理由
这种行为现在更接近于其他种类的加载器选项,比如关系加载器选项,比如joinedload()
在所有 SQLAlchemy 版本中,包括 1.3 和更早的版本,这在 UNION 情况下已经复制到查询的最顶层,并且只从 UNION 的第一个元素中获取,丢弃查询其他部分的任何选项。
上面演示的这种隐式复制和选择性忽略选项的行为是一种遗留行为,仅属于Query
的一部分,并且是一个特殊的例子,展示了Query
及其应用Query.union_all()
的方式存在缺陷,因为不清楚如何将单个 SELECT 转换为自身和另一个查询的 UNION,以及如何应用加载器选项到新语句。
对于更常见的情况,使用defer()
,演示了 SQLAlchemy 1.4 的行为通常优于 1.3。以下查询:
q1 = session.query(User).options(defer(User.name)) q2 = session.query(User).options(defer(User.name)) q1.union_all(q2).all()
在 1.3 中会尴尬地向内部查询添加 NULL,然后 SELECT 它:
SELECT anon_1.anon_2 AS anon_1_anon_2, anon_1.user_account_id AS anon_1_user_account_id FROM ( SELECT NULL AS anon_2, user_account.id AS user_account_id FROM user_account UNION ALL SELECT NULL AS anon_2, user_account.id AS user_account_id FROM user_account ) AS anon_1
如果所有查询没有设置相同的选项,上述情况将由于无法形成正确的 UNION 而引发错误。
而在 1.4 中,该选项仅应用于顶层,省略了对User.name
的提取,避免了这种复杂性:
SELECT anon_1.user_account_id AS anon_1_user_account_id FROM ( SELECT user_account.id AS user_account_id, user_account.name AS user_account_name FROM user_account UNION ALL SELECT user_account.id AS user_account_id, user_account.name AS user_account_name FROM user_account ) AS anon_1
正确的方法
使用 2.0 风格查询,目前不会发出警告,但是嵌套的with_expression()
选项始终被忽略,因为它们不适用于正在加载的实体,并且不会被隐式复制到任何地方。下面的查询对with_expression()
调用不会产生任何输出:
s1 = select(User).options(with_expression(User.expr, literal("u1"))) s2 = select(User).options(with_expression(User.expr, literal("u2"))) stmt = union_all(s1, s2) session.scalars(select(User).from_statement(stmt)).all()
生成 SQL:
SELECT user_account.id, user_account.name FROM user_account UNION ALL SELECT user_account.id, user_account.name FROM user_account
要正确应用with_expression()
到User
实体,应该应用到查询的最外层,使用普通的 SQL 表达式放在每个 SELECT 的 columns 子句中:
s1 = select(User, literal("u1").label("some_literal")) s2 = select(User, literal("u2").label("some_literal")) stmt = union_all(s1, s2) session.scalars( select(User) .from_statement(stmt) .options(with_expression(User.expr, stmt.selected_columns.some_literal)) ).all()
这将产生预期的 SQL:
SELECT user_account.id, user_account.name, ? AS some_literal FROM user_account UNION ALL SELECT user_account.id, user_account.name, ? AS some_literal FROM user_account
User
对象本身将包含这个表达式在它们的内容中User.expr
下面。
在瞬态对象上访问未初始化的集合属性不再改变 dict
对于新创建的对象访问映射属性始终返回隐式生成的值,而不是引发AttributeError
,例如标量属性返回None
或列表关系返回[]
:
>>> u1 = User() >>> u1.name None >>> u1.addresses []
以上行为的理由最初是为了使 ORM 对象更易于使用。由于 ORM 对象在首次创建时表示一个空行而没有任何状态,因此直观地,其未访问的属性应该解析为标量的None
(或 SQL NULL),并对关系解析为空集合。特别是,这使得一种极其常见的模式成为可能,即能够在不手动创建和分配空集合的情况下对新集合进行变异:
>>> u1 = User() >>> u1.addresses.append(Address()) # no need to assign u1.addresses = []
直到 SQLAlchemy 的 1.0 版本,这种初始化系统对标量属性以及集合的行为都是将None
或空集合填充到对象的状态中,例如__dict__
。这意味着以下两个操作是等效的:
>>> u1 = User() >>> u1.name = None # explicit assignment >>> u2 = User() >>> u2.name # implicit assignment just by accessing it None
在上述情况下,u1
和u2
的name
属性的值都将填充为None
。由于这是一个 SQL NULL,ORM 将跳过将这些值包含在 INSERT 中,以便发生 SQL 级别的默认值,如果有的话,否则值将在数据库端默认为 NULL。
在版本 1.0 中作为关于没有预先存在值的属性的属性事件和其他操作的更改的一部分,这种行为被调整,以便None
值不再填充到__dict__
中,只是返回。除了消除获取器操作的变异副作用外,这种变化还使得可以将具有服务器默认值的列设置为 NULL 值,方法是实际分配None
,这现在与仅仅读取它有所区别。
但是这种变化并没有考虑到集合,其中返回一个未分配的空集合意味着这个可变集合每次都会不同,也无法正确地适应对其进行的变异操作(例如追加、添加等)。虽然这种行为通常不会影响任何人,但最终在#4519中识别出了一个边缘情况,即当对象合并到会话中时,这个空集合可能会有害:
>>> u1 = User(id=1) # create an empty User to merge with id=1 in the database >>> merged1 = session.merge( ... u1 ... ) # value of merged1.addresses is unchanged from that of the DB >>> u2 = User(id=2) # create an empty User to merge with id=2 in the database >>> u2.addresses [] >>> merged2 = session.merge(u2) # value of merged2.addresses has been emptied in the DB
在上述情况下,merged1
上的.addresses
集合将包含数据库中已经存在的所有Address()
对象。merged2
不会;因为它有一个隐式分配的空列表,.addresses
集合将被擦除。这是一个实际上可以改变数据库本身的变异副作用的示例。
虽然考虑过属性系统是否应开始使用严格的“纯 Python”行为,在所有情况下对非存在属性的非持久对象引发AttributeError
,并要求所有集合都必须显式分配,但这样的改变可能对多年来依赖于这种行为的大量应用程序来说过于极端,导致复杂的发布/向后兼容性问题,以及恢复旧行为的解决方法可能会变得普遍,从而使整个改变失效。
改变的是保持默认的生成行为,但最终使标量的非变异行为对集合也成为现实,通过在集合系统中添加额外的机制。当访问空属性时,新集合将被创建并与状态关联,但直到实际发生变异才会被添加到__dict__
中:
>>> u1 = User() >>> l1 = u1.addresses # new list is created, associated with the state >>> assert u1.addresses is l1 # you get the same list each time you access it >>> assert ( ... "addresses" not in u1.__dict__ ... ) # but it won't go into __dict__ until it's mutated >>> from sqlalchemy import inspect >>> inspect(u1).attrs.addresses.history History(added=None, unchanged=None, deleted=None)
当列表发生变化时,它将成为要持久化到数据库的跟踪更改的一部分:
>>> l1.append(Address()) >>> assert "addresses" in u1.__dict__ >>> inspect(u1).attrs.addresses.history History(added=[<__main__.Address object at 0x7f49b725eda0>], unchanged=[], deleted=[])
这种改变预计对现有应用程序几乎没有任何影响,除了观察到一些应用程序可能依赖于对该集合的隐式赋值,例如根据其__dict__
来断定对象包含某些值:
>>> u1 = User() >>> u1.addresses [] # this will now fail, would pass before >>> assert {k: v for k, v in u1.__dict__.items() if not k.startswith("_")} == { ... "addresses": [] ... }
或确保集合不需要延迟加载才能继续,现在下面这段(尽管有些尴尬)代码也将失败:
>>> u1 = User() >>> u1.addresses [] >>> s.add(u1) >>> s.flush() >>> s.close() >>> u1.addresses # <-- will fail, .addresses is not loaded and object is detached
依赖于集合的隐式变异行为的应用程序需要更改,以便显式地分配所需的集合:
>>> u1.addresses = []
“新实例与现有标识冲突”错误现在是一个警告
SQLAlchemy 一直有逻辑来检测要插入Session
中的对象是否具有与已经存在的对象相同的主键:
class Product(Base): __tablename__ = "product" id = Column(Integer, primary_key=True) session = Session(engine) # add Product with primary key 1 session.add(Product(id=1)) session.flush() # add another Product with same primary key session.add(Product(id=1)) s.commit() # <-- will raise FlushError
改变是FlushError
被修改为仅作为警告:
sqlalchemy/orm/persistence.py:408: SAWarning: New instance <Product at 0x7f1ff65e0ba8> with identity key (<class '__main__.Product'>, (1,), None) conflicts with persistent instance <Product at 0x7f1ff60a4550>
随后,该条件将尝试将行插入数据库,这将引发IntegrityError
,这是如果主键标识在Session
中尚不存在时将引发的相同错误:
sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: product.id
其理念是允许使用IntegrityError
来捕获重复项的代码能够正常运行,而不受Session
的现有状态的影响,通常使用保存点来实现:
# add another Product with same primary key try: with session.begin_nested(): session.add(Product(id=1)) except exc.IntegrityError: print("row already exists")
上述逻辑在早期并不完全可行,因为在Session
中已经存在具有现有标识的Product
对象的情况下,代码还必须捕获FlushError
,而这种情况又没有针对完整性问题的特定条件进行过滤。通过这次更改,上述代码块的行为与警告也会被发出的例外情况一致。
由于涉及主键的逻辑会导致所有数据库在插入时出现主键冲突时发出完整性错误。不会引发错误的情况是极为罕见的,即在映射定义了比实际配置在数据库模式中更严格的主键的情况下,例如在映射到表的连接或在定义附加列作为复合主键的一部分时,这些列实际上在数据库模式中并没有约束。然而,这些情况也更一致地工作,即使现有标识仍然存在于数据库中,插入理论上也会继续进行。警告也可以通过 Python 警告过滤器配置为引发异常。
持久化相关级联操作在 viewonly=True 时不允许
当使用relationship.viewonly
标志将relationship()
设置为viewonly=True
时,表示此关系应仅用于从数据库加载数据,并且不应进行变异或参与持久化操作。为了确保此契约成功运行,关系不能再指定在“viewonly”方面毫无意义的relationship.cascade
设置。
这里的主要目标是“delete, delete-orphan”级联,即使 viewonly 为 True,通过 1.3 仍会影响持久性,这是一个错误;即使 viewonly 为 True,如果父对象被删除或对象被分离,对象仍会将这两个操作级联到相关对象。而不是修改级联操作以检查 viewonly,这两者的配置被简单地禁止在一起:
class User(Base): # ... # this is now an error addresses = relationship("Address", viewonly=True, cascade="all, delete-orphan")
上述将引发:
sqlalchemy.exc.ArgumentError: Cascade settings "delete, delete-orphan, merge, save-update" apply to persistence operations and should not be combined with a viewonly=True relationship.
作为 SQLAlchemy 1.3.12 的一部分,存在此问题的应用程序应该发出警告,对于上述错误,解决方案是删除视图关系的级联设置。
使用自定义查询查询继承映射时更严格的行为
此更改适用于查询已完成的 SELECT 子查询以选择的连接或单表继承子类实体的情况。如果给定的子查询返回与请求的多态标识或标识不对应的行,则会引发错误。以前,在连接表继承下,此条件会悄悄通过,返回一个无效的子类,并且在单表继承下,Query
会添加额外的条件来限制结果,这可能会不当地干扰查询的意图。
鉴于Employee
,Engineer(Employee)
,Manager(Employee)
的示例映射,在 1.3 系列中,如果我们针对连接继承映射发出以下查询:
s = Session(e) s.add_all([Engineer(), Manager()]) s.commit() print(s.query(Manager).select_entity_from(s.query(Employee).subquery()).all())
子查询选择了Engineer
和Manager
行,即使外部查询针对Manager
,我们也会得到一个非Manager
对象:
SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id FROM (SELECT employee.type AS type, employee.id AS id FROM employee) AS anon_1 2020-01-29 18:04:13,524 INFO sqlalchemy.engine.base.Engine () [<__main__.Engineer object at 0x7f7f5b9a9810>, <__main__.Manager object at 0x7f7f5b9a9750>]
新行为是这种情况会引发错误:
sqlalchemy.exc.InvalidRequestError: Row with identity key (<class '__main__.Employee'>, (1,), None) can't be loaded into an object; the polymorphic discriminator column '%(140205120401296 anon)s.type' refers to mapped class Engineer->engineer, which is not a sub-mapper of the requested mapped class Manager->manager
仅当该实体的主键列为非 NULL 时才会引发上述错误。如果行中没有给定实体的主键,则不会尝试构造实体。
在单继承映射的情况下,行为的变化稍微更为复杂;如果上述的Engineer
和Manager
被映射为单表继承,在 1.3 中,将发出以下查询,并且只返回一个Manager
对象:
SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id FROM (SELECT employee.type AS type, employee.id AS id FROM employee) AS anon_1 WHERE anon_1.type IN (?) 2020-01-29 18:08:32,975 INFO sqlalchemy.engine.base.Engine ('manager',) [<__main__.Manager object at 0x7ff1b0200d50>]
Query
向子查询添加了“单表继承”条件,对最初设置的意图进行了评论。此行为是在版本 1.0 中添加的#3891,并在“连接”和“单”表继承之间创建了行为不一致,并且修改了给定查询的意图,可能意图返回列对应于继承实体的空值的其他行,这是一个有效的用例。该行为现在等同于连接表继承的行为,其中假定子查询返回正确的行,如果遇到意外的多态标识,则会引发错误:
SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id FROM (SELECT employee.type AS type, employee.id AS id FROM employee) AS anon_1 2020-01-29 18:13:10,554 INFO sqlalchemy.engine.base.Engine () Traceback (most recent call last): # ... sqlalchemy.exc.InvalidRequestError: Row with identity key (<class '__main__.Employee'>, (1,), None) can't be loaded into an object; the polymorphic discriminator column '%(140700085268432 anon)s.type' refers to mapped class Engineer->employee, which is not a sub-mapper of the requested mapped class Manager->employee
如上所述的情况下的正确调整是调整给定的子查询,以正确根据鉴别器列过滤行:
print( s.query(Manager) .select_entity_from( s.query(Employee).filter(Employee.discriminator == "manager").subquery() ) .all() )
SELECT anon_1.type AS anon_1_type, anon_1.id AS anon_1_id FROM (SELECT employee.type AS type, employee.id AS id FROM employee WHERE employee.type = ?) AS anon_1 2020-01-29 18:14:49,770 INFO sqlalchemy.engine.base.Engine ('manager',) [<__main__.Manager object at 0x7f70e13fca90>]
方言更改
pg8000 的最低版本为 1.16.6,仅支持 Python 3
支持 pg8000 方言已经得到显着改进,得益于该项目的维护者。
由于 API 更改,pg8000 方言现在需要版本 1.16.6 或更高版本。从 1.13 系列开始,pg8000 系列已经放弃了 Python 2 支持。需要 pg8000 的 Python 2 用户应确保他们的要求被固定在 SQLAlchemy<1.4
。
PostgreSQL psycopg2 方言需要 psycopg2 版本 2.7 或更高版本。
psycopg2 方言依赖于过去几年中发布的许多 psycopg2 特性。为了简化方言,现在要求的最低版本是 2017 年 3 月发布的版本 2.7。
psycopg2 方言不再对绑定参数名称有限制
SQLAlchemy 1.3 无法适应在 psycopg2 方言下包含百分号或括号的绑定参数名称。这反过来意味着包含这些字符的列名也有问题,因为 INSERT 和其他 DML 语句会生成与列名匹配的参数名称,这将导致失败。解决方法是利用 Column.key
参数,以便生成用于生成参数的替代名称,或者必须在 create_engine()
级别更改方言的参数样式。从 SQLAlchemy 1.4.0beta3 开始,所有命名限制都已被移除,并且在所有情况下参数都被完全转义,因此这些解决方法不再必要。
#5653 ### psycopg2 方言默认使用“execute_values”和 RETURNING 来进行 INSERT 语句
使用 Core 和 ORM 时,PostgreSQL 的一个重要性能增强的前半部分,psycopg2 方言现在默认使用 psycopg2.extras.execute_values()
来编译 INSERT 语句,并在此模式下实现了 RETURNING 支持。这一变化的另一半是 ORM 批量插入现在在大多数情况下使用带有 RETURNING 的 psycopg2 批量语句,这使得 ORM 能够利用 executemany(即批量插入语句)的 RETURNING,因此使用 psycopg2 进行的 ORM 批量插入速度提高了 400%,具体取决于具体情况。
此扩展方法允许在单个语句中插入多行,使用语句的扩展 VALUES 子句。虽然 SQLAlchemy 的insert()
构造已经通过Insert.values()
方法支持此语法,但扩展方法允许在执行语句时动态构建 VALUES 子句,这是当将参数字典列表传递给Connection.execute()
时发生的“executemany”执行。它还发生在缓存边界之外,以便在渲染 VALUES 之前可以缓存 INSERT 语句。
在性能示例套件中使用bulk_inserts.py
脚本快速测试execute_values()
方法,显示出约五倍的性能提升:
$ python -m examples.performance bulk_inserts --test test_core_insert --num 100000 --dburl postgresql://scott:tiger@localhost/test # 1.3 test_core_insert : A single Core INSERT construct inserting mappings in bulk. (100000 iterations); total time 5.229326 sec # 1.4 test_core_insert : A single Core INSERT construct inserting mappings in bulk. (100000 iterations); total time 0.944007 sec
“batch”扩展的支持是在版本 1.2 中添加的,在支持批处理模式/快速执行助手,并在 1.3 中增强以支持execute_values
扩展在#4623中。在 1.4 中,execute_values
扩展现在默认为 INSERT 语句打开;UPDATE 和 DELETE 的“batch”扩展默认关闭。
此外,execute_values
扩展函数支持将由 RETURNING 生成的行作为聚合列表返回。如果给定的insert()
构造请求通过Insert.returning()
方法或类似用于返回生成默认值的方法来返回,psycopg2 方言现在将检索此列表;然后将这些行安装在结果中,以便它们被检索为直接来自游标。这允许 ORM 等工具在所有情况下使用批量插入,预计将提供显著的性能改进。
psycopg2 方言的executemany_mode
功能已经进行了以下更改:
- 添加了一个新模式
"values_only"
。此模式使用非常高效的psycopg2.extras.execute_values()
扩展方法来运行使用 executemany()的编译 INSERT 语句,但不使用execute_batch()
来运行 UPDATE 和 DELETE 语句。这种新模式现在是 psycopg2 方言的默认设置。 - 现有的
"values"
模式现在被命名为"values_plus_batch"
。此模式将使用execute_values
进行 INSERT 语句,使用execute_batch
进行 UPDATE 和 DELETE 语句。该模式默认未启用,因为它会禁用使用executemany()
执行 UPDATE 和 DELETE 语句时的cursor.rowcount
的正确功能。 - 对于 INSERT 语句,启用了
"values_only"
和"values"
的 RETURNING 支持。psycopg2 方言将使用 fetch=True 标志从 psycopg2 接收行,并将它们安装到结果集中,就好像它们直接来自游标(尽管最终确实是这样,但是 psycopg2 的扩展函数已经将多个批次聚合成一个列表)。 execute_values
的默认“page_size”设置从 100 增加到 1000。execute_batch
函数的默认值仍为 100。这些参数可以像以前一样进行修改。- 1.2 版本功能中的
use_batch_mode
标志已被移除;行为仍可通过 1.3 中添加的executemany_mode
标志进行控制。 - 核心引擎和方言已经增强,以支持
executemany
加上返回模式,目前仅适用于 psycopg2,通过提供新的CursorResult.inserted_primary_key_rows
和CursorResult.returned_default_rows
访问器。
另请参见
Psycopg2 快速执行助手
#5401 ### 从 SQLite 方言中删除了“连接重写”逻辑;更新了导入
放弃了支持右嵌套连接重写,以支持 2013 年发布的旧 SQLite 版本低于 3.7.16。不希望任何现代 Python 版本依赖于此限制。
该行为首次在 0.9 版本中引入,并作为更大变化的一部分,允许右嵌套连接,如migration_09.html#feature-joins-09所述。然而,由于其复杂性,SQLite 的解决方法在 2013-2014 年间产生了许多回归问题。2016 年,方言被修改,以便连接重写逻辑仅在 SQLite 版本低于 3.7.16 时发生,通过二分法确定 SQLite 修复了对此结构的支持的位置,并且没有进一步的问题报告该行为(尽管在内部发现了一些错误)。现在预计,几乎没有 Python 2.7 或 3.5 及以上版本(支持的 Python 版本)的构建包含 SQLite 版本低于 3.7.17,该行为仅在更复杂的 ORM 连接场景中才是必要的。如果安装的 SQLite 版本旧于 3.7.16,则现在会发出警告。
在相关更改中,SQLite 的模块导入不再尝试在 Python 3 上导入“pysqlite2”驱动程序,因为该驱动程序在 Python 3 上不存在;对于旧的 pysqlite2 版本的非常古老的警告也被删除。
#4895 ### 为 MariaDB 10.3 添加了序列支持
MariaDB 数据库截至 10.3 版本支持序列。SQLAlchemy 的 MySQL 方言现在在该数据库上实现了对Sequence
对象的支持,这意味着对于在相同方式下的Table
或MetaData
集合中存在的Sequence
将发出“CREATE SEQUENCE” DDL,就像对于后端如 PostgreSQL、Oracle 等一样,当方言的服务器版本检查确认数据库是 MariaDB 10.3 或更高版本时。此外,当以这些方式使用时,Sequence
将作为列默认值和主键生成对象。
由于此更改将影响当前部署在 MariaDB 10.3 上的应用程序的 DDL 和 INSERT 语句的行为假设,同时也会显式使用Sequence
构造在其表定义中,因此重要的是要注意Sequence
支持一个标志Sequence.optional
,用于限制Sequence
生效的情况。当在表的整数主键列中使用“optional”时,Sequence
:
Table( "some_table", metadata, Column( "id", Integer, Sequence("some_seq", start=1, optional=True), primary_key=True ), )
上述Sequence
仅在目标数据库不支持其他生成整数主键值的方式时用于 DDL 和 INSERT 语句。也就是说,上述 Oracle 数据库将使用该序列,但 PostgreSQL 和 MariaDB 10.3 数据库不会。对于正在升级到 SQLAlchemy 1.4 的现有应用程序而言,这可能很重要,因为如果尝试使用未创建的序列进行 INSERT 语句,则会失败。
另请参阅
定义序列
#4976 ### 添加了对 SQL Server 的与 IDENTITY 不同的 Sequence 支持
Sequence
构造现在与 Microsoft SQL Server 完全兼容。当应用于Column
时,表的 DDL 将不再包含 IDENTITY 关键字,而是依赖于“CREATE SEQUENCE”来确保存在一个序列,然后将用于表上的 INSERT 语句。
在版本 1.3 之前,Sequence
用于控制 SQL Server 中 IDENTITY 列的参数;这种用法在 1.3 期间发出了弃用警告,并在 1.4 中被移除。要控制 IDENTITY 列的参数,应使用mssql_identity_start
和mssql_identity_increment
参数;请参阅下面链接的 MSSQL 方言文档。
另请参见
自增行为/IDENTITY 列
pg8000 的最低版本为 1.16.6,仅支持 Python 3
pg8000 方言的支持得到了显著改善,得益于项目的维护者的帮助。
由于 API 更改,pg8000 方言现在要求版本为 1.16.6 或更高。从 1.13 系列开始,pg8000 系列已经放弃了对 Python 2 的支持。需要 pg8000 的 Python 2 用户应确保他们的要求被固定在SQLAlchemy<1.4
。
要求使用版本为 2.7 或更高的 psycopg2 以支持 PostgreSQL psycopg2 方言
psycopg2 方言依赖于过去几年中发布的许多 psycopg2 功能。为了简化方言,现在要求的最低版本是 2017 年 3 月发布的版本 2.7。
psycopg2 方言不再对绑定参数名称有限制
SQLAlchemy 1.3 无法容纳包含百分号或括号的绑定参数名称,这意味着包含这些字符的列名也会有问题,因为 INSERT 和其他 DML 语句会生成与列名匹配的参数名称,这将导致失败。解决方法是利用Column.key
参数,以便使用替代名称来生成参数,或者在create_engine()
级别更改方言的参数样式。从 SQLAlchemy 1.4.0beta3 开始,所有命名限制都已被移除,并且在所有情况下参数都被完全转义,因此这些解决方法不再必要。
psycopg2 方言默认使用“execute_values”与 RETURNING 来处理 INSERT 语句
在使用 Core 和 ORM 时,PostgreSQL 的一个重要性能增强的前半部分,psycopg2 方言现在默认使用psycopg2.extras.execute_values()
来编译 INSERT 语句,并且还在此模式下实现了 RETURNING 支持。这一变化的另一半是 ORM Batch inserts with psycopg2 now batch statements with RETURNING in most cases,这允许 ORM 利用 RETURNING 与 executemany(即批量插入 INSERT 语句)以便 ORM 批量插入与 psycopg2 在具体情况下快 400%。
这个扩展方法允许在单个语句中插入多行,使用扩展的 VALUES 子句。虽然 SQLAlchemy 的insert()
构造已经通过Insert.values()
方法支持这种语法,但是扩展方法允许在执行语句时动态构建 VALUES 子句,这是在通过将参数字典列表传递给Connection.execute()
时发生的“executemany”执行。它还发生在缓存边界之外,以便在渲染 VALUES 之前可以缓存 INSERT 语句。
在 Performance 示例套件中使用bulk_inserts.py
脚本快速测试execute_values()
方法,发现大约五倍的性能提升:
$ python -m examples.performance bulk_inserts --test test_core_insert --num 100000 --dburl postgresql://scott:tiger@localhost/test # 1.3 test_core_insert : A single Core INSERT construct inserting mappings in bulk. (100000 iterations); total time 5.229326 sec # 1.4 test_core_insert : A single Core INSERT construct inserting mappings in bulk. (100000 iterations); total time 0.944007 sec
在版本 1.2 中添加了对“batch”扩展的支持 Support for Batch Mode / Fast Execution Helpers,并在 1.3 中增强以包括对execute_values
扩展的支持#4623。在 1.4 中,execute_values
扩展现在默认为 INSERT 语句打开;UPDATE 和 DELETE 的“batch”扩展默认关闭。
此外,execute_values
扩展函数支持将由RETURNING
生成的行作为聚合列表返回。如果给定的insert()
构造请求通过Insert.returning()
方法或类似方法返回生成的默认值,则psycopg2
方言现在将检索此列表;然后将这些行安装在结果中,以便像直接来自游标一样检索它们。这允许 ORM 等工具在所有情况下使用批量插入,预计将提供显著的性能改进。
psycopg2
方言的executemany_mode
功能已经进行了以下更改:
- 添加了一个新模式
"values_only"
。此模式使用非常高效的psycopg2.extras.execute_values()
扩展方法来运行使用executemany()
的编译 INSERT 语句,但不使用execute_batch()
来运行 UPDATE 和 DELETE 语句。这个新模式现在是psycopg2
方言的默认设置。 - 现有的
"values"
模式现在被命名为"values_plus_batch"
。此模式将使用execute_values
进行 INSERT 语句,使用execute_batch
进行 UPDATE 和 DELETE 语句。该模式默认情况下未启用,因为它会禁用使用executemany()
执行 UPDATE 和 DELETE 语句时的cursor.rowcount
的正确功能。 - 对于 INSERT 语句,
"values_only"
和"values"
启用了 RETURNING 支持。psycopg2
方言将使用fetch=True
标志从psycopg2
接收行,并将它们安装到结果集中,就好像它们直接来自游标一样(尽管它们最终确实来自游标,但psycopg2
的扩展函数已将多个批次聚合成一个列表)。 execute_values
的默认“page_size”设置已从 100 增加到 1000。execute_batch
函数的默认值仍为 100。这些参数可以像以前一样进行修改。- 1.2 版本功能的
use_batch_mode
标志已被移除;行为仍可通过 1.3 版本中添加的executemany_mode
标志进行控制。 - 核心引擎和方言已经增强以支持
executemany
加返回模式,目前仅适用于psycopg2
,通过提供新的CursorResult.inserted_primary_key_rows
和CursorResult.returned_default_rows
访问器。
另请参见
Psycopg2 快速执行助手
从 SQLite 方言中删除了“join rewriting”逻辑;更新了导入
放弃了支持右嵌套连接重写以支持 2013 年发布的旧 SQLite 版本 3.7.16 之前的版本。不希望任何现代 Python 版本依赖于此限制。
该行为首次在 0.9 版本中引入,并作为允许右嵌套连接的较大更改的一部分,如许多 JOIN 和 LEFT OUTER JOIN 表达式将不再包装在(SELECT * FROM …) AS ANON_1 中所述。然而,由于其复杂性,SQLite 的解决方法在 2013-2014 年期间产生了许多回归。2016 年,方言被修改,以便仅在 SQLite 版本低于 3.7.16 的情况下进行连接重写逻辑,通过二分法确定 SQLite 修复了对此构造的支持的位置,并且没有进一步的问题报告该行为(尽管在内部发现了一些错误)。现在预计,几乎没有 Python 2.7 或 3.5 及以上版本(支持的 Python 版本)的构建会包含 SQLite 版本低于 3.7.17,该行为仅在更复杂的 ORM 连接场景中才是必要的。如果安装的 SQLite 版本旧于 3.7.16,则现在会发出警告。
在相关更改中,SQLite 的模块导入不再尝试在 Python 3 上导入“pysqlite2”驱动程序,因为该驱动程序在 Python 3 上不存在;对于旧的 pysqlite2 版本的非常古老警告也被删除。
为 MariaDB 10.3 添加了 Sequence 支持
截至 10.3 版本,MariaDB 数据库支持序列。SQLAlchemy 的 MySQL 方言现在实现了对该数据库的Sequence
对象的支持,这意味着当方言的服务器版本检查确认数据库是 MariaDB 10.3 或更高版本时,将为Table
或MetaData
集合中存在的Sequence
发出“CREATE SEQUENCE” DDL,就像对于后端如 PostgreSQL、Oracle 等一样。此外,当以这些方式使用时,Sequence
将作为列默认值和主键生成对象。
由于这一变化将影响 DDL 的假设以及针对 MariaDB 10.3 的当前部署应用程序的 INSERT 语句的行为,该应用程序也恰好在其表定义中明确使用Sequence
构造,因此重要的是要注意Sequence
支持一个标志Sequence.optional
,用于限制Sequence
生效的情况。当在表的整数主键列上使用“optional”时:
Table( "some_table", metadata, Column( "id", Integer, Sequence("some_seq", start=1, optional=True), primary_key=True ), )
上述Sequence
仅在目标数据库不支持其他生成整数主键值的方式时用于 DDL 和 INSERT 语句。也就是说,上述 Oracle 数据库将使用该序列,但 PostgreSQL 和 MariaDB 10.3 数据库不会。这对于正在升级到 SQLAlchemy 1.4 的现有应用程序可能很重要,因为如果插入语句试图使用未创建的序列,则会失败。
另请参见
定义序列
添加了对 SQL Server 的与 IDENTITY 不同的 Sequence 支持
Sequence
构造现在已经完全与 Microsoft SQL Server 兼容。当应用于Column
时,表的 DDL 将不再包含 IDENTITY 关键字,而是依赖于“CREATE SEQUENCE”来确保存在一个序列,然后将用于表的 INSERT 语句。
在版本 1.3 之前,Sequence
用于控制 SQL Server 中的 IDENTITY 列的参数;这种用法在 1.3 中发出了弃用警告,并在 1.4 中已被移除。对于控制 IDENTITY 列的参数,应使用mssql_identity_start
和mssql_identity_increment
参数;请参阅下面链接的 MSSQL 方言文档。
另请参见
自增行为 / IDENTITY 列
#4976 ### 添加了对 SQL Server 的与 IDENTITY 不同的 Sequence 支持
Sequence
构造现在与 Microsoft SQL Server 完全兼容。当应用于Column
时,表的 DDL 将不再包含 IDENTITY 关键字,而是依赖于“CREATE SEQUENCE”来确保存在一个序列,然后将用于表上的 INSERT 语句。
在版本 1.3 之前,Sequence
用于控制 SQL Server 中 IDENTITY 列的参数;这种用法在 1.3 期间发出了弃用警告,并在 1.4 中被移除。要控制 IDENTITY 列的参数,应使用mssql_identity_start
和mssql_identity_increment
参数;请参阅下面链接的 MSSQL 方言文档。
另请参见
自增行为/IDENTITY 列
pg8000 的最低版本为 1.16.6,仅支持 Python 3
pg8000 方言的支持得到了显著改善,得益于项目的维护者的帮助。
由于 API 更改,pg8000 方言现在要求版本为 1.16.6 或更高。从 1.13 系列开始,pg8000 系列已经放弃了对 Python 2 的支持。需要 pg8000 的 Python 2 用户应确保他们的要求被固定在SQLAlchemy<1.4
。
要求使用版本为 2.7 或更高的 psycopg2 以支持 PostgreSQL psycopg2 方言
psycopg2 方言依赖于过去几年中发布的许多 psycopg2 功能。为了简化方言,现在要求的最低版本是 2017 年 3 月发布的版本 2.7。
psycopg2 方言不再对绑定参数名称有限制
SQLAlchemy 1.3 无法容纳包含百分号或括号的绑定参数名称,这意味着包含这些字符的列名也会有问题,因为 INSERT 和其他 DML 语句会生成与列名匹配的参数名称,这将导致失败。解决方法是利用Column.key
参数,以便使用替代名称来生成参数,或者在create_engine()
级别更改方言的参数样式。从 SQLAlchemy 1.4.0beta3 开始,所有命名限制都已被移除,并且在所有情况下参数都被完全转义,因此这些解决方法不再必要。
psycopg2 方言默认使用“execute_values”与 RETURNING 来处理 INSERT 语句
在使用 Core 和 ORM 时,PostgreSQL 的一个重要性能增强的前半部分,psycopg2 方言现在默认使用psycopg2.extras.execute_values()
来编译 INSERT 语句,并且还在此模式下实现了 RETURNING 支持。这一变化的另一半是 ORM Batch inserts with psycopg2 now batch statements with RETURNING in most cases,这允许 ORM 利用 RETURNING 与 executemany(即批量插入 INSERT 语句)以便 ORM 批量插入与 psycopg2 在具体情况下快 400%。
这个扩展方法允许在单个语句中插入多行,使用扩展的 VALUES 子句。虽然 SQLAlchemy 的insert()
构造已经通过Insert.values()
方法支持这种语法,但是扩展方法允许在执行语句时动态构建 VALUES 子句,这是在通过将参数字典列表传递给Connection.execute()
时发生的“executemany”执行。它还发生在缓存边界之外,以便在渲染 VALUES 之前可以缓存 INSERT 语句。
在 Performance 示例套件中使用bulk_inserts.py
脚本快速测试execute_values()
方法,发现大约五倍的性能提升:
$ python -m examples.performance bulk_inserts --test test_core_insert --num 100000 --dburl postgresql://scott:tiger@localhost/test # 1.3 test_core_insert : A single Core INSERT construct inserting mappings in bulk. (100000 iterations); total time 5.229326 sec # 1.4 test_core_insert : A single Core INSERT construct inserting mappings in bulk. (100000 iterations); total time 0.944007 sec
在版本 1.2 中添加了对“batch”扩展的支持 Support for Batch Mode / Fast Execution Helpers,并在 1.3 中增强以包括对execute_values
扩展的支持#4623。在 1.4 中,execute_values
扩展现在默认为 INSERT 语句打开;UPDATE 和 DELETE 的“batch”扩展默认关闭。
此外,execute_values
扩展函数支持将由RETURNING
生成的行作为聚合列表返回。如果给定的insert()
构造请求通过Insert.returning()
方法或类似方法返回生成的默认值,则psycopg2
方言现在将检索此列表;然后将这些行安装在结果中,以便像直接来自游标一样检索它们。这允许 ORM 等工具在所有情况下使用批量插入,预计将提供显著的性能改进。
psycopg2
方言的executemany_mode
功能已经进行了以下更改:
- 添加了一个新模式
"values_only"
。此模式使用非常高效的psycopg2.extras.execute_values()
扩展方法来运行使用executemany()
的编译 INSERT 语句,但不使用execute_batch()
来运行 UPDATE 和 DELETE 语句。这个新模式现在是psycopg2
方言的默认设置。 - 现有的
"values"
模式现在被命名为"values_plus_batch"
。此模式将使用execute_values
进行 INSERT 语句,使用execute_batch
进行 UPDATE 和 DELETE 语句。该模式默认情况下未启用,因为它会禁用使用executemany()
执行 UPDATE 和 DELETE 语句时的cursor.rowcount
的正确功能。 - 对于 INSERT 语句,
"values_only"
和"values"
启用了 RETURNING 支持。psycopg2
方言将使用fetch=True
标志从psycopg2
接收行,并将它们安装到结果集中,就好像它们直接来自游标一样(尽管它们最终确实来自游标,但psycopg2
的扩展函数已将多个批次聚合成一个列表)。 execute_values
的默认“page_size”设置已从 100 增加到 1000。execute_batch
函数的默认值仍为 100。这些参数可以像以前一样进行修改。- 1.2 版本功能的
use_batch_mode
标志已被移除;行为仍可通过 1.3 版本中添加的executemany_mode
标志进行控制。 - 核心引擎和方言已经增强以支持
executemany
加返回模式,目前仅适用于psycopg2
,通过提供新的CursorResult.inserted_primary_key_rows
和CursorResult.returned_default_rows
访问器。
另请参见
Psycopg2 快速执行助手
从 SQLite 方言中删除了“join rewriting”逻辑;更新了导入
放弃了支持右嵌套连接重写以支持 2013 年发布的旧 SQLite 版本 3.7.16 之前的版本。不希望任何现代 Python 版本依赖于此限制。
该行为首次在 0.9 版本中引入,并作为允许右嵌套连接的较大更改的一部分,如许多 JOIN 和 LEFT OUTER JOIN 表达式将不再包装在(SELECT * FROM …) AS ANON_1 中所述。然而,由于其复杂性,SQLite 的解决方法在 2013-2014 年期间产生了许多回归。2016 年,方言被修改,以便仅在 SQLite 版本低于 3.7.16 的情况下进行连接重写逻辑,通过二分法确定 SQLite 修复了对此构造的支持的位置,并且没有进一步的问题报告该行为(尽管在内部发现了一些错误)。现在预计,几乎没有 Python 2.7 或 3.5 及以上版本(支持的 Python 版本)的构建会包含 SQLite 版本低于 3.7.17,该行为仅在更复杂的 ORM 连接场景中才是必要的。如果安装的 SQLite 版本旧于 3.7.16,则现在会发出警告。
在相关更改中,SQLite 的模块导入不再尝试在 Python 3 上导入“pysqlite2”驱动程序,因为该驱动程序在 Python 3 上不存在;对于旧的 pysqlite2 版本的非常古老警告也被删除。
为 MariaDB 10.3 添加了 Sequence 支持
截至 10.3 版本,MariaDB 数据库支持序列。SQLAlchemy 的 MySQL 方言现在实现了对该数据库的Sequence
对象的支持,这意味着当方言的服务器版本检查确认数据库是 MariaDB 10.3 或更高版本时,将为Table
或MetaData
集合中存在的Sequence
发出“CREATE SEQUENCE” DDL,就像对于后端如 PostgreSQL、Oracle 等一样。此外,当以这些方式使用时,Sequence
将作为列默认值和主键生成对象。
由于这一变化将影响 DDL 的假设以及针对 MariaDB 10.3 的当前部署应用程序的 INSERT 语句的行为,该应用程序也恰好在其表定义中明确使用Sequence
构造,因此重要的是要注意Sequence
支持一个标志Sequence.optional
,用于限制Sequence
生效的情况。当在表的整数主键列上使用“optional”时:
Table( "some_table", metadata, Column( "id", Integer, Sequence("some_seq", start=1, optional=True), primary_key=True ), )
上述Sequence
仅在目标数据库不支持其他生成整数主键值的方式时用于 DDL 和 INSERT 语句。也就是说,上述 Oracle 数据库将使用该序列,但 PostgreSQL 和 MariaDB 10.3 数据库不会。这对于正在升级到 SQLAlchemy 1.4 的现有应用程序可能很重要,因为如果插入语句试图使用未创建的序列,则会失败。
另请参见
定义序列
添加了对 SQL Server 的与 IDENTITY 不同的 Sequence 支持
Sequence
构造现在已经完全与 Microsoft SQL Server 兼容。当应用于Column
时,表的 DDL 将不再包含 IDENTITY 关键字,而是依赖于“CREATE SEQUENCE”来确保存在一个序列,然后将用于表的 INSERT 语句。
在版本 1.3 之前,Sequence
用于控制 SQL Server 中的 IDENTITY 列的参数;这种用法在 1.3 中发出了弃用警告,并在 1.4 中已被移除。对于控制 IDENTITY 列的参数,应使用mssql_identity_start
和mssql_identity_increment
参数;请参阅下面链接的 MSSQL 方言文档。
另请参见
自增行为 / IDENTITY 列