SqlAlchemy 2.0 中文文档(七十八)(1)https://developer.aliyun.com/article/1560950
新的核心功能
完全可扩展,核心中支持类型级别的操作符
到目前为止,Core 从未有过任何系统来为 Column 和其他表达式构造添加对新 SQL 操作符的支持,除了ColumnOperators.op() 方法,这个方法“刚好”能使事情正常工作。此外,Core 中也从未存在过任何允许覆盖现有操作符行为的系统。直到现在,操作符能够灵活重新定义的唯一方式是在 ORM 层,使用给定 comparator_factory 参数的 column_property()。因此,像 GeoAlchemy 这样的第三方库被迫以 ORM 为中心,并依赖于一系列的黑客技巧来应用新的操作以及正确地传播它们。
Core 中的新操作符系统增加了一直缺失的一个关键点,即将新的和被覆盖的操作符与 类型 关联起来。毕竟,真正驱动操作存在的不是列、CAST 操作符或 SQL 函数,而是表达式的 类型。实现细节很少——只需向核心 ColumnElement 类型添加几个额外的方法,以便它向其 TypeEngine 对象咨询可选的一组操作符。新的或修订过的操作可以与任何类型关联,可以通过对现有类型进行子类化、使用 TypeDecorator,或者通过将新的 Comparator 对象附加到现有类型类来“全面覆盖”地关联。
例如,要为 Numeric 类型添加对数支持:
from sqlalchemy.types import Numeric from sqlalchemy.sql import func class CustomNumeric(Numeric): class comparator_factory(Numeric.Comparator): def log(self, other): return func.log(self.expr, other)
新类型可以像任何其他类型一样使用:
data = Table( "data", metadata, Column("id", Integer, primary_key=True), Column("x", CustomNumeric(10, 5)), Column("y", CustomNumeric(10, 5)), ) stmt = select([data.c.x.log(data.c.y)]).where(data.c.x.log(2) < value) print(conn.execute(stmt).fetchall())
从这里产生的新功能包括对 PostgreSQL 的 HSTORE 类型的支持,以及与 PostgreSQL 的 ARRAY 类型相关的新操作。它还为现有类型铺平了道路,使其能够获取更多特定于这些类型的运算符,例如更多的字符串、整数和日期运算符。
另请参阅
重新定义和创建新的操作符
HSTORE
对插入的多值支持
Insert.values() 方法现在支持字典列表,将呈现多 VALUES 语句,如 VALUES (), (), ...。这仅适用于支持此语法的后端,包括 PostgreSQL、SQLite 和 MySQL。这与通常的 executemany() 样式的 INSERT 不同:
users.insert().values( [ {"name": "some name"}, {"name": "some other name"}, {"name": "yet another name"}, ] )
另请参阅
Insert.values()
类型表达式
现在可以将 SQL 表达式与类型关联起来。从历史上看,TypeEngine 一直允许 Python 端函数接收绑定参数和结果行值,通过 Python 端转换函数来回传递到/从数据库。新功能允许类似的功能,但在数据库端实现:
from sqlalchemy.types import String from sqlalchemy import func, Table, Column, MetaData class LowerString(String): def bind_expression(self, bindvalue): return func.lower(bindvalue) def column_expression(self, col): return func.lower(col) metadata = MetaData() test_table = Table("test_table", metadata, Column("data", LowerString))
在上面的例子中,LowerString 类型定义了一个 SQL 表达式,每当 test_table.c.data 列在 SELECT 语句的列子句中呈现时,该表达式将被发出:
>>> print(select([test_table]).where(test_table.c.data == "HI")) SELECT lower(test_table.data) AS data FROM test_table WHERE test_table.data = lower(:data_1)
这一功能也被新版的 GeoAlchemy 大量使用,可以根据类型规则在 SQL 中内联嵌入 PostGIS 表达式。
另请参阅
应用 SQL 级别的绑定/结果处理
核心检查系统
New Class/Object Inspection System 中引入的 inspect() 函数也适用于核心。应用于一个 Engine 会产生一个 Inspector 对象:
from sqlalchemy import inspect from sqlalchemy import create_engine engine = create_engine("postgresql://scott:tiger@localhost/test") insp = inspect(engine) print(insp.get_table_names())
它也可以应用于任何 ClauseElement,它返回 ClauseElement 本身,比如 Table,Column,Select 等。这使得它可以在核心和 ORM 构造之间流畅工作。
新方法 Select.correlate_except()
select() 现在有一个方法 Select.correlate_except(),指定“除了指定的所有 FROM 子句之外的相关性”。它可用于映射场景,其中相关子查询应该正常关联,除了针对特定目标可选择的情况:
class SnortEvent(Base): __tablename__ = "event" id = Column(Integer, primary_key=True) signature = Column(Integer, ForeignKey("signature.id")) signatures = relationship("Signature", lazy=False) class Signature(Base): __tablename__ = "signature" id = Column(Integer, primary_key=True) sig_count = column_property( select([func.count("*")]) .where(SnortEvent.signature == id) .correlate_except(SnortEvent) )
另请参阅
Select.correlate_except()
PostgreSQL HSTORE 类型
PostgreSQL 的HSTORE类型的支持现在可用作为HSTORE。此类型充分利用了新的运算符系统,为 HSTORE 类型提供了一整套运算符,包括索引访问、连接和包含方法,如comparator_factory.has_key()、comparator_factory.has_any()和comparator_factory.matrix():
from sqlalchemy.dialects.postgresql import HSTORE data = Table( "data_table", metadata, Column("id", Integer, primary_key=True), Column("hstore_data", HSTORE), ) engine.execute(select([data.c.hstore_data["some_key"]])).scalar() engine.execute(select([data.c.hstore_data.matrix()])).scalar()
另请参阅
HSTORE
hstore
增强的 PostgreSQL ARRAY 类型
ARRAY 类型将接受一个可选的“维度”参数,将其固定到一个固定数量的维度,并在检索结果时大大提高效率:
# old way, still works since PG supports N-dimensions per row: Column("my_array", postgresql.ARRAY(Integer)) # new way, will render ARRAY with correct number of [] in DDL, # will process binds and results more efficiently as we don't need # to guess how many levels deep to go Column("my_array", postgresql.ARRAY(Integer, dimensions=2))
该类型还引入了新的运算符,使用新的类型特定运算符框架。新操作包括索引访问:
result = conn.execute(select([mytable.c.arraycol[2]]))
切片访问在 SELECT 中:
result = conn.execute(select([mytable.c.arraycol[2:4]]))
切片更新在 UPDATE 中:
conn.execute(mytable.update().values({mytable.c.arraycol[2:3]: [7, 8]}))
独立的数组文字:
>>> from sqlalchemy.dialects import postgresql >>> conn.scalar(select([postgresql.array([1, 2]) + postgresql.array([3, 4, 5])])) [1, 2, 3, 4, 5]
数组连接,在下面,右侧的[4, 5, 6] 被强制转换为数组文字:
select([mytable.c.arraycol + [4, 5, 6]])
另请参阅
ARRAY
array
新的、可配置的 DATE、TIME 类型用于 SQLite
SQLite 没有内置的 DATE、TIME 或 DATETIME 类型,而是提供了一些支持将日期和时间值存储为字符串或整数的方法。SQLite 的日期和时间类型在 0.8 中得到了增强,可以更加灵活地配置特定格式,包括“微秒”部分是可选的,以及几乎所有其他内容。
Column("sometimestamp", sqlite.DATETIME(truncate_microseconds=True)) Column( "sometimestamp", sqlite.DATETIME( storage_format=( "%(year)04d%(month)02d%(day)02d" "%(hour)02d%(minute)02d%(second)02d%(microsecond)06d" ), regexp="(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{6})", ), ) Column( "somedate", sqlite.DATE( storage_format="%(month)02d/%(day)02d/%(year)04d", regexp="(?P<month>\d+)/(?P<day>\d+)/(?P<year>\d+)", ), )
非常感谢 Nate Dub 在 Pycon 2012 上的努力。
另请参阅
DATETIME
DATE
TIME
“COLLATE”在所有方言中都受支持;特别是 MySQL、PostgreSQL、SQLite
“collate”关键字,长期被 MySQL 方言接受,现在已经在所有String类型上建立,并且将在任何后端渲染,包括在使用MetaData.create_all()和cast()等功能时:
>>> stmt = select([cast(sometable.c.somechar, String(20, collation="utf8"))]) >>> print(stmt) SELECT CAST(sometable.somechar AS VARCHAR(20) COLLATE "utf8") AS anon_1 FROM sometable
另请参阅
String
现在支持“前缀”用于update()、delete()
面向 MySQL,一个“前缀”可以在任何这些结构中渲染。例如:
stmt = table.delete().prefix_with("LOW_PRIORITY", dialect="mysql") stmt = table.update().prefix_with("LOW_PRIORITY", dialect="mysql")
该方法是新增的,除了已经存在于insert()、select()和Query上的方法之外。
另请参阅
Update.prefix_with()
Delete.prefix_with()
Insert.prefix_with()
Select.prefix_with()
Query.prefix_with()
行为变更
将“待定”对象视为“孤立”已经更加积极
这是对 0.8 系列的一个晚期补充,但希望新行为在更广泛的情况下更一致和直观。ORM 自至少版本 0.4 以来就包含了这样的行为,即一个“挂起”的对象,意味着它与Session相关联,但尚未插入数据库,当它变成“孤儿”时,即已与引用它的父对象解除关联,并且在配置的relationship()上指定了delete-orphan级联时,将自动从Session中清除。这种行为旨在大致模拟持久对象(即已插入)的行为,ORM 将根据分离事件的拦截发出 DELETE 来删除成为孤儿的对象。
行为变更适用于被多种父对象引用并且每个父对象都指定了delete-orphan的对象;典型示例是在多对多模式中桥接两种其他对象的关联对象。以前,行为是这样的,即挂起对象仅在与所有父对象解除关联时才会被清除。随着行为的变更,只要挂起对象与先前相关联的任何父对象解除关联,它就会被清除。这种行为旨在更接近持久对象的行为,即只要它们与任何父对象解除关联,它们就会被删除。
较旧行为的基本原因可以追溯至至少版本 0.4,基本上是一种防御性决定,试图在对象仍在构建 INSERT 时减轻混淆。但事实是,无论如何,一旦对象附加到任何新父对象,它就会重新与Session关联。
仍然可以刷新一个对象,即使它没有与所有必需的父对象关联,如果该对象一开始就没有与这些父对象关联,或者如果它被清除,但随后通过后续的附加事件重新与Session关联,但仍未完全关联。在这种情况下,预计数据库会发出完整性错误,因为可能存在未填充的 NOT NULL 外键列。ORM 决定让这些 INSERT 尝试发生,基于这样的判断:一个只与其必需的父对象部分关联但已经积极地与其中一些关联的对象,更多的情况下是用户错误,而不是应该被默默跳过的有意遗漏 - 在这里默默跳过 INSERT 会使这种用户错误非常难以调试。
对于可能依赖于旧行为的应用程序,可以通过将标志legacy_is_orphan作为映射器选项指定来重新启用旧行为。
新行为允许以下测试用例正常工作:
from sqlalchemy import Column, Integer, String, ForeignKey from sqlalchemy.orm import relationship, backref from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class User(Base): __tablename__ = "user" id = Column(Integer, primary_key=True) name = Column(String(64)) class UserKeyword(Base): __tablename__ = "user_keyword" user_id = Column(Integer, ForeignKey("user.id"), primary_key=True) keyword_id = Column(Integer, ForeignKey("keyword.id"), primary_key=True) user = relationship( User, backref=backref("user_keywords", cascade="all, delete-orphan") ) keyword = relationship( "Keyword", backref=backref("user_keywords", cascade="all, delete-orphan") ) # uncomment this to enable the old behavior # __mapper_args__ = {"legacy_is_orphan": True} class Keyword(Base): __tablename__ = "keyword" id = Column(Integer, primary_key=True) keyword = Column("keyword", String(64)) from sqlalchemy import create_engine from sqlalchemy.orm import Session # note we're using PostgreSQL to ensure that referential integrity # is enforced, for demonstration purposes. e = create_engine("postgresql://scott:tiger@localhost/test", echo=True) Base.metadata.drop_all(e) Base.metadata.create_all(e) session = Session(e) u1 = User(name="u1") k1 = Keyword(keyword="k1") session.add_all([u1, k1]) uk1 = UserKeyword(keyword=k1, user=u1) # previously, if session.flush() were called here, # this operation would succeed, but if session.flush() # were not called here, the operation fails with an # integrity error. # session.flush() del u1.user_keywords[0] session.commit()
after_attach 事件在项目与会话关联之后触发,而不是之前;before_attach 添加
使用after_attach的事件处理程序现在可以假定给定实例与给定会话关联:
@event.listens_for(Session, "after_attach") def after_attach(session, instance): assert instance in session
有些用例要求以这种方式工作。然而,其他用例要求项目尚未成为会话的一部分,比如当一个查询,旨在加载实例所需的某些状态,首先发出自动刷新,否则会过早刷新目标对象。这些用例应该使用新的“before_attach”事件:
@event.listens_for(Session, "before_attach") def before_attach(session, instance): instance.some_necessary_attribute = ( session.query(Widget).filter_by(instance.widget_name).first() )
查询现在像select()一样自动关联
以前需要调用Query.correlate()才能使列或 WHERE 子查询与父级关联:
subq = ( session.query(Entity.value) .filter(Entity.id == Parent.entity_id) .correlate(Parent) .as_scalar() ) session.query(Parent).filter(subq == "some value")
这与普通的select()构造相反,后者默认情况下会假定自动关联。在 0.8 中,上述语句将自动关联:
subq = session.query(Entity.value).filter(Entity.id == Parent.entity_id).as_scalar() session.query(Parent).filter(subq == "some value")
就像在select()中一样,可以通过调用query.correlate(None)来禁用关联,或者通过传递一个实体来手动设置关联,query.correlate(someentity)。
关联现在始终是上下文特定的
为了允许更广泛的相关性场景,Select.correlate() 和 Query.correlate() 的行为略有改变,以便 SELECT 语句仅在实际上下文中使用时才从 FROM 子句中省略“相关”的目标。此外,不再可能让作为外部 SELECT 语句中的 FROM 的 SELECT 语句“相关”(即省略)FROM 子句。
这个改变只会在渲染 SQL 方面变得更好,因为不再可能渲染出不合法的 SQL,其中所选内容相对于所选的 FROM 对象不足:
from sqlalchemy.sql import table, column, select t1 = table("t1", column("x")) t2 = table("t2", column("y")) s = select([t1, t2]).correlate(t1) print(s)
在这个改变之前,上述内容将返回:
SELECT t1.x, t2.y FROM t2
这是无效的 SQL,因为“t1”在任何 FROM 子句中都没有被引用。
现在,在没有外部 SELECT 的情况下,它将返回:
SELECT t1.x, t2.y FROM t1, t2
在 SELECT 中,相关性会如预期地生效:
s2 = select([t1, t2]).where(t1.c.x == t2.c.y).where(t1.c.x == s) print(s2)
SELECT t1.x, t2.y FROM t1, t2 WHERE t1.x = t2.y AND t1.x = (SELECT t1.x, t2.y FROM t2)
这个改变不会影响任何现有应用程序,因为对于正确构建的表达式,相关性行为保持不变。只有依赖于在非相关上下文中使用相关 SELECT 的无效字符串输出的应用程序(很可能是在测试场景中),才会看到任何变化。
#2668 ### create_all() 和 drop_all() 现在将空列表视为如此
方法 MetaData.create_all() 和 MetaData.drop_all() 现在将接受一个空的 Table 对象列表,并且不会发出任何 CREATE 或 DROP 语句。以前,空列表被解释为与传递 None 相同,对所有项目都会无条件发出 CREATE/DROP。
这是一个错误修复,但一些应用程序可能一直依赖于先前的行为。
修复了 InstrumentationEvents 的事件目标定位
InstrumentationEvents系列事件目标已经记录,事件将仅根据传递的实际类别触发。直到 0.7 版本,这并不是这种情况,应用于InstrumentationEvents的任何事件监听器都将为所有映射的类调用。在 0.8 中,添加了额外的逻辑,使事件仅对发送的那些类调用。这里的propagate标志默认设置为True,因为类仪器事件通常用于拦截尚未创建的类。
不再将“=”自动转换为 IN,当与 MS-SQL 中的子查询进行比较时
我们在 MSSQL 方言中发现了一个非常古老的行为,当用户尝试执行类似以下操作时,它会试图拯救用户:
scalar_subq = select([someothertable.c.id]).where(someothertable.c.data == "foo") select([sometable]).where(sometable.c.id == scalar_subq)
SQL Server 不允许将相等比较与标量 SELECT 进行比较,即,“x = (SELECT something)”。 MSSQL 方言会将其转换为 IN。然而,当进行类似“(SELECT something) = x”的比较时,也会发生同样的情况,总体上,这种猜测的水平超出了 SQLAlchemy 通常的范围,因此这种行为被移除。
修复了Session.is_modified()的行为
Session.is_modified()方法接受一个参数passive,基本上不应该是必要的,所有情况下该参数的值应为True - 当保持默认值False时,它会导致命中数据库,并经常触发自动刷新,这将改变结果。在 0.8 中,passive参数将不起作用,并且未加载的属性永远不会被检查历史记录,因为根据定义,未加载的属性上不会有待处理的状态更改。
另请参阅
Session.is_modified()
Column.key在Select.c属性中受到Select.apply_labels()的尊重
表达式系统的用户知道Select.apply_labels()会在每个列名前面添加表名,影响从Select.c中可用的名称:
s = select([table1]).apply_labels() s.c.table1_col1 s.c.table1_col2
在 0.8 版本之前,如果Column的Column.key不同,这个键会被忽略,与未使用Select.apply_labels()时不一致:
# before 0.8 table1 = Table("t1", metadata, Column("col1", Integer, key="column_one")) s = select([table1]) s.c.column_one # would be accessible like this s.c.col1 # would raise AttributeError s = select([table1]).apply_labels() s.c.table1_column_one # would raise AttributeError s.c.table1_col1 # would be accessible like this
在 0.8 版本中,Column.key在两种情况下都受到尊重:
# with 0.8 table1 = Table("t1", metadata, Column("col1", Integer, key="column_one")) s = select([table1]) s.c.column_one # works s.c.col1 # AttributeError s = select([table1]).apply_labels() s.c.table1_column_one # works s.c.table1_col1 # AttributeError
关于“name”和“key”的所有其他行为都是相同的,包括渲染的 SQL 仍然使用形式_ - 这里的重点是防止Column.key内容被渲染到SELECT语句中,以便在Column.key中使用特殊/非 ASCII 字符时不会出现问题。
single_parent警告现在变成了错误
一个relationship(),它是多对一或多对多关系,并指定“cascade=‘all, delete-orphan’”,这是一个尴尬但仍然支持的用例(带有限制),如果关系没有指定single_parent=True选项,现在将引发错误。以前只会发出警告,但在任何情况下几乎立即会在属性系统中跟随失败。
添加inspector参数到column_reflect事件
0.7 版本添加了一个名为column_reflect的新事件,提供了每个列反射时可以增强的机会。我们在这个事件上稍微出了点错,因为事件没有提供获取当前用于反射的Inspector和Connection的方法,以防需要来自数据库的额外信息。由于这是一个尚未广泛使用的新事件,我们将直接在其中添加inspector参数:
@event.listens_for(Table, "column_reflect") def listen_for_col(inspector, table, column_info): ...
禁用 MySQL 的自动检测排序规则和大小写敏感性
MySQL 方言进行两次调用,其中一次非常昂贵,从数据库加载所有可能的排序规则以及大小写信息,第一次Engine连接时。这两个集合都不会用于任何 SQLAlchemy 函数,因此这些调用将不再自动发出。可能依赖于这些集合存在于engine.dialect上的应用程序将需要直接调用_detect_collations()和_detect_casing()。
“未使用的列名”警告变成异常
在insert()或update()构造中引用不存在的列将引发错误而不是警告:
t1 = table("t1", column("x")) t1.insert().values(x=5, z=5) # raises "Unconsumed column names: z"
Inspector.get_primary_keys()已被弃用,请使用 Inspector.get_pk_constraint
Inspector上的这两种方法是多余的,其中get_primary_keys()将返回与get_pk_constraint()相同的信息,减去约束的名称:
>>> insp.get_primary_keys() ["a", "b"] >>> insp.get_pk_constraint() {"name":"pk_constraint", "constrained_columns":["a", "b"]}
在大多数情况下,不区分大小写的结果行名称将被禁用
一个非常古老的行为,在RowProxy中的列名始终是不区分大小写比较的:
>>> row = result.fetchone() >>> row["foo"] == row["FOO"] == row["Foo"] True
这是为了一些早期需要这样做的方言的好处,比如 Oracle 和 Firebird,但在现代用法中,我们有更准确的方法来处理这两个平台的不区分大小写行为。
未来,这种行为将仅可选地通过将标志case_sensitive=False传递给create_engine()来使用,但否则从行中请求的列名必须匹配大小写。
InstrumentationManager和替代类仪器现在是一个扩展
sqlalchemy.orm.interfaces.InstrumentationManager类已移动到sqlalchemy.ext.instrumentation.InstrumentationManager。 “替代仪器”系统是为了极少数需要使用现有或不寻常的类仪器系统的安装而构建的,并且通常很少使用。这个系统的复杂性已经导出到一个ext.模块中。它保持未使用,直到被导入一次,通常是当第三方库导入InstrumentationManager时,此时它通过用ExtendedInstrumentationRegistry替换默认的InstrumentationFactory注入回sqlalchemy.orm。
已移除
SQLSoup
SQLSoup 是一个方便的包,它在 SQLAlchemy ORM 的基础上提供了一个替代接口。SQLSoup 现在已经移动到自己的项目中,并且有单独的文档/发布;请参见bitbucket.org/zzzeek/sqlsoup。
SQLSoup 是一个非常简单的工具,也可以受益于对其使用方式感兴趣的贡献者。
MutableType
SQLAlchemy ORM 中的旧“可变”系统已被移除。这指的是应用于诸如PickleType的类型和有条件地应用于TypeDecorator的MutableType接口,并且自早期的 SQLAlchemy 版本以来一直提供了一种让 ORM 检测所谓的“可变”数据结构(如 JSON 结构和 pickled 对象)变化的方式。然而,实现从未合理,并迫使在单位操作期间发生昂贵的对象扫描的 ORM 使用方式。在 0.7 中,引入了sqlalchemy.ext.mutable扩展,以便用户定义的数据类型可以在发生更改时适当地向单位操作发送事件。
如今,MutableType 的使用预计会很少,因为多年来一直有关于其效率低下的警告。
sqlalchemy.exceptions(多年来一直是 sqlalchemy.exc)
我们曾留下了一个别名 sqlalchemy.exceptions,以使一些尚未升级以使用 sqlalchemy.exc 的非常老的库稍微容易一些。然而,一些用户仍然感到困惑,因此在 0.8 版本中我们将其完全删除,以消除任何困惑。
介绍
本指南介绍了 SQLAlchemy 0.8 版本的新功能,还记录了影响用户将其应用程序从 SQLAlchemy 0.7 系列迁移到 0.8 版本的更改。
SQLAlchemy 的发布版本即将接近 1.0,自 0.5 版本以来,每个新版本都减少了主要的使用变化。大多数已经适应现代 0.7 模式的应用程序应该可以无需更改地迁移到 0.8 版本。使用 0.6 甚至 0.5 模式的应用程序也应该可以直接迁移到 0.8 版本,尽管较大的应用程序可能需要测试每个中间版本。
平台支持
现在的目标是 Python 2.5 及以上版本
SQLAlchemy 0.8 将以 Python 2.5 为目标版本;不再兼容 Python 2.4。
内部将能够使用 Python 三元表达式(即,x if y else z),这将改善与使用 y and x or z 相比的情况,后者自然地导致了一些错误,以及上下文管理器(即,with:)和在某些情况下 try:/except:/else: 块,这将有助于提高代码的可读性。
SQLAlchemy 最终也会放弃对 2.5 版本的支持 - 当基线达到 2.6 时,SQLAlchemy 将转向使用 2.6/3.3 的就地兼容性,去除 2to3 工具的使用,并保持一个同时适用于 Python 2 和 3 的源代码库。
现在的目标是 Python 2.5 及以上版本
SQLAlchemy 0.8 将以 Python 2.5 为目标版本;不再兼容 Python 2.4。
内部将能够使用 Python 三元表达式(即,x if y else z),这将改善与使用 y and x or z 相比的情况,后者自然地导致了一些错误,以及上下文管理器(即,with:)和在某些情况下 try:/except:/else: 块,这将有助于提高代码的可读性。
SQLAlchemy 最终也会放弃对 2.5 版本的支持 - 当基线达到 2.6 时,SQLAlchemy 将转向使用 2.6/3.3 的就地兼容性,去除 2to3 工具的使用,并保持一个同时适用于 Python 2 和 3 的源代码库。
SqlAlchemy 2.0 中文文档(七十八)(3)https://developer.aliyun.com/article/1560963