SqlAlchemy 2.0 中文文档(五十七)(4)https://developer.aliyun.com/article/1563166
ORM 声明性模型
SQLAlchemy 1.4 引入了第一个使用sqlalchemy2-stubs和 Mypy 插件的 SQLAlchemy 本机 ORM 类型支持。在 SQLAlchemy 2.0 中,Mypy 插件仍然可用,并已更新以与 SQLAlchemy 2.0 的类型系统配合使用。然而,现在应该将其视为已弃用,因为应用程序现在有一条直接的路径来采用新的类型支持,而不使用插件或存根。
概述
新系统的基本方法是,当使用完全声明式模型(即不使用混合声明式或命令式配置,这些配置不变)时,映射的列声明首先通过检查每个属性声明左侧的类型注解(如果存在)在运行时派生。期望左手类型注释包含在Mapped
泛型类型中,否则不认为该属性是映射属性。然后,属性声明可以引用右侧的mapped_column()
构造,该构造用于提供关于要生成和映射的Column
的附加核心级模式信息。如果左侧存在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()
替换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
或非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()
已经支持基于字符串的正向引用十年了 😉 ):
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()
定义,通过将任何参数传递给 Annotated[]
构造 mapped_column()
构造(感谢 @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__()
等方法。下一节作为 ORM 模型映射的 dataclasses 的本机支持进一步说明了上述模型的转换。
从第 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
列。
概述
新系统的基本方法是,当使用完全声明式模型(即不使用混合声明式或命令式配置,这些配置不变)时,映射列声明首先在运行时通过检查每个属性声明左侧的类型注释来推导,如果存在的话。左手类型注释应该包含在Mapped
泛型类型中,否则该属性不被视为映射属性。然后属性声明可以引用右侧的mapped_column()
构造,用于提供有关要生成和映射的Column
的附加核心级模式信息。如果左侧存在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
第二步 - 将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]
类型进行类型化;这是因为我们可以声明任何列是否为Optional
,而且在明确类型时不可能有“猜测”,否则会在明确类型时导致类型错误。
但是,在这一步骤中,我们上述的映射已经为所有属性设置了适当的描述符类型,并且可以用于查询以及实例级别的操作,所有这些操作都可以在不使用插件的情况下通过 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")
SqlAlchemy 2.0 中文文档(五十七)(6)https://developer.aliyun.com/article/1563168