SqlAlchemy 2.0 中文文档(五十七)(5)https://developer.aliyun.com/article/1563167
第五步 - 利用 pep-593 Annotated
将常见指令打包成类型
这是一个激进的新功能,提供了一个替代方案,或者说是补充性方法,用于提供类型定向配置,还可以在大多数情况下替代declared_attr
修饰的函数的需求。
首先,Declarative 映射允许将 Python 类型映射到 SQL 类型,例如str
到String
的自定义,使用registry.type_annotation_map
。使用PEP 593 Annotated
允许我们创建特定 Python 类型的变体,以便使用相同的类型,例如str
,为其提供String
的变体,如下所示,使用Annotated
str
称为str50
将指示String(50)
:
from typing_extensions import Annotated from sqlalchemy.orm import DeclarativeBase str50 = Annotated[str, 50] # declarative base with a type-level override, using a type that is # expected to be used in multiple places class Base(DeclarativeBase): type_annotation_map = { str50: String(50), }
其次,如果使用Annotated[]
,Declarative 将从左侧类型中提取完整的mapped_column()
定义,通过将mapped_column()
构造传递给Annotated[]
构造(感谢@adriangb01说明了这个想法)。此功能可能在将来的版本中扩展到还包括relationship()
、composite()
和其他构造,但目前仅限于mapped_column()
。下面的示例除了我们的str50
示例之外,还添加了其他额外的Annotated
类型,以说明此功能:
from typing_extensions import Annotated from typing import List from typing import Optional from sqlalchemy import ForeignKey from sqlalchemy import String from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import relationship # declarative base from previous example str50 = Annotated[str, 50] class Base(DeclarativeBase): type_annotation_map = { str50: String(50), } # set up mapped_column() overrides, using whole column styles that are # expected to be used in multiple places intpk = Annotated[int, mapped_column(primary_key=True)] user_fk = Annotated[int, mapped_column(ForeignKey("user_account.id"))] class User(Base): __tablename__ = "user_account" id: Mapped[intpk] name: Mapped[str50] fullname: Mapped[Optional[str]] addresses: Mapped[List["Address"]] = relationship(back_populates="user") class Address(Base): __tablename__ = "address" id: Mapped[intpk] email_address: Mapped[str50] user_id: Mapped[user_fk] user: Mapped["User"] = relationship(back_populates="addresses")
上面,使用Mapped[str50]
、Mapped[intpk]
或Mapped[user_fk]
映射的列直接从registry.type_annotation_map
和Annotated
构造中汲取,以便重新使用预先建立的类型和列配置。
可选步骤 - 将映射类转换为dataclasses
我们可以将映射类转换为dataclasses,其中一个关键优势是我们可以构建一个严格类型化的__init__()
方法,具有显式位置参数、仅关键字参数和默认参数,更不用说我们免费获取了__str__()
和__repr__()
等方法。 下一节 Native Support for Dataclasses Mapped as ORM Models 进一步说明了上述模型的转换。
从第 3 步开始支持类型注解
配上上述示例,从“步骤 3”开始的任何示例都会包括模型的属性类型,并将通过select()
,Query
和Row
对象进行填充:
# (variable) stmt: Select[Tuple[int, str]] stmt = select(User.id, User.name) with Session(e) as sess: for row in sess.execute(stmt): # (variable) row: Row[Tuple[int, str]] print(row) # (variable) users: Sequence[User] users = sess.scalars(select(User)).all() # (variable) users_legacy: List[User] users_legacy = sess.query(User).all()
请参见
使用 mapped_column()声明式表 - 更新了声明式文档以声明式生成和映射Table
列。
第一步 - declarative_base()
已被DeclarativeBase
取代。
在 Python 类型注解中观察到的一个限制是似乎没有能力从函数中动态生成类,然后将其理解为新类的基础的功能。 要解决此问题而不使用插件,通常调用declarative_base()
的方法可以替换为使用DeclarativeBase
类,该类产生与通常相同的Base
对象,只是类型工具将其理解为:
from sqlalchemy.orm import DeclarativeBase class Base(DeclarativeBase): pass
第二步 - 将Column
的声明式用法替换为mapped_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]
;这是因为我们可以声明任何列是可选的或不可选的,而且在我们显式类型化时,没有办法有一个“猜测”的方式不会在类型化时导致类型错误。
然而,在此步骤中,我们上述的映射已经为所有属性设置了适当的 描述符 类型,并且可以在查询中使用以及进行实例级别的操作,所有这些操作都将在不使用插件的情况下通过 mypy –strict 模式。
步骤三 - 使用 Mapped
需要的精确 Python 类型。
对于希望精确类型化的所有属性,都可以执行此操作;对于希望保留为 Any
的属性可以跳过。为了提供背景信息,我们还展示了将 Mapped
用于 relationship()
的情况,我们在此应用了精确类型。在此过渡阶段内的映射将更为冗长,但是随着熟练程度的提高,可以将此步骤与后续步骤结合起来更直接地更新映射:
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()
已经支持基于字符串的前向引用十年一样 😉):
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(primary_key=True) name: Mapped[str] = mapped_column(String(30)) fullname: Mapped[Optional[str]] addresses: Mapped[List["Address"]] = relationship(back_populates="user") class Address(Base): __tablename__ = "address" id: Mapped[int] = mapped_column(primary_key=True) email_address: Mapped[str] user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id")) user: Mapped["User"] = relationship(back_populates="addresses")
第五步 - 利用 pep-593 的Annotated
将常见指令打包成类型
这是一个全新的功能,提供了一种替代或补充方法,作为提供面向类型的配置的手段,也替代了大多数情况下对declared_attr
装饰函数的需求。
首先,Declarative 映射允许将 Python 类型映射到 SQL 类型,例如将str
定制为String
,使用registry.type_annotation_map
进行自定义。使用PEP 593的Annotated
允许我们创建特定 Python 类型的变体,以便可以使用相同的类型,例如str
,每个都提供String
的变体,如下所示,使用Annotated
str
称为str50
将指示String(50)
:
from typing_extensions import Annotated from sqlalchemy.orm import DeclarativeBase str50 = Annotated[str, 50] # declarative base with a type-level override, using a type that is # expected to be used in multiple places class Base(DeclarativeBase): type_annotation_map = { str50: String(50), }
第二,如果使用Annotated[]
,Declarative 将从左侧类型中提取完整的mapped_column()
定义,方法是将mapped_column()
构造作为任何参数传递给Annotated[]
构造(感谢@adriangb01提出这个想法)。未来的版本可能会扩展此功能,以包括relationship()
、composite()
和其他构造,但目前仅限于mapped_column()
。下面的示例除了我们的str50
示例外,还添加了额外的Annotated
类型,以说明此功能:
from typing_extensions import Annotated from typing import List from typing import Optional from sqlalchemy import ForeignKey from sqlalchemy import String from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import relationship # declarative base from previous example str50 = Annotated[str, 50] class Base(DeclarativeBase): type_annotation_map = { str50: String(50), } # set up mapped_column() overrides, using whole column styles that are # expected to be used in multiple places intpk = Annotated[int, mapped_column(primary_key=True)] user_fk = Annotated[int, mapped_column(ForeignKey("user_account.id"))] class User(Base): __tablename__ = "user_account" id: Mapped[intpk] name: Mapped[str50] fullname: Mapped[Optional[str]] addresses: Mapped[List["Address"]] = relationship(back_populates="user") class Address(Base): __tablename__ = "address" id: Mapped[intpk] email_address: Mapped[str50] user_id: Mapped[user_fk] user: Mapped["User"] = relationship(back_populates="addresses")
以上,使用 Mapped[str50]
、Mapped[intpk]
或 Mapped[user_fk]
映射的列会直接从 registry.type_annotation_map
和 Annotated
结构中获取,以便重新使用预先建立的类型化和列配置。
可选步骤 - 将映射类转换为 数据类
我们可以将映射类转换为 数据类,其中一个关键优势是,我们可以构建一个严格类型化的 __init__()
方法,具有显式的位置、关键字和默认参数,更不用说我们可以免费获得 __str__()
和 __repr__()
等方法了。下一节 数据类作为 ORM 模型的本地支持 进一步说明了以上模型的转换。
从第三步开始支持类型化
通过以上示例,从“第三步”开始的任何示例都将包括模型属性是经过类型化的,并将通过到 select()
、Query
和 Row
对象:
# (variable) stmt: Select[Tuple[int, str]] stmt = select(User.id, User.name) with Session(e) as sess: for row in sess.execute(stmt): # (variable) row: Row[Tuple[int, str]] print(row) # (variable) users: Sequence[User] users = sess.scalars(select(User)).all() # (variable) users_legacy: List[User] users_legacy = sess.query(User).all()
另请参见
使用 mapped_column() 的声明式表 - 更新了声明式文档以声明性生成和映射 Table
列。
使用传统 Mypy 类型化模型
使用 Mypy 插件 的 SQLAlchemy 应用,在显式注释中不使用 Mapped
的情况下,会在新系统下产生错误,因为这样的注释在使用 relationship()
等结构时被标记为错误。
章节 2.0 迁移第六步 - 向显式类型化的 ORM 模型添加 allow_unmapped 说明了如何临时禁用对使用显式注释的遗留 ORM 模型引发的错误。
另请参见
2.0 迁移第六步 - 向显式类型化的 ORM 模型添加 allow_unmapped
数据类作为 ORM 模型的本地支持
上面介绍的新的 ORM 声明性特性在 ORM 声明模型中引入了新的mapped_column()
构造,并且演示了以类型为中心的映射,可选地使用PEP 593 Annotated
。我们可以通过将其与 Python 的dataclasses集成,进一步推进映射。这个新特性通过PEP 681实现,允许类型检查器识别符合数据类兼容性的类,或者是完全数据类,但是通过替代 API 声明的类。
使用数据类特性,映射类获得了一个支持位置参数以及可选关键字参数的可定制默认值的__init__()
方法。正如之前提到的,数据类还生成许多有用的方法,如__str__()
,__eq__()
。数据类序列化方法,如dataclasses.asdict()和dataclasses.astuple()也可以使用,但目前不支持自引用结构,这使得它们对于具有双向关系的映射不太可行。
SQLAlchemy 当前的集成方法将用户定义的类转换为真实数据类以提供运行时功能;该特性利用了 SQLAlchemy 1.4 中引入的现有数据类功能,在 Python 数据类,支持 attrs w/声明性,命令式映射中介绍了一个等效的运行时映射,具有完全集成的配置样式,这样做比以前的方法更正确地类型化。
为了支持符合PEP 681的数据类,ORM 构造如mapped_column()
和relationship()
接受额外的PEP 681参数init
、default
和default_factory
,这些参数会传递到数据类创建过程中。这些参数目前必须存在于右侧的显式指令中,就像它们将与dataclasses.field()
一起使用一样;目前它们不能作为左侧Annotated
构造中的局部变量存在。为了支持方便使用Annotated
同时仍支持数据类配置,mapped_column()
可以将右侧的最小一组参数与左侧Annotated
构造中的现有mapped_column()
构造合并,以便保持大部分的简洁性,如下所示。
为了使用类继承启用数据类,我们使用MappedAsDataclass
mixin,可以直接在每个类上使用,也可以在Base
类上使用,如下所示,我们进一步修改了来自 ORM 声明性模型“步骤 5”的示例映射:
from typing_extensions import Annotated from typing import List from typing import Optional from sqlalchemy import ForeignKey from sqlalchemy import String from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import Mapped from sqlalchemy.orm import MappedAsDataclass from sqlalchemy.orm import mapped_column from sqlalchemy.orm import relationship class Base(MappedAsDataclass, DeclarativeBase): """subclasses will be converted to dataclasses""" intpk = Annotated[int, mapped_column(primary_key=True)] str30 = Annotated[str, mapped_column(String(30))] user_fk = Annotated[int, mapped_column(ForeignKey("user_account.id"))] class User(Base): __tablename__ = "user_account" id: Mapped[intpk] = mapped_column(init=False) name: Mapped[str30] fullname: Mapped[Optional[str]] = mapped_column(default=None) addresses: Mapped[List["Address"]] = relationship( back_populates="user", default_factory=list ) class Address(Base): __tablename__ = "address" id: Mapped[intpk] = mapped_column(init=False) email_address: Mapped[str] user_id: Mapped[user_fk] = mapped_column(init=False) user: Mapped["User"] = relationship(back_populates="addresses", default=None)
上述映射在设置声明性映射的同时直接在每个映射类上使用了@dataclasses.dataclass
装饰器,内部设置了每个dataclasses.field()
指令,如所示。使用位置参数可以配置User
/ Address
结构:
>>> u1 = User("username", fullname="full name", addresses=[Address("email@address")]) >>> u1 User(id=None, name='username', fullname='full name', addresses=[Address(id=None, email_address='email@address', user_id=None, user=...)])
另请参阅
声明式数据类映射
SqlAlchemy 2.0 中文文档(五十七)(7)https://developer.aliyun.com/article/1563169