复合列类型

一组列可以与单个用户定义的数据类型相关联,在现代使用中,通常是 Python dataclass。ORM 提供一个表示列组的单个属性,使用您提供的类。

一个简单的示例将 Integer 列对表示为 Point 对象,具有属性 .x.y。使用 dataclass,这些属性使用相应的 int Python 类型定义。

import dataclasses


@dataclasses.dataclass
class Point:
    x: int
    y: int

也接受非 dataclass 形式,但需要实现额外的​​方法。有关使用非 dataclass 类的示例,请参见 使用传统非 dataclass 部分。

版本 2.0 中新增: composite() 结构完全支持 Python dataclasses,包括从复合类型类派生映射列数据类型的能力。

我们将创建一个到 vertices 表的映射,该表将两个点表示为 x1/y1x2/y2。使用 composite() 结构,将 Point 类与映射的列相关联。

下面的示例说明了 composite() 的最现代形式,它与完全 带注解的声明式表 配置一起使用。mapped_column() 结构表示每个列直接传递给 composite(),指示要生成的列的零个或多个方面,在本例中是名称;composite() 结构从 dataclass 本身直接派生列类型(在本例中为 int,对应于 Integer)。

from sqlalchemy.orm import DeclarativeBase, Mapped
from sqlalchemy.orm import composite, mapped_column


class Base(DeclarativeBase):
    pass


class Vertex(Base):
    __tablename__ = "vertices"

    id: Mapped[int] = mapped_column(primary_key=True)

    start: Mapped[Point] = composite(mapped_column("x1"), mapped_column("y1"))
    end: Mapped[Point] = composite(mapped_column("x2"), mapped_column("y2"))

    def __repr__(self):
        return f"Vertex(start={self.start}, end={self.end})"

提示

在上面的示例中,表示复合类型的列(x1y1 等)也可以在类上访问,但类型检查器无法正确理解它们。如果访问单个列很重要,可以显式声明它们,如 直接映射列,将属性名称传递给复合类型 中所示。

上面的映射将对应于以下 CREATE TABLE 语句:

>>> from sqlalchemy.schema import CreateTable
>>> print(CreateTable(Vertex.__table__))
CREATE TABLE vertices ( id INTEGER NOT NULL, x1 INTEGER NOT NULL, y1 INTEGER NOT NULL, x2 INTEGER NOT NULL, y2 INTEGER NOT NULL, PRIMARY KEY (id) )

使用映射的复合列类型

使用上面部分中所示的映射,我们可以使用 Vertex 类,其中 .start.end 属性将透明地引用 Point 类引用的列,以及使用 Vertex 类的实例,其中 .start.end 属性将引用 Point 类的实例。 x1y1x2y2 列将透明地处理。

  • 持久化 Point 对象

    我们可以创建一个 Vertex 对象,为其成员分配 Point 对象,它们将按预期持久化。

    >>> v = Vertex(start=Point(3, 4), end=Point(5, 6))
    >>> session.add(v)
    >>> session.commit()
    
    BEGIN (implicit) INSERT INTO vertices (x1, y1, x2, y2) VALUES (?, ?, ?, ?) [generated in ...] (3, 4, 5, 6) COMMIT
  • 选择 Point 对象作为列

    composite() 将允许 Vertex.startVertex.end 属性在使用 ORM Session(包括传统的 Query 对象)选择 Point 对象时尽可能地表现得像单个 SQL 表达式。

    >>> stmt = select(Vertex.start, Vertex.end)
    >>> session.execute(stmt).all()
    
    SELECT vertices.x1, vertices.y1, vertices.x2, vertices.y2 FROM vertices [...] ()
    [(Point(x=3, y=4), Point(x=5, y=6))]
  • 在 SQL 表达式中比较 Point 对象

    可以使用 Vertex.startVertex.end 属性作为 WHERE 条件和类似的条件,使用临时 Point 对象进行比较。

    >>> stmt = select(Vertex).where(Vertex.start == Point(3, 4)).where(Vertex.end < Point(7, 8))
    >>> session.scalars(stmt).all()
    
    SELECT vertices.id, vertices.x1, vertices.y1, vertices.x2, vertices.y2 FROM vertices WHERE vertices.x1 = ? AND vertices.y1 = ? AND vertices.x2 < ? AND vertices.y2 < ? [...] (3, 4, 7, 8)
    [Vertex(Point(x=3, y=4), Point(x=5, y=6))]

    版本 2.0 中新增: composite() 结构现在支持“排序”比较,例如 <>= 和类似的比较,以及对 ==!= 的已有支持。

    提示

    上面的“排序”比较使用“小于”运算符(<),以及使用 == 的“相等”比较,当用于生成 SQL 表达式时,由 Comparator 类实现,并且不使用复合类型类本身的比较方法,例如 __lt__()__eq__() 方法。因此,上面的 Point dataclass 也无需实现 dataclasses order=True 参数即可使上面的 SQL 操作正常工作。重新定义复合类型的比较操作 部分包含有关如何自定义比较操作的信息。

  • 更新 Vertex 实例上的 Point 对象

    默认情况下,Point 对象必须用新对象替换才能检测到更改。

    >>> v1 = session.scalars(select(Vertex)).one()
    
    SELECT vertices.id, vertices.x1, vertices.y1, vertices.x2, vertices.y2 FROM vertices [...] ()
    >>> v1.end = Point(x=10, y=14) >>> session.commit()
    UPDATE vertices SET x2=?, y2=? WHERE vertices.id = ? [...] (10, 14, 1) COMMIT

    为了允许对复合类型对象进行就地更改,必须使用 变异跟踪 扩展。有关示例,请参见 在复合类型上建立可变性 部分。

