原文:
docs.sqlalchemy.org/en/20/contents.html
SQLAlchemy 2.0 有哪些新功能?
原文:
docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html
读者注意事项
SQLAlchemy 2.0 的过渡文档分为 两个 文档 - 一个详细说明了从 1.x 到 2.x 系列的主要 API 转换,另一个详细说明了与 SQLAlchemy 1.4 相关的新功能和行为:
- SQLAlchemy 2.0 - Major Migration Guide - 1.x 到 2.x API 转换
- SQLAlchemy 2.0 有哪些新功能? - 本文档,SQLAlchemy 2.0 的新功能和行为
尚未将其 1.4 应用程序更新为遵循 SQLAlchemy 2.0 引擎和 ORM 约定的读者可以导航到 SQLAlchemy 2.0 - Major Migration Guide 了解确保 SQLAlchemy 2.0 兼容性的指南,这是在版本 2.0 下拥有可工作代码的先决条件。
关于本文档
本文描述了 SQLAlchemy 版本 1.4 与版本 2.0 之间的变化,与 1.x 风格和 2.0 风格的主要变化无关。读者应该从 SQLAlchemy 2.0 - Major Migration Guide 文档开始,以了解 1.x 和 2.x 系列之间的主要兼容性变化的整体图片。
除了主要的 1.x->2.x 迁移路径之外,SQLAlchemy 2.0 中下一个最大的范式转变是与PEP 484类型实践和当前能力的深度集成,特别是在 ORM 中。受 Python dataclasses启发的新型基于类型的 ORM 声明风格,以及与 dataclasses 本身的新集成,补充了一种不再需要存根并且在从 SQL 语句到结果集的类型感知方法链方面取得了很大进展的整体方法。
Python 类型的突出地位不仅仅在于使得诸如mypy之类的类型检查器可以无需插件而运行;更重要的是,它使得像vscode和pycharm这样的集成开发环境能够在辅助编写 SQLAlchemy 应用程序时发挥更加积极的作用。
Core 和 ORM 中的新类型支持 - 不再使用存根 / 扩展
与版本 1.4 中通过sqlalchemy2-stubs包提供的临时方法相比,Core 和 ORM 的类型化方法已经完全重新设计。新方法从 SQLAlchemy 中最基本的元素开始,即Column
,或者更准确地说是支撑所有具有类型的 SQL 表达式的ColumnElement
。然后,这种表达级别的类型化扩展到语句构造、语句执行和结果集,并最终扩展到 ORM,其中新的 declarative 形式允许完全类型化的 ORM 模型,从语句到结果集完全集成。
提示
对于 2.0 系列,类型化支持应该被视为beta 级别软件。类型化细节可能会更改,但不计划进行重大的不兼容性更改。
SQL 表达式/语句/结果集类型化
本节提供了关于 SQLAlchemy 新的 SQL 表达式类型化方法的背景和示例,该方法从基本的ColumnElement
构造扩展到 SQL 语句和结果集,以及 ORM 映射的领域。
理论基础和概述
提示
本节是一个架构讨论。跳转到 SQL Expression Typing - Examples 只需查看新类型化的外观。
在sqlalchemy2-stubs中,SQL 表达式被标记为泛型,然后引用一个TypeEngine
对象,比如Integer
、DateTime
或String
作为它们的泛型参数(例如Column[Integer]
)。这本身就是与原始的 Dropbox sqlalchemy-stubs包不同的地方,原始的包直接将Column
及其基本构造标记为 Python 类型的泛型,比如int
、datetime
和str
。人们希望由于Integer
/ DateTime
/ String
本身与int
/ datetime
/ str
泛型相关,会有方法来保持两个级别的信息,并能够通过中间构造TypeEngine
从列表达式中提取 Python 类型。然而,事实并非如此,因为PEP 484实际上没有足够丰富的功能集来使这成为可行的选择,缺乏诸如higher kinded TypeVars之类的功能。
因此,在对PEP 484当前功能进行深入评估之后,SQLAlchemy 2.0 认识到了在这个领域原始的sqlalchemy-stubs的智慧,并回归到了直接将列表达式链接到 Python 类型的做法。这意味着,如果有 SQL 表达式到不同子类型的情况,比如Column(VARCHAR)
与Column(Unicode)
,这两种String
子类型的具体细节不会被传递,因为类型只会传递str
,但在实践中,这通常不是一个问题,直接出现 Python 类型通常更有用,因为它代表了将要直接存储和接收的 Python 数据。
具体来说,这意味着像 Column('id', Integer)
这样的表达式被类型为 Column[int]
。这允许建立一个可行的 SQLAlchemy 构造 -> Python 数据类型的流水线,而无需使用类型插件。关键是,它允许完全与 ORM 的范式进行互操作,即使用引用 ORM 映射类类型的 select()
和 Row
构造(例如,包含用户映射实例的 Row
,例如在我们的教程中使用的 User
和 Address
示例)。虽然 Python 的类型当前对于元组类型的定制支持非常有限(其中 PEP 646,第一个试图处理类似元组的对象的 pep,在功能上受到故意限制,并且本身尚不适用于任意元组操作),但已经设计了一个相当不错的方法,允许基本的 select()
-> Result
-> Row
类型功能,包括用于 ORM 类的功能,在要将 Row
对象展开为单独的列条目时,会添加一个小的面向类型的访问器,允许各个 Python 值保持与它们来源于的 SQL 表达式相关联的 Python 类型(翻译:它可以工作)。
SQL 表达式类型 - 示例
类型行为简要介绍。评论指示在 vscode 中悬停在代码上会看到什么(或者在使用 reveal_type() 助手时,大致会显示什么类型工具):
分配给 SQL 表达式的简单 Python 类型
代码语言:javascript复制# (variable) str_col: ColumnClause[str]
str_col = column("a", String)
# (variable) int_col: ColumnClause[int]
int_col = column("a", Integer)
# (variable) expr1: ColumnElement[str]
expr1 = str_col "x"
# (variable) expr2: ColumnElement[int]
expr2 = int_col 10
# (variable) expr3: ColumnElement[bool]
expr3 = int_col == 15
分配给 select()
构造的单个 SQL 表达式,以及任何返回行的构造,包括返回行的 DML,例如带有 Insert
的 Insert.returning()
,都被打包成一个 Tuple[]
类型,其中保留了每个元素的 Python 类型。
# (variable) stmt: Select[Tuple[str, int]]
stmt = select(str_col, int_col)
# (variable) stmt: ReturningInsert[Tuple[str, int]]
ins_stmt = insert(table("t")).returning(str_col, int_col)
从任何返回行构造的Tuple[]
类型,在调用.execute()
方法时,传递到Result
和Row
。为了将Row
对象解包为元组,Row.tuple()
或Row.t
访问器基本上将Row
强制转换为相应的Tuple[]
(尽管在运行时仍然是相同的Row
对象)。
with engine.connect() as conn:
# (variable) stmt: Select[Tuple[str, int]]
stmt = select(str_col, int_col)
# (variable) result: Result[Tuple[str, int]]
result = conn.execute(stmt)
# (variable) row: Row[Tuple[str, int]] | None
row = result.first()
if row is not None:
# for typed tuple unpacking or indexed access,
# use row.tuple() or row.t (this is the small typing-oriented accessor)
strval, intval = row.t
# (variable) strval: str
strval
# (variable) intval: int
intval
对于单列语句的标量值,像Connection.scalar()
、Result.scalars()
等方法会做正确的事情。
# (variable) data: Sequence[str]
data = connection.execute(select(str_col)).scalars().all()
上述对于返回行构造的支持与 ORM 映射类一起运作最好,因为映射类可以为其成员列出具体类型。下面的示例设置了一个类,使用了新的类型感知语法,在下一节中描述:
代码语言:javascript复制from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
addresses: Mapped[List["Address"]] = relationship()
class Address(Base):
__tablename__ = "address"
id: Mapped[int] = mapped_column(primary_key=True)
email_address: Mapped[str]
user_id = mapped_column(ForeignKey("user_account.id"))
使用上述映射,属性从语句到结果集都有类型并自我表达:
代码语言:javascript复制with Session(engine) as session:
# (variable) stmt: Select[Tuple[int, str]]
stmt_1 = select(User.id, User.name)
# (variable) result_1: Result[Tuple[int, str]]
result_1 = session.execute(stmt_1)
# (variable) intval: int
# (variable) strval: str
intval, strval = result_1.one().t
映射类本身也是类型,并且表现得相同,例如针对两个映射类进行的 SELECT:
代码语言:javascript复制with Session(engine) as session:
# (variable) stmt: Select[Tuple[User, Address]]
stmt_2 = select(User, Address).join_from(User, Address)
# (variable) result_2: Result[Tuple[User, Address]]
result_2 = session.execute(stmt_2)
# (variable) user_obj: User
# (variable) address_obj: Address
user_obj, address_obj = result_2.one().t
在选择映射类时,像aliased
这样的构造也可以工作,保持原始映射类的列级属性以及语句预期的返回类型:
with Session(engine) as session:
# this is in fact an Annotated type, but typing tools don't
# generally display this
# (variable) u1: Type[User]
u1 = aliased(User)
# (variable) stmt: Select[Tuple[User, User, str]]
stmt = select(User, u1, User.name).filter(User.id == 5)
# (variable) result: Result[Tuple[User, User, str]]
result = session.execute(stmt)
Core Table 还没有一个良好的方法来在通过Table.c
访问它们时维护Column
对象的类型。
由于Table
设置为类的一个实例,而Table.c
访问器通常通过名称动态访问Column
对象,因此尚未建立针对此的已知类型方法;需要一些替代语法。
ORM 类、标量等都很好用。
典型的 ORM 类选择用例,作为标量或元组,所有工作都可以,无论是 2.0 还是 1.x 风格的查询,都可以得到精确的类型,要么是自身,要么包含在适当的容器内,如 Sequence[]
、List[]
或 Iterator[]
:
# (variable) users1: Sequence[User]
users1 = session.scalars(select(User)).all()
# (variable) user: User
user = session.query(User).one()
# (variable) user_iter: Iterator[User]
user_iter = iter(session.scalars(select(User)))
旧版 Query
也获得了元组类型。
对于 Query
的类型支持远远超出了 sqlalchemy-stubs 或 sqlalchemy2-stubs 提供的范围,其中标量对象和元组类型的 Query
对象在大多数情况下都会保留结果级别的类型:
# (variable) q1: RowReturningQuery[Tuple[int, str]]
q1 = session.query(User.id, User.name)
# (variable) rows: List[Row[Tuple[int, str]]]
rows = q1.all()
# (variable) q2: Query[User]
q2 = session.query(User)
# (variable) users: List[User]
users = q2.all()
陷阱 - 所有存根必须被卸载
类型支持的一个关键注意事项是,所有 SQLAlchemy 存根包都必须被卸载 才能使类型化工作。当针对 Python 虚拟环境运行 mypy 时,只需卸载这些包即可。但是,目前 typeshed 中也包含 SQLAlchemy 存根包,typeshed 本身被捆绑到一些类型工具中,如 Pylance,因此在某些情况下,可能需要定位这些包的文件并删除它们,以确保新的类型化能够正确工作。
一旦 SQLAlchemy 2.0 发布为最终状态,typeshed 将从其自己的存根源中删除 SQLAlchemy。
ORM 声明性模型
SQLAlchemy 1.4 引入了第一个使用 sqlalchemy2-stubs 和 Mypy Plugin 的 SQLAlchemy 本机 ORM 类型支持的方法。在 SQLAlchemy 2.0 中,Mypy 插件 仍然可用,并已更新以与 SQLAlchemy 2.0 的类型系统一起工作。但是,现在应该将其视为已弃用,因为应用程序现在有了采用新的不使用插件或存根的类型支持的简单路径。
概览
新系统的基本方法是,当使用完全声明性模型(即不使用混合声明性或命令式配置,这些配置保持不变)时,映射列声明首先通过检查每个属性声明左侧的类型注释(如果存在)在运行时派生。左手类型注释应包含在Mapped
泛型类型中,否则不认为属性是映射属性。然后,属性声明可以引用右手边的mapped_column()
构造,该构造用于提供有关要生成和映射的Column
的附加 Core 级架构信息。如果左侧存在Mapped
注释,则此右侧声明是可选的;如果左侧没有注释,则mapped_column()
可用作Column
指令的精确替代,在这种情况下,它将提供更准确(但不精确)的属性类型行为,即使没有注释也是如此。
这种方法的灵感来自于 Python 的 dataclasses 方法,该方法从左侧开始注释,然后允许在右侧进行可选的 dataclasses.field()
规范;与 dataclasses 方法的关键区别在于 SQLAlchemy 的方法严格地 选择加入,其中使用 Column
而没有任何类型注释的现有映射继续按照其原来的方式工作,而且 mapped_column()
构造可以直接替代 Column
而不需要任何显式类型注释。只有在需要精确的属性级 Python 类型时才需要使用 Mapped
进行显式注释。这些注释可以根据需要在每个属性上使用,对于那些具体类型有帮助的属性;使用 mapped_column()
的非注释属性将在实例级别被标记为 Any
。
迁移现有映射
迁移到新的 ORM 方法开始时更加冗长,但随着可用的新功能的充分使用,变得比以前更加简洁。以下步骤详细说明了一个典型的过渡,然后继续说明了一些更多的选项。
第一步 - declarative_base()
被 DeclarativeBase
取代。
在 Python 类型中观察到的一个限制是似乎没有能力从函数动态生成一个类,然后这个类被理解为新类的基础。为了解决这个问题而不使用插件,通常对 declarative_base()
的调用可以被替换为使用 DeclarativeBase
类,该类产生与通常相同的 Base
对象,但是类型工具理解它:
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
第二步 - 用 mapped_column()
替换 Declarative 中的 Column
mapped_column()
是一个 ORM 类型感知的构造,可以直接替换为Column
的使用。给定一个 1.x 风格的映射如下:
from sqlalchemy import Column
from sqlalchemy.orm import relationship
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "user_account"
id = Column(Integer, primary_key=True)
name = Column(String(30), nullable=False)
fullname = Column(String)
addresses = relationship("Address", back_populates="user")
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
email_address = Column(String, nullable=False)
user_id = Column(ForeignKey("user_account.id"), nullable=False)
user = relationship("User", back_populates="addresses")
我们用mapped_column()
替换Column
; 不需要更改任何参数:
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "user_account"
id = mapped_column(Integer, primary_key=True)
name = mapped_column(String(30), nullable=False)
fullname = mapped_column(String)
addresses = relationship("Address", back_populates="user")
class Address(Base):
__tablename__ = "address"
id = mapped_column(Integer, primary_key=True)
email_address = mapped_column(String, nullable=False)
user_id = mapped_column(ForeignKey("user_account.id"), nullable=False)
user = relationship("User", back_populates="addresses")
上面的单独列 尚未使用 Python 类型进行类型化,而是被类型化为Mapped[Any]
;这是因为我们可以声明任何列为Optional
或者不声明,而且没有办法在我们明确地对其进行类型化时有一个“猜测”。
但是,在这一步,我们上面的映射已经为所有属性设置了适当的描述符类型,并且可以用于查询以及实例级别的操作,所有这些操作都将以不使用插件的 mypy –strict 模式通过。
第三步 - 使用Mapped
根据需要应用确切的 Python 类型。
这可以用于所有需要确切类型的属性;那些可以留作Any
的属性可以跳过。为了上下文,我们还说明了在一个relationship()
中应用确切类型时如何使用Mapped
。这个中间步骤中的映射会更冗长,但是熟练之后,这一步可以与后续步骤结合起来更直接地更新映射:
from typing import List
from typing import Optional
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(30), nullable=False)
fullname: Mapped[Optional[str]] = mapped_column(String)
addresses: Mapped[List["Address"]] = relationship("Address", back_populates="user")
class Address(Base):
__tablename__ = "address"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
email_address: Mapped[str] = mapped_column(String, nullable=False)
user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"), nullable=False)
user: Mapped["User"] = relationship("User", back_populates="addresses")
在这一点上,我们的 ORM 映射是完全类型化的,并将生成精确类型的select()
、Query
和Result
构造。我们现在可以继续简化映射声明中的冗余部分。
第四步 - 删除不再需要的mapped_column()
指令
所有的nullable
参数都可以使用Optional[]
隐含;在没有Optional[]
的情况下,nullable
默认为False
。所有没有参数的 SQL 类型,如Integer
和String
,可以单独表示为 Python 注释。不带参数的mapped_column()
指令可以完全移除。relationship()
现在从左侧注释派生其类,还支持前向引用(正如relationship()
已经支持了十年的字符串型前向引用一样