SqlAlchemy 2.0 中文文档(七)(2)

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

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]]

注意

attrsslots=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]

类级别的功能配置

数据类功能的支持是部分的。目前支持的是initrepreqorderunsafe_hash功能,match_argskw_only在 Python 3.10+ 上支持。目前不支持的是 frozenslots 功能。

使用带有 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() 支持每个属性字段选项,包括initdefaultdefault_factoryrepr。这些参数的名称固定为PEP 681中指定的名称。功能等同于 dataclasses:

  • init,如mapped_column.initrelationship.init,如果为 False,则表示该字段不应该是__init__()方法的一部分
  • default,如mapped_column.defaultrelationship.default,表示字段的默认值,可以作为关键字参数在__init__()方法中传递。
  • default_factory,如mapped_column.default_factoryrelationship.default_factory,表示一个可调用函数,如果未明确传递给__init__()方法,则会调用它生成新的默认值。
  • repr 默认为 True,表示该字段应该是生成的__repr__()方法的一部分

与 dataclasses 的另一个关键区别是,属性的默认值必须使用 ORM 构造函数的default参数进行配置,例如mapped_column(default=None)。不支持类似 dataclass 语法的语法,该语法接受简单的 Python 值作为默认值,而不使用@dataclases.field()

作为使用mapped_column()的示例,下面的映射将生成一个仅接受字段namefullname__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 593Annotated 对象来打包整个 mapped_column() 构造以便重用。该功能支持使用数据类特性。然而,该特性的一个方面在与类型工具一起使用时需要一种解决方法,即必须将 PEP 681 中的参数 initdefaultreprdefault_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 版开始已弃用:在MappedAsDataclassregistry.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_factoryrelationship.default的存在决定了参数在渲染到__init__()方法时是必需还是可选。


SqlAlchemy 2.0 中文文档(七)(3)https://developer.aliyun.com/article/1560924

相关文章
|
2天前
|
SQL 前端开发 数据库
SqlAlchemy 2.0 中文文档(六)(1)
SqlAlchemy 2.0 中文文档(六)
13 0
|
2天前
|
测试技术 API 数据库
SqlAlchemy 2.0 中文文档(九)(5)
SqlAlchemy 2.0 中文文档(九)
7 0
|
3天前
|
SQL 自然语言处理 数据库
SqlAlchemy 2.0 中文文档(二)(3)
SqlAlchemy 2.0 中文文档(二)
10 2
|
3天前
|
SQL JSON 关系型数据库
SqlAlchemy 2.0 中文文档(二)(2)
SqlAlchemy 2.0 中文文档(二)
15 2
|
3天前
|
SQL 测试技术 Python
SqlAlchemy 2.0 中文文档(二)(1)
SqlAlchemy 2.0 中文文档(二)
14 2
|
2天前
|
SQL 存储 数据库
SqlAlchemy 2.0 中文文档(九)(1)
SqlAlchemy 2.0 中文文档(九)
10 0
|
2天前
|
SQL 测试技术 索引
SqlAlchemy 2.0 中文文档(六)(5)
SqlAlchemy 2.0 中文文档(六)
7 0
|
2天前
|
SQL 数据库 数据安全/隐私保护
SqlAlchemy 2.0 中文文档(七)(3)
SqlAlchemy 2.0 中文文档(七)
12 0
|
2天前
|
Python
SqlAlchemy 2.0 中文文档(九)(3)
SqlAlchemy 2.0 中文文档(九)
8 0
|
2天前
|
Python
SqlAlchemy 2.0 中文文档(七)(4)
SqlAlchemy 2.0 中文文档(七)
8 0