复合类型的其他映射形式

composite() 结构可以使用 mapped_column() 结构、Column 或现有映射列的字符串名称传递相关列。以下示例说明了与上面部分中的主要映射等效的映射。

直接映射列,然后传递给复合类型

这里我们将现有的 mapped_column() 实例传递给 composite() 结构,就像下面非注释示例中一样,我们也传递了 Point 类作为 composite() 的第一个参数

from sqlalchemy import Integer
from sqlalchemy.orm import mapped_column, composite


class Vertex(Base):
    __tablename__ = "vertices"

    id = mapped_column(Integer, primary_key=True)
    x1 = mapped_column(Integer)
    y1 = mapped_column(Integer)
    x2 = mapped_column(Integer)
    y2 = mapped_column(Integer)

    start = composite(Point, x1, y1)
    end = composite(Point, x2, y2)

直接映射列,将属性名称传递给复合类型

我们可以使用更多带注释的形式编写上面的相同示例,其中我们可以选择将属性名称传递给 composite(),而不是完整的列结构

from sqlalchemy.orm import mapped_column, composite, Mapped


class Vertex(Base):
    __tablename__ = "vertices"

    id: Mapped[int] = mapped_column(primary_key=True)
    x1: Mapped[int]
    y1: Mapped[int]
    x2: Mapped[int]
    y2: Mapped[int]

    start: Mapped[Point] = composite("x1", "y1")
    end: Mapped[Point] = composite("x2", "y2")

命令式映射和命令式表

当使用 命令式表 或完全 命令式 映射时,我们可以直接访问 Column 对象。这些也可以传递给 composite(),就像下面这个命令式示例一样

mapper_registry.map_imperatively(
    Vertex,
    vertices_table,
    properties={
        "start": composite(Point, vertices_table.c.x1, vertices_table.c.y1),
        "end": composite(Point, vertices_table.c.x2, vertices_table.c.y2),
    },
)

使用传统的非数据类

如果不使用数据类,则自定义数据类型类的要求是它必须具有一个构造函数,该构造函数接受与其列格式相对应的 positional 参数,并且还提供一个方法 __composite_values__(),该方法以其基于列的属性的顺序返回对象的 state 作为列表或元组。它还应该提供足够的 __eq__()__ne__() 方法来测试两个实例的相等性。

为了说明主部分中未使用数据类的等效 Point

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __composite_values__(self):
        return self.x, self.y

    def __repr__(self):
        return f"Point(x={self.x!r}, y={self.y!r})"

    def __eq__(self, other):
        return isinstance(other, Point) and other.x == self.x and other.y == self.y

    def __ne__(self, other):
        return not self.__eq__(other)

