SqlAlchemy 2.0 中文文档(七)(1)https://developer.aliyun.com/article/1560920
使用 Declarative “Imperative Table” 映射 attrs
在“声明式与命令式表”风格中,Table
对象与声明式类内联声明。首先将 @define
装饰器应用于类,然后将registry.mapped()
装饰器应用于类:
from __future__ import annotations from typing import List from typing import Optional from attrs import define from sqlalchemy import Column from sqlalchemy import ForeignKey from sqlalchemy import Integer from sqlalchemy import MetaData from sqlalchemy import String from sqlalchemy import Table from sqlalchemy.orm import Mapped from sqlalchemy.orm import registry from sqlalchemy.orm import relationship mapper_registry = registry() @mapper_registry.mapped @define(slots=False) class User: __table__ = Table( "user", mapper_registry.metadata, Column("id", Integer, primary_key=True), Column("name", String(50)), Column("FullName", String(50), key="fullname"), Column("nickname", String(12)), ) id: Mapped[int] name: Mapped[str] fullname: Mapped[str] nickname: Mapped[str] addresses: Mapped[List[Address]] __mapper_args__ = { # type: ignore "properties": { "addresses": relationship("Address"), } } @mapper_registry.mapped @define(slots=False) class Address: __table__ = Table( "address", mapper_registry.metadata, Column("id", Integer, primary_key=True), Column("user_id", Integer, ForeignKey("user.id")), Column("email_address", String(50)), ) id: Mapped[int] user_id: Mapped[int] email_address: Mapped[Optional[str]]
注意
attrs
的slots=True
选项,该选项在映射类上启用__slots__
,不能与 SQLAlchemy 映射一起使用,除非完全实现了替代的属性仪器化,因为映射类通常依赖于对__dict__
的直接访问来存储状态。当此选项存在时,行为是未定义的。
使用命令式映射映射 attrs
就像对待 dataclasses 一样,我们可以利用registry.map_imperatively()
来将现有的 attrs
类映射为:
from __future__ import annotations from typing import List from attrs import define from sqlalchemy import Column from sqlalchemy import ForeignKey from sqlalchemy import Integer from sqlalchemy import MetaData from sqlalchemy import String from sqlalchemy import Table from sqlalchemy.orm import registry from sqlalchemy.orm import relationship mapper_registry = registry() @define(slots=False) class User: id: int name: str fullname: str nickname: str addresses: List[Address] @define(slots=False) class Address: id: int user_id: int email_address: Optional[str] metadata_obj = MetaData() user = Table( "user", metadata_obj, Column("id", Integer, primary_key=True), Column("name", String(50)), Column("fullname", String(50)), Column("nickname", String(12)), ) address = Table( "address", metadata_obj, Column("id", Integer, primary_key=True), Column("user_id", Integer, ForeignKey("user.id")), Column("email_address", String(50)), ) mapper_registry.map_imperatively( User, user, properties={ "addresses": relationship(Address, backref="user", order_by=address.c.id), }, ) mapper_registry.map_imperatively(Address, address)
上述形式等同于之前使用声明式与命令式表的示例。## 声明式数据类映射
SQLAlchemy 带注释的声明表映射可以通过附加的 mixin 类或装饰器指令来增强,这将在映射完成后的声明过程中添加一个额外的步骤,将映射的类原地转换为 Python dataclass,然后完成应用于类的 ORM 特定仪器化过程。这提供的最突出的行为增强是生成具有对位置和关键字参数的细粒度控制的__init__()
方法,有或没有默认值,以及生成诸如__repr__()
和__eq__()
之类的方法。
从PEP 484的类型学角度来看,该类被认为具有 Dataclass 特定的行为,最值得注意的是通过利用PEP 681“Dataclass Transforms”,这允许类型工具将该类视为明确使用@dataclasses.dataclass
装饰器装饰的类。
注意
截至2023 年 4 月 4 日,在类型工具中支持PEP 681的情况是有限的,并且目前已知被Pyright以及1.2 版的Mypy支持。请注意,Mypy 1.1.1 引入了PEP 681支持,但未正确适应 Python 描述符,这将导致在使用 SQLAlchemy 的 ORM 映射方案时出现错误。
另请参阅
peps.python.org/pep-0681/#the-dataclass-transform-decorator
- 背景介绍了像 SQLAlchemy 这样的库如何启用 PEP 681 支持
数据类转换可以通过将 MappedAsDataclass
mixin 添加到 DeclarativeBase
类层次结构中的任何声明性类来实现,或者通过使用 registry.mapped_as_dataclass()
类装饰器来进行装饰映射。
MappedAsDataclass
mixin 可以应用于声明性的 Base
类或任何超类,如下例所示:
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import MappedAsDataclass class Base(MappedAsDataclass, DeclarativeBase): """subclasses will be converted to dataclasses""" class User(Base): __tablename__ = "user_account" id: Mapped[int] = mapped_column(init=False, primary_key=True) name: Mapped[str]
也可以直接应用于从声明性基类扩展的类:
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import MappedAsDataclass class Base(DeclarativeBase): pass class User(MappedAsDataclass, Base): """User class will be converted to a dataclass""" __tablename__ = "user_account" id: Mapped[int] = mapped_column(init=False, primary_key=True) name: Mapped[str]
使用装饰器形式时,仅支持 registry.mapped_as_dataclass()
装饰器:
from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import registry reg = registry() @reg.mapped_as_dataclass class User: __tablename__ = "user_account" id: Mapped[int] = mapped_column(init=False, primary_key=True) name: Mapped[str]
类级别的功能配置
数据类功能的支持是部分的。目前支持的是init
、repr
、eq
、order
和unsafe_hash
功能,match_args
和 kw_only
在 Python 3.10+ 上支持。目前不支持的是 frozen
和 slots
功能。
使用带有 MappedAsDataclass
的 mixin 类形式时,类配置参数作为类级别参数传递:
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import MappedAsDataclass class Base(DeclarativeBase): pass class User(MappedAsDataclass, Base, repr=False, unsafe_hash=True): """User class will be converted to a dataclass""" __tablename__ = "user_account" id: Mapped[int] = mapped_column(init=False, primary_key=True) name: Mapped[str]
使用带有 registry.mapped_as_dataclass()
的装饰器形式时,类配置参数直接传递给装饰器:
from sqlalchemy.orm import registry from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column reg = registry() @reg.mapped_as_dataclass(unsafe_hash=True) class User: """User class will be converted to a dataclass""" __tablename__ = "user_account" id: Mapped[int] = mapped_column(init=False, primary_key=True) name: Mapped[str]
有关数据类选项的背景,请参阅 dataclasses 文档中的 @dataclasses.dataclass.
属性配置
SQLAlchemy 原生数据类与普通数据类不同之处在于,要映射的属性在所有情况下都是使用 Mapped
泛型注释容器描述的。映射遵循与 声明性表格与 mapped_column() 中记录的相同形式,所有 mapped_column()
和 Mapped
的功能都得到支持。
此外,ORM 属性配置构造,包括mapped_column()
,relationship()
和composite()
支持每个属性字段选项,包括init
,default
,default_factory
和repr
。这些参数的名称固定为PEP 681中指定的名称。功能等同于 dataclasses:
init
,如mapped_column.init
,relationship.init
,如果为 False,则表示该字段不应该是__init__()
方法的一部分default
,如mapped_column.default
,relationship.default
,表示字段的默认值,可以作为关键字参数在__init__()
方法中传递。default_factory
,如mapped_column.default_factory
,relationship.default_factory
,表示一个可调用函数,如果未明确传递给__init__()
方法,则会调用它生成新的默认值。repr
默认为 True,表示该字段应该是生成的__repr__()
方法的一部分
与 dataclasses 的另一个关键区别是,属性的默认值必须使用 ORM 构造函数的default
参数进行配置,例如mapped_column(default=None)
。不支持类似 dataclass 语法的语法,该语法接受简单的 Python 值作为默认值,而不使用@dataclases.field()
。
作为使用mapped_column()
的示例,下面的映射将生成一个仅接受字段name
和fullname
的__init__()
方法,其中name
是必需的,可以按位置传递,而fullname
是可选的。我们预期会从数据库生成id
字段,根本不是构造函数的一部分:
from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import registry reg = registry() @reg.mapped_as_dataclass class User: __tablename__ = "user_account" id: Mapped[int] = mapped_column(init=False, primary_key=True) name: Mapped[str] fullname: Mapped[str] = mapped_column(default=None) # 'fullname' is optional keyword argument u1 = User("name")
列默认值
为了适应 default
参数与现有 Column.default
参数之间的名称重叠,mapped_column()
构造通过添加一个新参数 mapped_column.insert_default
来消除这两个名称的歧义,该参数将直接填充到 Column.default
参数中,并且独立于在 mapped_column.default
上设置的内容,后者始终用于数据类配置。例如,配置一个带有 func.utc_timestamp()
SQL 函数作为 Column.default
的日期时间列,但在构造函数中该参数是可选的:
from datetime import datetime from sqlalchemy import func from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import registry reg = registry() @reg.mapped_as_dataclass class User: __tablename__ = "user_account" id: Mapped[int] = mapped_column(init=False, primary_key=True) created_at: Mapped[datetime] = mapped_column( insert_default=func.utc_timestamp(), default=None )
使用上述映射,对于未传递任何 created_at
参数的新 User
对象的 INSERT
如下进行:
>>> with Session(e) as session: ... session.add(User()) ... session.commit() BEGIN (implicit) INSERT INTO user_account (created_at) VALUES (utc_timestamp()) [generated in 0.00010s] () COMMIT
与 Annotated 的集成
将整个列声明映射到 Python 类型 中介绍的方法演示了如何使用 PEP 593 的 Annotated
对象来打包整个 mapped_column()
构造以便重用。该功能支持使用数据类特性。然而,该特性的一个方面在与类型工具一起使用时需要一种解决方法,即必须将 PEP 681 中的参数 init
、default
、repr
和 default_factory
必须 放在右侧,并打包到一个明确的 mapped_column()
构造中,以便类型工具正确解释属性。例如,下面的方法在运行时完全正常,但是类型工具将认为 User()
构造无效,因为它们看不到 init=False
参数:
from typing import Annotated from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import registry # typing tools will ignore init=False here intpk = Annotated[int, mapped_column(init=False, primary_key=True)] reg = registry() @reg.mapped_as_dataclass class User: __tablename__ = "user_account" id: Mapped[intpk] # typing error: Argument missing for parameter "id" u1 = User()
相反,mapped_column()
必须出现在右侧,并且使用明确设置的 mapped_column.init
;其他参数可以保留在 Annotated
结构内:
from typing import Annotated from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import registry intpk = Annotated[int, mapped_column(primary_key=True)] reg = registry() @reg.mapped_as_dataclass class User: __tablename__ = "user_account" # init=False and other pep-681 arguments must be inline id: Mapped[intpk] = mapped_column(init=False) u1 = User()
使用混入和抽象超类
在MappedAsDataclass
映射类中使用的任何混入或基类,其中包括Mapped
属性,必须本身是 MappedAsDataclass
层次结构的一部分,例如下面的示例使用混入:
class Mixin(MappedAsDataclass): create_user: Mapped[int] = mapped_column() update_user: Mapped[Optional[int]] = mapped_column(default=None, init=False) class Base(DeclarativeBase, MappedAsDataclass): pass class User(Base, Mixin): __tablename__ = "sys_user" uid: Mapped[str] = mapped_column( String(50), init=False, default_factory=uuid4, primary_key=True ) username: Mapped[str] = mapped_column() email: Mapped[str] = mapped_column()
支持 PEP 681 的 Python 类型检查器将否则不会认为来自非数据类混入的属性属于数据类的一部分。
自 2.0.8 版开始已弃用:在MappedAsDataclass
或registry.mapped_as_dataclass()
层次结构中使用混入和抽象基类,这些结构本身不是数据类已被弃用,因为这些字段不被 PEP 681 视为数据类的一部分。对于这种情况会发出警告,稍后将成为错误。
另请参见
将转换为数据类时,属性来自非数据类的超类。 - 关于原因的背景
关系配置
Mapped
注释与 relationship()
结合使用的方式与基本关系模式中描述的方式相同。当指定基于集合的 relationship()
作为可选关键字参数时,必须传递 relationship.default_factory
参数,并且它必须引用要使用的集合类。如果默认值为 None
,则多对一和标量对象引用可以使用 relationship.default
:
from typing import List from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import registry from sqlalchemy.orm import relationship reg = registry() @reg.mapped_as_dataclass class Parent: __tablename__ = "parent" id: Mapped[int] = mapped_column(primary_key=True) children: Mapped[List["Child"]] = relationship( default_factory=list, back_populates="parent" ) @reg.mapped_as_dataclass class Child: __tablename__ = "child" id: Mapped[int] = mapped_column(primary_key=True) parent_id: Mapped[int] = mapped_column(ForeignKey("parent.id")) parent: Mapped["Parent"] = relationship(default=None)
上述映射将在构造新的Parent()
对象时,如果没有传递children
参数,则为Parent.children
生成一个空列表,并且在构造新的Child()
对象时,如果没有传递parent
参数,则为Child.parent
生成一个None
值。
虽然relationship.default_factory
可以从relationship()
自身的给定集合类中自动推导出来,但这将与数据类的兼容性破坏,因为relationship.default_factory
或relationship.default
的存在决定了参数在渲染到__init__()
方法时是必需还是可选。
SqlAlchemy 2.0 中文文档(七)(3)https://developer.aliyun.com/article/1560924