SqlAlchemy 2.0 中文文档(七十二)(1)https://developer.aliyun.com/article/1561000
行为变化 - ORM
由 Query 返回的“KeyedTuple”对象被 Row 取代
如在 RowProxy is no longer a “proxy”; is now called Row and behaves like an enhanced named tuple 中所讨论的,核心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
的行为相匹配。
这表明的行为变化是:
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
,留为空,并且只在需要时创建一个新的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
和数据库本身的交互顺序也应保持不受影响,因为这些操作已经以按需方式运行。
#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
的行为。给定下面已经在 Session
中的 User
,将其分配给 Address
对象的 Address.user
属性,假设已经建立了双向关系,这意味着在那一点上 Address
也会被放入 Session
中:
u1 = User() session.add(u1) a1 = Address() a1.user = u1 # <--- adds "a1" to the Session
上述行为是 backref 行为的意外副作用,因为由于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 级联”实际发生时,将发出警告。要获得新行为,要么在任何目标关系上将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 ### Eager loaders emit during unexpire operations
长期以来一直期望的行为是,当访问一个过期对象时,配置的急切加载器将运行,以便在对象被刷新或以其他方式取消过期时急切加载过期对象上的关系。现在已经添加了这种行为,因此 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
中持久化,或者在急切选项应用之前用不同的查询加载了该对象,则该对象不具有与之关联的急切加载选项。这并不是一个新概念,但是寻找刷新行为上的 eagerload 的用户可能会发现这更加明显。
#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 版本中,1.3 及更早版本已经复制到查询的最顶层的关系加载器选项,如 joinedload()
,在 UNION 情况下已经复制到了查询的顶层,并且只从 UNION 的第一个元素中取出选项,丢弃查询的其他部分上的任何选项。
上面展示的隐式复制和选择性忽略选项的行为,是一种遗留行为,仅限于 Query
的一部分,是一个特殊的例子,说明了 Query
及其应用 Query.union_all()
的方式存在不足之处,因为不清楚如何将单个 SELECT 转换为自身和另一个查询的 UNION,并且不清楚如何将加载选项应用于该新语句。
SQLAlchemy 1.4 的行为可展示为通常优于 1.3 版本的情况,用于更常见情况的使用 defer()
。以下查询:
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 风格 查询时,目前不会发出警告,然而嵌套的 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
下面。### 在临时对象上访问未初始化的集合属性不再改变 dict
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
时,表示此关系仅应用于从数据库加载数据,不应进行变异或涉及持久性操作。为确保此约定成功运行,关系不再能指定在“仅查看”方面毫无意义的 relationship.cascade
设置。
这里的主要目标是“删除,删除孤儿”级联,即使 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>]
方言更改
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 方言需要版本 2.7 或更高版本的 psycopg2。
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”来进行 INSERT 语句的 RETURNING 操作
在使用 Core 和 ORM 时,对于 PostgreSQL 的重大性能增强的前半部分,psycopg2 方言现在默认使用 psycopg2.extras.execute_values()
来编译 INSERT 语句,并在此模式下实现了 RETURNING 支持。这个变化的另一半是 ORM 批量插入现在在大多数情况下使用 RETURNING 的批量语句,它允许 ORM 利用 executemany(即批量 INSERT 语句的批处理)从而使得使用 psycopg2 的 ORM 批量插入速度提高了 400% 取决于具体情况。
此扩展方法允许在单个语句中插入多行,使用语句的扩展 VALUES 子句。虽然 SQLAlchemy 的 insert()
构造已经通过 Insert.values()
方法支持此语法,但是扩展方法允许在执行语句时动态构建 VALUES 子句,当语句执行为“executemany”执行时,即当将参数字典列表传递给 Connection.execute()
时会发生这种情况。它还发生在缓存边界之外,因此在渲染 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 中添加了对“批处理”扩展的支持 Support for Batch Mode / Fast Execution Helpers,并在版本 1.3 中增强了对 execute_values
扩展的支持 #4623。在版本 1.4 中,对于 INSERT 语句现在默认打开了 execute_values
扩展;UPDATE 和 DELETE 的“批处理”扩展仍然默认关闭。
execute_values
扩展函数还支持将由 RETURNING 生成的行作为聚合列表返回。如果给定的 insert()
构造请求返回通过 Insert.returning()
方法或类似用于返回生成的默认值的方法生成的行,那么 psycopg2 方言现在将检索此列表;然后将行安装在结果中,以便像直接来自游标一样检索它们。这使得像 ORM 这样的工具在所有情况下都可以使用批量插入,预计将提供显著的性能改进。
psycopg2 方言的 executemany_mode
功能已经进行了以下更改:
- 添加了一个新模式
"values_only"
。此模式使用非常高效的psycopg2.extras.execute_values()
扩展方法来运行编译的 INSERT 语句,但不使用execute_batch()
来运行 UPDATE 和 DELETE 语句。此新模式现在是 psycopg2 方言的默认设置。 - 现有的
"values"
模式现已更名为"values_plus_batch"
。此模式将对 INSERT 语句使用execute_values
,并对 UPDATE 和 DELETE 语句使用execute_batch
。该模式默认未启用,因为它会导致使用executemany()
执行的 UPDATE 和 DELETE 语句的cursor.rowcount
的正常功能受到影响。 - 对于 INSERT 语句,启用了 RETURNING 支持,针对
"values_only"
和"values"
。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,并作为允许右嵌套连接的较大更改的一部分,如 Many JOIN and LEFT OUTER JOIN expressions will no longer be wrapped in (SELECT * FROM …) AS ANON_1 中所述。然而,由于其复杂性,SQLite 的解决方法在 2013-2014 年期间产生了许多回归。2016 年,方言被修改,使连接重写逻辑仅在 SQLite 版本低于 3.7.16 时发生,使用二分法确定了 SQLite 修复了此结构支持的位置之后,并且未报告进一步的问题(尽管在内部发现了一些错误)。现在预计,几乎没有 Python 2.7 或 3.5 及以上版本(支持的 Python 版本)的构建包含低于 3.7.17 的 SQLite 版本,并且该行为仅在更复杂的 ORM 连接方案中才是必需的。如果安装的 SQLite 版本旧于 3.7.16,则现在会发出警告。
在相关更改中,SQLite 的模块导入不再尝试在 Python 3 上导入“pysqlite2”驱动程序,因为此驱动程序在 Python 3 上不存在;对于旧的 pysqlite2 版本的非常古老警告也被删除。
#4895 ### 为 MariaDB 10.3 添加了序列支持
截至 MariaDB 10.3,MariaDB 数据库支持序列。SQLAlchemy 的 MySQL 方言现在实现了对此数据库的Sequence
对象的支持,这意味着对于在相同方式下的Table
或MetaData
集合中存在的Sequence
,将发出“CREATE SEQUENCE” DDL。就像对于后端如 PostgreSQL、Oracle 一样,当方言的服务器版本检查确认数据库是 MariaDB 10.3 或更高版本时。此外,当以这些方式使用时,Sequence
将作为列默认值和主键生成对象。
由于此更改将影响 DDL 和 INSERT 语句的假设,对于当前部署在 MariaDB 10.3 上的应用程序,该应用程序还明确使用其表定义中的Sequence
构造,重要的是要注意Sequence
支持一个标志Sequence.optional
,用于限制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 的现有应用程序可能很重要,因为如果试图使用未创建的序列,则 INSERT 语句将失败。
另请参阅
定义序列
#4976 ### 添加了与 SQL Server 不同的序列支持
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 列
主要的 API 更改和特性 - 通用
Python 3.6 是最低 Python 3 版本;仍支持 Python 2.7
由于 Python 3.5 在 2020 年 9 月到达 EOL,SQLAlchemy 1.4 现在将版本 3.6 作为最低 Python 3 版本。仍支持 Python 2.7,但是 SQLAlchemy 1.4 系列将是最后一个支持 Python 2 的系列。 ### ORM 查询在内部与选择、更新、删除统一;2.0 风格的执行可用
对于 SQLAlchemy 版本 2.0 和本质上的 1.4 来说,最大的概念性改变是核心中的 Select
构造和 ORM 中的 Query
对象之间的巨大分离已被移除,以及在它们之间的 Query.update()
和 Query.delete()
方法与 Update
和 Delete
的关系。
关于Select
和Query
,这两个对象在许多版本中具有类似的、大部分重叠的 API,甚至有一些能够在两者之间切换的能力,但在使用模式和行为上仍然有很大的不同。这一历史背景是,Query
对象是为了克服Select
对象的缺点而引入的,后者曾经是 ORM 对象查询的核心,只是它们必须以Table
元数据的形式进行查询。然而,Query
只有一个简单的接口来加载对象,只有在许多主要版本的发布过程中,它最终才获得了大部分Select
对象的灵活性,这导致这两个对象变得非常相似,但仍然在很大程度上不兼容。
在版本 1.4 中,所有核心和 ORM SELECT 语句都直接从Select
对象呈现;当使用Query
对象时,在语句调用时,它会将其状态复制到一个Select
对象中,然后使用 2.0 风格执行。未来,Query
对象将仅成为传统,应用程序将被鼓励转向 2.0 风格执行,允许核心构造自由地针对 ORM 实体使用:
with Session(engine, future=True) as sess: stmt = ( select(User) .where(User.name == "sandy") .join(User.addresses) .where(Address.email_address.like("%gmail%")) ) result = sess.execute(stmt) for user in result.scalars(): print(user)
以上示例的注意事项:
Session
和sessionmaker
对象现在具有完整的上下文管理器(即with:
语句)功能;请参阅打开和关闭会话的修订文档以获取示例。- 在 1.4 系列中,所有 2.0 风格 的 ORM 调用都使用一个包含
Session
的Session.future
标志设置为True
的标志;此标志表示Session
应具有 2.0 风格的行为,其中包括可以从execute
调用 ORM 查询以及一些事务特性的更改。在 2.0 版本中,此标志将始终为True
。 select()
构造不再需要在列子句周围加括号;有关此改进,请参见 select(), case() 现在接受位置表达式。select()
/Select
对象具有一个Select.join()
方法,其行为类似于Query
,甚至可以容纳 ORM 关系属性(而不会破坏 Core 和 ORM 之间的分离!)- 有关此内容,请参见 select().join() 和 outerjoin() 将 JOIN 条件添加到当前查询,而不是创建子查询。- 与 ORM 实体一起工作并且预计返回 ORM 结果的语句是使用
Session.execute()
调用的。请参见 Querying 以获取入门指南。另请参阅 ORM Session.execute() 在所有情况下使用“future”风格结果集 中的以下注意事项。 - 返回一个
Result
对象,而不是一个普通列表,这本身是以前的ResultProxy
对象的一个更复杂的版本;此对象现在用于 Core 和 ORM 结果。有关此信息,请参见 New Result object,RowProxy 不再是“代理”;现在称为 Row 并且行为类似于增强的命名元组,以及 Query 返回的“KeyedTuple”对象被 Row 替换。
在 SQLAlchemy 的文档中,将会有许多关于 1.x 风格和 2.0 风格执行的引用。这是为了区分两种查询风格,并尝试向前记录新的调用风格。在 SQLAlchemy 2.0 中,虽然Query
对象可能仍然作为传统构造保留,但在大多数文档中将不再出现。
对“批量更新和删除”进行了类似的调整,以便核心update()
和delete()
可用于批量操作。像下面这样的批量更新:
session.query(User).filter(User.name == "sandy").update( {"password": "foobar"}, synchronize_session="fetch" )
现在可以通过 2.0 风格来实现(实际上上述内容在内部以这种方式运行)如下所示:
with Session(engine, future=True) as sess: stmt = ( update(User) .where(User.name == "sandy") .values(password="foobar") .execution_options(synchronize_session="fetch") ) sess.execute(stmt)
请注意使用Executable.execution_options()
方法传递 ORM 相关选项。现在“执行选项”的使用在核心和 ORM 中更加普遍,许多来自Query
的 ORM 相关方法现在被实现为执行选项(查看Query.execution_options()
以获取一些示例)。
另请参阅
SQLAlchemy 2.0 - 主要迁移指南
#5159 ### ORM Session.execute()
在所有情况下都使用“future”风格的Result
集
如 RowProxy 不再是“代理”;现在称为 Row 并且行为类似增强的命名元组中所述,当与设置为True
的create_engine.future
参数的Engine
一起使用时,Result
和Row
对象现在具有“命名元组”行为。这些特定的“命名元组”行现在包括一项行为变更,即 Python 包含表达式使用in
,例如:
>>> engine = create_engine("...", future=True) >>> conn = engine.connect() >>> row = conn.execute.first() >>> "name" in row True
上述包含测试将使用值包含,而不是键包含;row
需要具有“name”的值才能返回True
。
在 SQLAlchemy 1.4 中,当create_engine.future
参数设置为False
时,将返回传统风格的LegacyRow
对象,其具有之前 SQLAlchemy 版本的部分命名元组行为,其中包含性检查继续使用键包含;如果行中有名为“name”的列,则"name" in row
将返回 True,而不是一个值。
在使用Session.execute()
时,完整的命名元组样式被无条件地启用,这意味着"name" in row
将使用值包含作为测试,而不是键包含。这是为了适应Session.execute()
现在返回一个Result
,该结果还适应 ORM 结果,其中甚至像由Query.all()
返回的传统 ORM 结果行也使用值包含。
这是从 SQLAlchemy 1.3 到 1.4 的行为变更。要继续接收键包含集合,请使用Result.mappings()
方法接收返回行为字典的MappingResult
:
for dict_row in session.execute(text("select id from table")).mappings(): assert "id" in dict_row ```### 透明 SQL 编译缓存添加到 Core、ORM 中的所有 DQL、DML 语句 这是单个 SQLAlchemy 版本中最广泛涵盖的更改之一,经过数月的重新组织和重构,从 Core 的基础到 ORM,现在允许大多数涉及从用户构造的语句生成 SQL 字符串和相关语句元数据的 Python 计算被缓存在内存中,因此对于相同的语句构造的后续调用将使用 35-60%更少的 CPU 资源。 这种缓存不仅限于构建 SQL 字符串,还包括构建将 SQL 结构链接到结果集的结果获取结构,以及在 ORM 中包括适应 ORM 启用的属性加载器、关系急加载器和其他选项,以及每次 ORM 查询试图运行并从结果集构建 ORM 对象时必须构建的对象构造例程。 为了介绍该功能的一般思想,给出了来自性能套件的代码,将调用一个非常简单的查询“n”次,其中 n 的默认值为 10000。查询仅返回一行,因为我们要减少的开销是**许多小查询**的开销。对于返回许多行的查询,优化并不那么显著: ```py session = Session(bind=engine) for id_ in random.sample(ids, n): result = session.query(Customer).filter(Customer.id == id_).one()
在 Dell XPS13 运行 Linux 的 SQLAlchemy 1.3 版本中,此示例完成如下:
test_orm_query : (10000 iterations); total time 3.440652 sec
在 1.4 版本中,上述代码不经修改即可完成:
test_orm_query : (10000 iterations); total time 2.367934 sec
这个第一个测试表明,当使用缓存时,常规的 ORM 查询可以在很多次迭代中以30% 更快的速度运行。
功能的第二个变体是可选使用 Python lambdas 来延迟查询本身的构建。这是一种更复杂的方法变体,类似于版本 1.0.0 中引入的“烘焙查询”扩展。 “lambda” 功能可以以非常类似于烘焙查询的方式使用,除了它可以以临时方式用于任何 SQL 结构之外。它还包括扫描每次 lambda 调用的功能,以查找每次调用都会更改的绑定文字值,以及对其他结构的更改,例如每次查询不同的实体或列,同时仍然不必每次都运行实际代码。
使用此 API 如下所示:
session = Session(bind=engine) for id_ in random.sample(ids, n): stmt = lambda_stmt(lambda: future_select(Customer)) stmt += lambda s: s.where(Customer.id == id_) session.execute(stmt).scalar_one()
上述代码完成:
test_orm_query_newstyle_w_lambdas : (10000 iterations); total time 1.247092 sec
此测试表明,使用较新的“select()”风格的 ORM 查询,与完全“烘焙”样式调用结合使用,后者缓存了整个构建过程,可以在很多次迭代中以60% 更快的速度运行,并且性能与被本地缓存系统取代的烘焙查询系统相当。
新系统利用现有的 Connection.execution_options.compiled_cache
执行选项,并直接向 Engine
添加缓存,该缓存使用 Engine.query_cache_size
参数进行配置。
API 和行为变化的重大部分是为了支持这一新功能而进行的。
另请参阅
SQL 编译缓存
#4639 #5380 #4645 #4808 #5004 ### 声明式现在与 ORM 集成,并带有新功能
大约十年左右的时间后,sqlalchemy.ext.declarative
包现在已集成到 sqlalchemy.orm
命名空间中,除了声明式“扩展”类仍然保持为声明式扩展之外。
sqlalchemy.orm
中新增的新类包括:
registry
- 一个新的类,取代了“声明基类”的角色,作为映射类的注册表,可以通过字符串名称在relationship()
调用中引用,并且不受任何特定类被映射的风格的影响。declarative_base()
- 这是在声明系统中一直在使用的相同声明基类,只是现在在内部引用了一个registry
对象,并由registry.generate_base()
方法实现,可以直接从registry
中调用。declarative_base()
函数会自动生成这个注册表,因此不会影响现有代码。sqlalchemy.ext.declarative.declarative_base
名称仍然存在,在启用 2.0 弃用模式时会发出 2.0 弃用警告。declared_attr()
- 现在是sqlalchemy.orm
的一部分的相同“声明属性”函数调用。sqlalchemy.ext.declarative.declared_attr
名称仍然存在,在启用 2.0 弃用模式时会发出 2.0 弃用警告。- 其他移入
sqlalchemy.orm
的名称包括has_inherited_table()
,synonym_for()
,DeclarativeMeta
,as_declarative()
。
另外,instrument_declarative()
函数已被弃用,被registry.map_declaratively()
取代。ConcreteBase
、AbstractConcreteBase
和DeferredReflection
类仍然作为声明性扩展包中的扩展。
映射样式现在已经组织起来,它们都从registry
对象扩展,并分为以下类别:
- 声明性映射
- 使用
declarative_base()
带有元类的基类
- 使用
mapped_column()
的声明性表 - 命令式表(又名“混合表”)
- 使用
registry.mapped()
声明性装饰器
- 声明性表
- 命令式表(混合)
- 将 ORM 映射应用于现有数据类(传统数据类用法)
- 命令式(又名“经典”映射)
- 使用
registry.map_imperatively()
- 使用命令式映射映射预先存在的数据类
现有的经典映射函数sqlalchemy.orm.mapper()
仍然存在,但直接调用sqlalchemy.orm.mapper()
已被弃用;新的registry.map_imperatively()
方法现在通过sqlalchemy.orm.registry()
路由请求,以便与其他声明性映射明确集成。
新方法与第三方类仪器系统互操作,这些系统必须在映射过程之前对类进行操作,允许声明性映射通过装饰器而不是声明性基类工作,以便像dataclasses和attrs这样的包可以与声明性映射一起使用,除了与经典映射一起使用。
声明性文档现已完全整合到 ORM 映射器配置文档中,并包括所有样式的映射示例,组织在一个地方。请查看新组织文档的开始部分 ORM 映射类概述。
另请参阅
ORM 映射类概述
Python Dataclasses, attrs 支持声明性,命令式映射
#5508 ### Python Dataclasses, attrs 支持声明性,命令式映射
除了在声明性现在与新功能整合到 ORM 中中引入的新声明性装饰器样式外,Mapper
现在明确意识到 Python 的dataclasses
模块,并将识别以这种方式配置的属性,并继续映射它们,而不像以前那样跳过它们。对于attrs
模块,attrs
已经从类中删除了自己的属性,因此已经与 SQLAlchemy 经典映射兼容。通过添加registry.mapped()
装饰器,两个属性系统现在可以与声明性映射互操作。
另请参阅
将 ORM 映射应用于现有数据类(传统数据类用法)
使用命令式映射映射预先存在的数据类
#5027 ### Core 和 ORM 的异步 IO 支持
SQLAlchemy 现在支持使用全新的asyncio
前端接口来支持 Python 的数据库驱动程序,用于Connection
的 Core 使用以及用于 ORM 使用的Session
,使用AsyncConnection
和AsyncSession
对象。
注意
初始的 SQLAlchemy 1.4 版本应该考虑新的 asyncio 功能是alpha 级别的。这是一种全新的东西,使用了一些以前不熟悉的编程技术。
初始支持的数据库 API 是用于 PostgreSQL 的 asyncpg asyncio 驱动程序。
SQLAlchemy 的内部特性完全集成了greenlet库,以便将 SQLAlchemy 内部的执行流程适应于将 asyncio 的await
关键字从数据库驱动器传播到端用户 API,该 API 具有async
方法。使用这种方法,asyncpg 驱动程序在 SQLAlchemy 自己的测试套件中完全可操作,并与大多数 psycopg2 特性兼容。这种方法经过了 greenlet 项目的开发人员的审查和改进,对此 SQLAlchemy 表示感激。
用户接口的async
API 本身集中于像AsyncEngine.connect()
和AsyncConnection.execute()
这样的 IO 导向方法。新的 Core 构造严格支持 2.0 样式的使用方式;这意味着所有语句必须在给定连接对象的情况下调用,即在这种情况下为AsyncConnection
。
在 ORM 中,支持 2.0 样式的查询执行,使用select()
构造与AsyncSession.execute()
结合;传统的Query
对象本身不受AsyncSession
类支持。
ORM 功能,如延迟加载相关属性以及过期属性的非过期化,在传统的 asyncio 编程模型中是被禁止的,因为它们表示将隐式运行的 IO 操作在 Python 的getattr()
操作的范围内。为了克服这一问题,传统 asyncio 应用程序应该适度利用 eager loading 技术,并放弃使用诸如 expire on commit 之类的特性,以便不需要这些加载。
对于选择与传统决裂的 asyncio 应用程序开发人员,新的 API 提供了一个严格可选的功能,使希望利用此类 ORM 功能的应用程序可以选择将与数据库相关的代码组织到函数中,然后使用 AsyncSession.run_sync()
方法在 greenlets 中运行。请参阅 Asyncio Integration 中的 greenlet_orm.py
示例以进行演示。
还提供了对异步游标的支持,使用新方法 AsyncConnection.stream()
和 AsyncSession.stream()
,支持一个新的 AsyncResult
对象,该对象本身提供了常见方法的可等待版本,如 AsyncResult.all()
和 AsyncResult.fetchmany()
。核心和 ORM 都与传统 SQLAlchemy 中使用“服务器端游标”的功能集成。
另请参见
异步 I/O (asyncio)
Asyncio Integration
#3414 ### 许多核心和 ORM 语句对象现在在编译阶段执行大部分构建和验证工作
1.4 系列的一个重要举措是接近核心 SQL 语句和 ORM 查询的模型,以实现高效、可缓存的语句创建和编译模型,其中编译步骤将被缓存,基于创建的语句对象生成的缓存键,该对象本身是为每次使用新创建的。为实现这一目标,特别是在构建语句时发生的大部分 Python 计算,特别是 ORM Query
和 select()
构造在用于调用 ORM 查询时,正在移至语句的编译阶段,该阶段仅在调用语句后发生,并且仅在语句的编译形式尚未被缓存时才会发生。
从最终用户的角度来看,这意味着基于传递给对象的参数可能引发的某些错误消息将不再立即引发,而是仅在首次调用语句时发生。这些条件始终是结构性的,而不是数据驱动的,因此不会由于缓存语句而错过此类条件的风险。
属于此类别的错误条件包括:
- 当构造
_selectable.CompoundSelect
(例如 UNION,EXCEPT 等)并且传递的 SELECT 语句列数不同时,现在会引发CompileError
;以前,在语句构造时会立即引发ArgumentError
。 - 当调用
Query.join()
时可能出现的各种错误条件将在语句编译时进行评估,而不是在首次调用方法时。
可能会发生变化的其他事情涉及直接Query
对象:
- 当调用
Query.statement
访问器时,行为可能会有所不同。返回的Select
对象现在是与Query
中存在的相同状态的直接副本,而不执行任何 ORM 特定的编译(这意味着速度大大提高)。然而,Select
将不会像 1.3 中那样具有相同的内部状态,包括如果在Query
中没有明确声明,则明确拼写出 FROM 子句等内容。这意味着依赖于操作此Select
语句的代码,例如调用Select.with_only_columns()
等方法,可能需要适应 FROM 子句。
另请参阅
透明 SQL 编译缓存添加到 Core,ORM 中的所有 DQL,DML 语句 ### 修复了内部导入约定,使代码检查工具可以正常工作
SQLAlchemy 长期以来一直使用参数注入装饰器来帮助解决相互依赖的模块导入,就像这样:
@util.dependency_for("sqlalchemy.sql.dml") def insert(self, dml, *args, **kw): ...
上述函数将被重写,不再在外部具有dml
参数。这会让代码检查工具看到函数缺少参数而感到困惑。已经内部实现了一种新方法,使函数签名不再被修改,模块对象在函数内部获取。
#4689 ### 支持 SQL 正则表达式操作符
期待已久的功能,为数据库正则表达式操作符提供了基本支持,以补充ColumnOperators.like()
和ColumnOperators.match()
操作套件。新功能包括实现类似正则表达式匹配的ColumnOperators.regexp_match()
函数,以及实现正则表达式字符串替换的ColumnOperators.regexp_replace()
函数。
支持的后端包括 SQLite、PostgreSQL、MySQL / MariaDB 和 Oracle。SQLite 后端仅支持“regexp_match”而不支持“regexp_replace”。
正则表达式语法和标志不是通用于所有后端。未来的功能将允许一次指定多个正则表达式语法,以便在不同后端之间动态切换。
对于 SQLite,Python 的re.search()
函数已被确定为实现,无需额外参数。
另请参阅
ColumnOperators.regexp_match()
ColumnOperators.regexp_replace()
正则表达式支持 - SQLite 实现注意事项
#1390 ### SQLAlchemy 2.0 弃用模式
1.4 版本的主要目标之一是提供一个“过渡”版本,以便应用程序可以逐渐迁移到 SQLAlchemy 2.0。为此,1.4 版本的一个主要特性是“2.0 弃用模式”,这是一系列针对每个可检测到的 API 模式发出的弃用警告,在版本 2.0 中将以不同方式工作。所有警告都使用RemovedIn20Warning
类。由于这些警告影响包括select()
和Engine
构造在内的基础模式,即使是简单的应用程序也可能生成大量警告,直到适当的 API 更改完成。因此,默认情况下警告模式是关闭的,直到开发人员启用环境变量SQLALCHEMY_WARN_20=1
。
要了解如何完整使用 2.0 弃用模式,请参阅迁移到 2.0 第二步 - 打开 RemovedIn20Warnings。
另请参见
SQLAlchemy 2.0 - 主要迁移指南
迁移到 2.0 第二步 - 打开 RemovedIn20Warnings ### Python 3.6 是最低 Python 3 版本;Python 2.7 仍受支持
由于 Python 3.5 在 2020 年 9 月已达到生命周期终点,SQLAlchemy 1.4 现在将版本 3.6 作为最低 Python 3 版本。Python 2.7 仍然受支持,但 SQLAlchemy 1.4 系列将是最后一个支持 Python 2 的系列。
ORM 查询在内部与 select、update、delete 统一;2.0 风格的执行可用
对于版本 2.0 的 SQLAlchemy 最大的概念性变化,实际上也是在 1.4 版本中,是 Core 中的Select
构造和 ORM 中的Query
对象之间的巨大分离已被移除,以及Query.update()
和Query.delete()
方法与Update
和Delete
的关系。
关于Select
和Query
,这两个对象在许多版本中具有类似的、大部分重叠的 API,甚至可以在两者之间切换,但在使用模式和行为上仍然有很大的不同。这背后的历史背景是,Query
对象是为了克服Select
对象的缺点而引入的,后者曾经是 ORM 对象查询的核心,但只能根据Table
元数据进行查询。然而,Query
只有一个简单的接口来加载对象,直到经过多个重大版本的发布,它最终才获得了大部分Select
对象的灵活性,这导致这两个对象变得非常相似,但仍然在很大程度上不兼容。
在 1.4 版本中,所有 Core 和 ORM SELECT 语句都直接从Select
对象渲染;当使用Query
对象时,在语句调用时,它会将其状态复制到一个Select
对象中,然后使用 2.0 风格执行内部调用。未来,Query
对象将仅作为传统遗留,应用程序将被鼓励转向 2.0 风格执行,允许 Core 构造自由地针对 ORM 实体使用:
with Session(engine, future=True) as sess: stmt = ( select(User) .where(User.name == "sandy") .join(User.addresses) .where(Address.email_address.like("%gmail%")) ) result = sess.execute(stmt) for user in result.scalars(): print(user)
关于上面的示例需要注意的事项:
Session
和sessionmaker
对象现在具有完整的上下文管理器(即with:
语句)功能;请参阅打开和关闭会话的修订文档以获取示例。- 在 1.4 系列中,所有 2.0 风格的 ORM 调用都使用了一个包含
Session
的标志设置为True
的标志;这个标志表示Session
应该具有 2.0 风格的行为,其中包括 ORM 查询可以从execute
中调用,以及一些事务特性的变化。在 2.0 版本中,这个标志将始终为True
。 select()
构造不再需要在列子句周围加括号;请参考 select(), case()现在接受位置表达式以了解此改进的背景。select()
/Select
对象具有一个Select.join()
方法,其行为类似于Query
的方法,甚至可以容纳 ORM 关系属性(而不会破坏 Core 和 ORM 之间的分离!)- 请参考 select().join()和 outerjoin()向当前查询添加 JOIN 条件,而不是创建子查询以了解此背景。- 与 ORM 实体一起工作并预计返回 ORM 结果的语句是使用
Session.execute()
来调用的。查看查询以获取入门指南。另请参阅 ORM Session.execute()在所有情况下使用“future”风格结果集中的以下注意事项。 - 返回一个
Result
对象,而不是一个普通列表,这本身是以前ResultProxy
对象的一个更复杂的版本;这个对象现在被用于 Core 和 ORM 结果。查看新的 Result 对象,RowProxy 不再是一个“代理”;现在被称为 Row 并且行为类似于增强的命名元组,以及 Query 返回的“KeyedTuple”对象被 Row 替换以获取更多信息。
在 SQLAlchemy 的文档中,将会有许多关于 1.x 风格和 2.0 风格执行的引用。这是为了区分两种查询风格,并尝试向前文档化新的调用风格。在 SQLAlchemy 2.0 中,虽然Query
对象可能仍然是一个遗留构造,但它将不再在大多数文档中出现。
对“批量更新和删除”进行了类似的调整,以便 Core update()
和delete()
可以用于批量操作。类似以下的批量更新:
session.query(User).filter(User.name == "sandy").update( {"password": "foobar"}, synchronize_session="fetch" )
现在可以以 2.0 风格实现(实际上上述在内部以这种方式运行)如下:
with Session(engine, future=True) as sess: stmt = ( update(User) .where(User.name == "sandy") .values(password="foobar") .execution_options(synchronize_session="fetch") ) sess.execute(stmt)
请注意使用Executable.execution_options()
方法传递 ORM 相关选项。现在“执行选项”的使用在 Core 和 ORM 中更加普遍,许多来自Query
的 ORM 相关方法现在被实现为执行选项(查看Query.execution_options()
以获取一些示例)。
另请参阅
SQLAlchemy 2.0 - 主要迁移指南
SqlAlchemy 2.0 中文文档(七十二)(3)https://developer.aliyun.com/article/1561024