composite() 的使用然后继续进行,其中要与 Point 类关联的列也必须使用显式类型声明,使用 复合类型的其他映射形式 中的其中一种形式。

跟踪复合类型上的就地修改

对现有复合值进行的就地更改不会自动跟踪。相反,复合类需要向其父对象显式提供事件。此任务在很大程度上通过使用 MutableComposite 混合类实现自动化,该混合类使用事件将每个用户定义的复合对象与所有父关联关联。请参阅 在复合类型上建立可变性 中的示例。

重新定义复合类型的比较运算符

默认情况下,“等于”比较操作生成所有对应列的 AND,这些列彼此相等。这可以使用 composite()comparator_factory 参数更改,我们在其中指定一个自定义 Comparator 类来定义现有或新的操作。下面我们说明“大于”运算符,实现与基本“大于”运算符相同的表达式

import dataclasses

from sqlalchemy.orm import composite
from sqlalchemy.orm import CompositeProperty
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.sql import and_


@dataclasses.dataclass
class Point:
    x: int
    y: int


class PointComparator(CompositeProperty.Comparator):
    def __gt__(self, other):
        """redefine the 'greater than' operation"""

        return and_(
            *[
                a > b
                for a, b in zip(
                    self.__clause_element__().clauses,
                    dataclasses.astuple(other),
                )
            ]
        )


class Base(DeclarativeBase):
    pass


class Vertex(Base):
    __tablename__ = "vertices"

    id: Mapped[int] = mapped_column(primary_key=True)

    start: Mapped[Point] = composite(
        mapped_column("x1"), mapped_column("y1"), comparator_factory=PointComparator
    )
    end: Mapped[Point] = composite(
        mapped_column("x2"), mapped_column("y2"), comparator_factory=PointComparator
    )

由于 Point 是一个数据类,因此我们可以使用 dataclasses.astuple() 获取 Point 实例的元组形式。

自定义比较器然后返回适当的 SQL 表达式

>>> print(Vertex.start > Point(5, 6))
vertices.x1 > :x1_1 AND vertices.y1 > :y1_1

嵌套复合类型

复合对象可以定义为在简单的嵌套方案中工作,方法是重新定义复合类中的行为以按预期工作,然后将复合类映射到通常的单个列的完整长度。这要求定义额外的用于在“嵌套”和“扁平”形式之间移动的方法。

下面我们将 Vertex 类重新组织为它本身是一个复合对象,它引用 Point 对象。 VertexPoint 可以是数据类,但是我们将在 Vertex 中添加一个自定义构造方法,该方法可用于在给定四个列值的情况下创建新的 Vertex 对象,我们将任意命名为 _generate() 并将其定义为类方法,以便我们可以通过将值传递给 Vertex._generate() 方法来创建新的 Vertex 对象。

我们还将实现 __composite_values__() 方法,这是一个由 composite() 结构(之前在 使用传统的非数据类 中介绍过)识别的固定名称,它指示以列值的扁平元组形式接收对象的标准方式,在这种情况下将取代通常面向数据类的方法。

使用我们自定义的 _generate() 构造函数和 __composite_values__() 序列化方法,我们现在可以在包含 Point 实例的列的扁平元组和 Vertex 对象之间移动。 Vertex._generate 方法作为 composite() 结构的第一个参数传递,作为新 Vertex 实例的来源,并且 __composite_values__() 方法将被 composite() 隐式使用。

出于示例的目的,Vertex 复合类型随后被映射到一个名为 HasVertex 的类,该类是包含四个源列的 Table 最终所在的类

from __future__ import annotations

import dataclasses
from typing import Any
from typing import Tuple

from sqlalchemy.orm import composite
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column


@dataclasses.dataclass
class Point:
    x: int
    y: int


@dataclasses.dataclass
class Vertex:
    start: Point
    end: Point

    @classmethod
    def _generate(cls, x1: int, y1: int, x2: int, y2: int) -> Vertex:
        """generate a Vertex from a row"""
        return Vertex(Point(x1, y1), Point(x2, y2))

    def __composite_values__(self) -> Tuple[Any, ...]:
        """generate a row from a Vertex"""
        return dataclasses.astuple(self.start) + dataclasses.astuple(self.end)


