SqlAlchemy 2.0 中文文档(五十七)(6)

简介: SqlAlchemy 2.0 中文文档(五十七)

SqlAlchemy 2.0 中文文档(五十七)(5)https://developer.aliyun.com/article/1563167


第五步 - 利用 pep-593 Annotated将常见指令打包成类型

这是一个激进的新功能,提供了一个替代方案,或者说是补充性方法,用于提供类型定向配置,还可以在大多数情况下替代declared_attr修饰的函数的需求。

首先,Declarative 映射允许将 Python 类型映射到 SQL 类型,例如strString的自定义,使用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_mapAnnotated构造中汲取,以便重新使用预先建立的类型和列配置。

可选步骤 - 将映射类转换为dataclasses

我们可以将映射类转换为dataclasses,其中一个关键优势是我们可以构建一个严格类型化的__init__()方法,具有显式位置参数、仅关键字参数和默认参数,更不用说我们免费获取了__str__()__repr__()等方法。 下一节 Native Support for Dataclasses Mapped as ORM Models 进一步说明了上述模型的转换。

从第 3 步开始支持类型注解

配上上述示例,从“步骤 3”开始的任何示例都会包括模型的属性类型,并将通过select()QueryRow对象进行填充:

# (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()QueryResult 构造。现在我们可以开始减少映射声明中的冗余。

步骤四 - 移除不再需要的 mapped_column() 指令。

所有nullable参数都可以使用Optional[]来隐含;在没有Optional[]的情况下,nullable默认为False。所有没有参数的 SQL 类型,如IntegerString,可以仅用 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 593Annotated允许我们创建特定 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_mapAnnotated 结构中获取,以便重新使用预先建立的类型化和列配置。

可选步骤 - 将映射类转换为 数据类

我们可以将映射类转换为 数据类,其中一个关键优势是,我们可以构建一个严格类型化的 __init__() 方法,具有显式的位置、关键字和默认参数,更不用说我们可以免费获得 __str__()__repr__() 等方法了。下一节 数据类作为 ORM 模型的本地支持 进一步说明了以上模型的转换。

从第三步开始支持类型化

通过以上示例,从“第三步”开始的任何示例都将包括模型属性是经过类型化的,并将通过到 select()QueryRow 对象:

# (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参数initdefaultdefault_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

相关文章
|
2月前
|
SQL Oracle 关系型数据库
SqlAlchemy 2.0 中文文档(五十七)(9)
SqlAlchemy 2.0 中文文档(五十七)
25 0
|
2月前
|
SQL Oracle 关系型数据库
SqlAlchemy 2.0 中文文档(五十七)(3)
SqlAlchemy 2.0 中文文档(五十七)
19 0
|
2月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(五十七)(1)
SqlAlchemy 2.0 中文文档(五十七)
26 0
|
2月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(五十七)(7)
SqlAlchemy 2.0 中文文档(五十七)
27 0
|
2月前
|
SQL 存储 测试技术
SqlAlchemy 2.0 中文文档(五十七)(4)
SqlAlchemy 2.0 中文文档(五十七)
22 0
|
2月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(五十七)(2)
SqlAlchemy 2.0 中文文档(五十七)
15 0
|
2月前
|
SQL Python
SqlAlchemy 2.0 中文文档(五十七)(5)
SqlAlchemy 2.0 中文文档(五十七)
12 0
|
2月前
|
SQL Oracle 关系型数据库
SqlAlchemy 2.0 中文文档(五十七)(8)
SqlAlchemy 2.0 中文文档(五十七)
22 0
|
2月前
|
SQL Oracle 关系型数据库
SqlAlchemy 2.0 中文文档(六十五)(4)
SqlAlchemy 2.0 中文文档(六十五)
17 0
|
2月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(六十五)(2)
SqlAlchemy 2.0 中文文档(六十五)
22 0