SqlAlchemy 2.0 中文文档(七)(2)https://developer.aliyun.com/article/1560922
使用非映射数据类字段
当使用声明性数据类时,类上也可以使用非映射字段,这些字段将成为数据类构造过程的一部分,但不会被映射。任何未使用Mapped的字段都将被映射过程忽略。在下面的示例中,字段ctrl_one和ctrl_two将成为对象的实例级状态的一部分,但不会由 ORM 持久化:
from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import registry reg = registry() @reg.mapped_as_dataclass class Data: __tablename__ = "data" id: Mapped[int] = mapped_column(init=False, primary_key=True) status: Mapped[str] ctrl_one: Optional[str] = None ctrl_two: Optional[str] = None
上面的Data实例可以创建为:
d1 = Data(status="s1", ctrl_one="ctrl1", ctrl_two="ctrl2")
一个更实际的例子可能是结合数据类的InitVar特性和__post_init__()特性来接收仅初始化字段,这些字段可用于组成持久化数据。在下面的示例中,User类使用id、name和password_hash作为映射特性,但使用仅初始化的password和repeat_password字段表示用户创建过程(注意:要运行此示例,请将函数your_crypt_function_here()替换为第三方加密函数,如bcrypt或argon2-cffi):
from dataclasses import InitVar from typing import Optional 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] password: InitVar[str] repeat_password: InitVar[str] password_hash: Mapped[str] = mapped_column(init=False, nullable=False) def __post_init__(self, password: str, repeat_password: str): if password != repeat_password: raise ValueError("passwords do not match") self.password_hash = your_crypt_function_here(password)
上述对象使用参数password和repeat_password创建,这些参数被立即消耗,以便生成password_hash变量:
>>> u1 = User(name="some_user", password="xyz", repeat_password="xyz") >>> u1.password_hash '$6$9ppc... (example crypted string....)'
从版本 2.0.0rc1 开始更改:当使用registry.mapped_as_dataclass()或MappedAsDataclass时,可以包括不包括Mapped注释的字段,这些字段将被视为生成的数据类的一部分,但不会被映射,无需指定__allow_unmapped__类属性。以前的 2.0 beta 版本将要求明确存在此属性,即使此属性的目的仅是允许旧版 ORM 类型映射继续运行。### 与诸如 Pydantic 等替代数据类提供者集成
警告
Pydantic 的 dataclass 层与 SQLAlchemy 的类仪器化不完全兼容,需要额外的内部更改,许多功能,例如相关集合,可能无法正常工作。
为了与 Pydantic 兼容,请考虑使用SQLModel ORM,该 ORM 基于 SQLAlchemy ORM 构建,使用 Pydantic,其中包括明确解决这些不兼容性的特殊实现细节。
SQLAlchemy 的MappedAsDataclass类和registry.mapped_as_dataclass()方法调用直接进入 Python 标准库 dataclasses.dataclass 类装饰器,经过类的声明性映射处理后。此函数调用可以通过MappedAsDataclass作为类关键字参数以及registry.mapped_as_dataclass()接受的dataclass_callable参数交换为备用 dataclasses 提供程序,例如 Pydantic 的提供程序:
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import MappedAsDataclass from sqlalchemy.orm import registry class Base( MappedAsDataclass, DeclarativeBase, dataclass_callable=pydantic.dataclasses.dataclass, ): pass class User(Base): __tablename__ = "user" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str]
上述 User 类将被应用为一个数据类,使用 Pydantic 的 pydantic.dataclasses.dataclasses 可调用。该过程对映射类以及从MappedAsDataclass扩展的混合类或直接应用了registry.mapped_as_dataclass()的类都可用。
新版本 2.0.4 中:为MappedAsDataclass和registry.mapped_as_dataclass()添加了 dataclass_callable 类和方法参数,并调整了一些数据类内部,以适应更严格的数据类函数,例如 Pydantic 的函数。
类级特性配置
对 dataclasses 特性的支持是部分的。当前支持的特性包括 init、repr、eq、order 和 unsafe_hash 特性,match_args 和 kw_only 在 Python 3.10+ 上受支持。当前不支持的特性包括 frozen 和 slots 特性。
当使用与MappedAsDataclass配合使用的混合类形式时,类配置参数被作为类级参数传递:
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]
有关 dataclass 类选项的背景,请参阅dataclasses文档中的@dataclasses.dataclass。
属性配置
SQLAlchemy 本地 dataclasses 与普通 dataclasses 不同之处在于,要映射的属性在所有情况下都使用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)。不支持接受简单 Python 值作为默认值的类似 dataclass 语法,而不使用 @dataclases.field()。
作为使用 mapped_column() 的示例,下面的映射将生成一个 __init__() 方法,该方法仅接受字段 name 和 fullname,其中 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 上的设置,后者始终用于数据类配置。例如,要配置一个 datetime 列,其中 Column.default 设置为 func.utc_timestamp() SQL 函数,但构造函数中该参数是可选的:
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 )
使用上述映射,对于一个新的 User 对象的 INSERT,如果没有传递 created_at 的参数,操作将如下进行:
>>> 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() 构建打包以供重复使用。此功能支持 dataclasses 功能。然而,该功能的一个方面在使用类型工具时需要一个解决方法,即 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()
列默认值
为了适应 default 参数与 Column.default 构造函数的现有参数的名称重叠,mapped_column() 构造函数通过添加一个新参数 mapped_column.insert_default 来消除这两个名称之间的歧义,该参数将直接填充到 Column.default 参数中,而与 mapped_column.default 设置无关,后者始终用于 dataclasses 配置。例如,要配置一个 datetime 列,并将 Column.default 设置为 func.utc_timestamp() SQL 函数,但构造函数中该参数是可选的:
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()也必须出现在右侧,并在Annotated结构中包含对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()
Python 类型检查器,支持PEP 681,否则将不考虑非数据类混入的属性作为数据类的一部分。
从版本 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()对象时为Parent.children生成一个空列表,当构造新的Child()对象时,如果不传递parent,则Child.parent将生成一个None值。
虽然relationship.default_factory可以自动从relationship()本身的给定集合类中派生,但这会与数据类兼容性破坏,因为relationship.default_factory或relationship.default的存在决定了参数在渲染为__init__()方法时是必需还是可选。
使用非映射数据类字段
当使用声明性数据类时,也可以在类上使用非映射字段,这些字段将成为数据类构造过程的一部分,但不会被映射。任何不使用Mapped的字段都将被映射过程忽略。在下面的示例中,字段ctrl_one和ctrl_two将成为对象的实例级状态的一部分,但不会被 ORM 持久化:
from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import registry reg = registry() @reg.mapped_as_dataclass class Data: __tablename__ = "data" id: Mapped[int] = mapped_column(init=False, primary_key=True) status: Mapped[str] ctrl_one: Optional[str] = None ctrl_two: Optional[str] = None
上面的Data实例可以创建为:
d1 = Data(status="s1", ctrl_one="ctrl1", ctrl_two="ctrl2")
更真实的示例可能是结合 Dataclasses 的InitVar特性和__post_init__()特性来使用仅初始化的字段,这些字段可以用于组合持久化数据。在下面的示例中,User类使用id、name和password_hash作为映射特性声明,但使用了仅初始化的password和repeat_password字段来表示用户创建过程(注意:要运行此示例,请将函数your_crypt_function_here()替换为第三方加密函数,如bcrypt或argon2-cffi):
from dataclasses import InitVar from typing import Optional 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] password: InitVar[str] repeat_password: InitVar[str] password_hash: Mapped[str] = mapped_column(init=False, nullable=False) def __post_init__(self, password: str, repeat_password: str): if password != repeat_password: raise ValueError("passwords do not match") self.password_hash = your_crypt_function_here(password)
上述对象使用了参数password和repeat_password,这些参数首先被使用,以便生成password_hash变量:
>>> u1 = User(name="some_user", password="xyz", repeat_password="xyz") >>> u1.password_hash '$6$9ppc... (example crypted string....)'
从版本 2.0.0rc1 开始更改:当使用registry.mapped_as_dataclass()或MappedAsDataclass时,可以包括不包含Mapped注释的字段,这些字段将被视为生成的 dataclass 的一部分,但不会被映射,无需另外指示__allow_unmapped__类属性。先前的 2.0 beta 版本将要求明确包含此属性,即使此属性的目的仅是允许旧的 ORM 类型映射继续工作。
与 Pydantic 等替代 Dataclass 提供程序集成
警告
Pydantic 的 dataclass 层与 SQLAlchemy 的类仪器化不完全兼容,需要额外的内部更改,许多功能,如相关集合,可能无法正常工作。
为了与 Pydantic 兼容,请考虑使用SQLModel ORM,它是在 SQLAlchemy ORM 的基础上构建的 Pydantic,其中包含了专门解决这些不兼容性的实现细节。
SQLAlchemy 的MappedAsDataclass类和registry.mapped_as_dataclass()方法调用直接进入 Python 标准库的dataclasses.dataclass类装饰器中,声明性映射过程应用到类之后。此函数调用可以用 Pydantic 等替代数据类提供程序替换,使用MappedAsDataclass作为类关键字参数接受的dataclass_callable参数,以及registry.mapped_as_dataclass()同样接受的参数:
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import MappedAsDataclass from sqlalchemy.orm import registry class Base( MappedAsDataclass, DeclarativeBase, dataclass_callable=pydantic.dataclasses.dataclass, ): pass class User(Base): __tablename__ = "user" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str]
上述的User类将被应用为一个数据类,使用 Pydantic 的pydantic.dataclasses.dataclasses可调用。此过程对于映射类和扩展自MappedAsDataclass或直接应用了registry.mapped_as_dataclass()的混入类都可用。
2.0.4 版本中的新内容:为MappedAsDataclass和registry.mapped_as_dataclass()添加了dataclass_callable类和方法参数,并调整了一些数据类内部,以适应更严格的数据类函数,例如 Pydantic 的函数。
SqlAlchemy 2.0 中文文档(七)(4)https://developer.aliyun.com/article/1560926