class Base(DeclarativeBase):
    pass


class HasVertex(Base):
    __tablename__ = "has_vertex"
    id: Mapped[int] = mapped_column(primary_key=True)
    x1: Mapped[int]
    y1: Mapped[int]
    x2: Mapped[int]
    y2: Mapped[int]

    vertex: Mapped[Vertex] = composite(Vertex._generate, "x1", "y1", "x2", "y2")

上面的映射然后可以使用 HasVertexVertexPoint 的形式来使用

hv = HasVertex(vertex=Vertex(Point(1, 2), Point(3, 4)))

session.add(hv)
session.commit()

stmt = select(HasVertex).where(HasVertex.vertex == Vertex(Point(1, 2), Point(3, 4)))

hv = session.scalars(stmt).first()
print(hv.vertex.start)
print(hv.vertex.end)

复合类型 API

对象名称 描述

composite([_class_or_attr], *attrs, [group, deferred, raiseload, comparator_factory, active_history, init, repr, default, default_factory, compare, kw_only, hash, info, doc], **__kw)

返回用于与 Mapper 一起使用的基于复合列的属性。

function sqlalchemy.orm.composite(_class_or_attr: None | Type[_CC] | Callable[..., _CC] | _CompositeAttrType[Any] = None, *attrs: _CompositeAttrType[Any], group: str | None = None, deferred: bool = False, raiseload: bool = False, comparator_factory: Type[Composite.Comparator[_T]] | None = None, active_history: bool = False, init: _NoArg | bool = _NoArg.NO_ARG, repr: _NoArg | bool = _NoArg.NO_ARG, default: Any | None = _NoArg.NO_ARG, default_factory: _NoArg | Callable[[], _T] = _NoArg.NO_ARG, compare: _NoArg | bool = _NoArg.NO_ARG, kw_only: _NoArg | bool = _NoArg.NO_ARG, hash: _NoArg | bool | None = _NoArg.NO_ARG, info: _InfoType | None = None, doc: str | None = None, **__kw: Any) Composite[Any]

返回用于与 Mapper 一起使用的基于复合列的属性。

有关完整使用示例,请参阅映射文档部分复合列类型

composite() 返回的MapperPropertyComposite

参数:
  • class_ – “复合类型”类,或任何类方法或可调用对象,该方法或可调用对象将根据按顺序排列的列值生成复合对象的实例。

  • *attrs

    要映射的元素列表,其中可能包括

    • Column 对象

    • mapped_column() 构造

    • 映射类中其他属性的字符串名称,这些属性可以是任何其他 SQL 属性或对象映射属性。例如,这可以允许一个复合属性引用一个多对一关系。

  • active_history=False – 当为 True 时,表示如果标量属性的“先前”值尚未加载,则在替换时应加载该值。有关详细信息,请参见column_property() 上的相同标志。

  • group – 将该属性标记为延迟加载时的组名称。

  • deferred – 当为 True 时,列属性为“延迟加载”,这意味着它不会立即加载,而是在首次访问实例上的属性时加载。另请参见deferred()

  • comparator_factory – 一个扩展了 Comparator 的类,为比较操作提供自定义 SQL 子句生成。

  • doc – 作为类绑定描述符的文档使用的可选字符串。

  • info – 可选的数据字典,它将填充到该对象的MapperProperty.info 属性中。

  • init – 特别适用于声明式数据类映射,指定映射属性是否应作为数据类进程生成的__init__() 方法的一部分。

  • repr – 特别适用于声明式数据类映射,指定映射属性是否应作为数据类进程生成的__repr__() 方法的一部分。

  • default_factory – 特别适用于声明式数据类映射,指定一个默认值生成函数,该函数将在数据类进程生成的__init__() 方法中执行。

  • compare

    特别适用于声明式数据类映射,指示在为映射类生成__eq__()__ne__() 方法时,是否应将此字段包含在比较操作中。

    版本 2.0.0b4 中的新功能。

  • kw_only – 特定于 声明式数据类映射,指示在生成 __init__() 时是否将此字段标记为关键字专用。

  • hash

    特定于 声明式数据类映射,控制在为映射类生成 __hash__() 方法时是否包含此字段。

    2.0.36 版本新增。