Composite Column Types

列のセットは、単一のユーザー定義データ型に関連付けることができます。現在の使用法では、通常はPython dataclass です。ORMは、指定したクラスを使用して列のグループを表す単一の属性を提供します。

簡単な例では、 Integer 列のペアを、属性 .x.y を持つ Point オブジェクトとして表します。データクラスを使うと、これらの属性は対応する int Python型で定義されます:

import dataclasses

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

データクラス以外のフォームも使用できますが、追加のメソッドを実装する必要があります。データクラス以外のクラスの使用例については、 Using Legacy Non-Dataclasses を参照してください。

2つの点を x1/y1 および x2/y2 として表すテーブル vertices へのマッピングを作成します。 Point クラスは、 composite() 構文を使ってマップされた列に関連付けられます。

以下の例は、完全な Annotated Declarative Table 設定で使用される composite() の最も現代的な形式を示しています。各列を表す mapped_column() 構文は直接 composite() に渡され、生成される列の0個以上の側面(この場合は名前)を示します。 composite() 構文は、データクラスから直接列の型(この場合は Integer に対応する int`)を導出します:

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})"

Tip

上記の例では、複合型を表す列( x1y1 など)もクラスでアクセスできますが、型チェッカーでは正しく理解されません。単一の列へのアクセスが重要な場合は、 Map columns directly, pass attribute names to composite に示すように、明示的に宣言することができます。

上記のマッピングは、次のような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) )

Working with Mapped Composite Column Types

上のセクションで説明したようなマッピングでは、 .start 属性と .end 属性が Point クラスによって参照される列を透過的に参照する Vertex クラスと、 .start 属性と .end 属性が Point クラスのインスタンスを参照する Vertex クラスのインスタンスを扱うことができます。 x1y1x2y2 列は透過的に処理されます:

  • Pointオブジェクトの保持 Vertex オブジェクトを作成し、 Point オブジェクトをメンバとして割り当てると、それらは期待通りに保持されます。: .. sourcecode:: pycon+sql

    >>> v = Vertex(start=Point(3, 4), end=Point(5, 6))
    >>> session.add(v)
    >>> session.commit()
    {execsql}BEGIN (implicit)
    INSERT INTO vertices (x1, y1, x2, y2) VALUES (?, ?, ?, ?)
    [generated in ...] (3, 4, 5, 6)
    COMMIT
  • Pointオブジェクトの列選択 composite() は、ORM Session (レガシーの Query オブジェクトを含む)を使って Point オブジェクトを選択するときに、 Vertex.start 属性と Vertex.end 属性ができるだけ単一の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.start 属性と Vertex.end 属性は、比較のためにアドホックな Point オブジェクトを使用して、WHERE基準などで使用できます。

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

    New in version 2.0: composite() 構文が、すでにサポートされている ==!= に加えて、 <>= などの”順序付け”比較をサポートするようになりました。

    Tip

    上記の”less than”演算子( < )を使用した”ordering”比較や、 == を使用した”equality”比較は、SQL式の生成に使用される場合、 Comparator クラスによって実装され、compositeクラス自体の比較メソッド、例えば、 __lt__()__eq__() メソッドを使用しません。このことから、上記のSQL操作が動作するためには、上記の”Point”データクラスもdataclasses order=True パラメータを実装する必要がないことになります。セクション Redefining Comparison Operations for Composites には、比較操作をカスタマイズする方法についての背景が含まれています。

  • 頂点インスタンス上の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

    コンポジットオブジェクトのインプレイス変更を可能にするには、 Mutation Tracking 拡張を使用する必要があります。例については Establishing Mutability on Composites を参照してください。

Other mapping forms for composites

composite() 構文には、 mapped_column() 構文、 Column 、または既存のマップされた列の文字列名を使用して、関連する列を渡すことができます。次の例は、上記のメインセクションと同等のマッピングを示しています。

Map columns directly, then pass to composite

ここでは、既存の mapped_column() インスタンスを composite() 構造体に渡します。以下の注釈なしの例では、 composite() の最初の引数として Point クラスも渡しています:

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)

Map columns directly, pass attribute names to composite

完全な列構成体の代わりに 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")

Imperative mapping and imperative table

imperative table または完全な imperative マッピングを使用する場合、 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),
    },
)

Using Legacy Non-Dataclasses

データクラスを使用しない場合、カスタムデータ型クラスの要件は、列フォーマットに対応する位置引数を受け入れるコンストラクタを持ち、列ベースの属性の順にオブジェクトの状態をリストまたはタプルとして返すメソッド __composite_values__() を提供することです。また、2つのインスタンスの等価性をテストする適切な __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() での使用は、 Other mapping forms for composites のいずれかの形式を使用して、Point クラスに関連付けられる列が明示的な型で宣言されなければならないところに進みます。

Tracking In-Place Mutations on Composites

既存の複合型の値に対するその場での変更は自動的には追跡されません。代わりに、複合型クラスは明示的にその親オブジェクトにイベントを提供する必要があります。このタスクは MutableComposite ミックスインの使用によって大部分が自動化されています。このミックスインはイベントを使用して、各ユーザ定義の複合型オブジェクトをすべての親の関連付けに関連付けます。 Establishing Mutability on Composites の例を参照してください。

Redefining Comparison Operations for Composites

“equals”比較演算は、デフォルトでは、互いに等しいすべての対応する列のANDを生成します。これは、既存または新規の演算を定義するためにカスタムの Comparator クラスを指定して、 composite() への comparator_factory 引数を使用して変更できます。以下では、基底の「より大きい」が行うのと同じ式を実装して、「より大きい」演算子を説明します:

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

Nesting Composites

必要に応じて動作するように複合クラス内の動作を再定義し、複合クラスを個々の列の全長に通常どおりマッピングすることで、複合オブジェクトを単純なネストされたスキームで動作するように定義できます。そのためには、”ネストされた”フォームと”フラットな”フォームの間を移動する追加のメソッドを定義する必要があります。

以下では、 Vertex クラスを再編成して、それ自体を Point オブジェクトを参照する複合オブジェクトにします。 VertexPoint はデータクラスにすることができますが、4つの列値を与えられた新しい Vertex オブジェクトを作成するために使用できるカスタム構築メソッドを Vertex に追加します。このメソッドは任意に _generate() という名前を付け、 Vertex._generate() メソッドに値を渡すことによって新しい Vertex オブジェクトを作成できるようにクラスメソッドとして定義します。

また、 __composite_values__() メソッドメソッドは composite() 構文(以前 Using Legacy Non-Dataclasses で紹介されました)で認識される固定名で、オブジェクトをカラム値のフラットなタプルとして受け取る標準的な方法を示します。この場合、通常のデータクラス指向の方法よりも優先されます。

カスタムの _generate() コンストラクタと __composite_values__() シリアライザメソッドを使用すると、列のフラットなタプルと Point インスタンスを含む Vertex オブジェクトの間を移動できます。 Vertex._generate メソッドは、新しい Vertex インスタンスのソースとして composite() 構文の最初の引数として渡され、 __composite_values__() メソッドは composite() によって暗黙的に使用されます。

この例では、 Vertex 合成は HasVertex と呼ばれるクラスにマップされます。このクラスには、4つのソース列を含む 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)

Composite API

Object Name Description

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

Return a composite column-based property for use with a 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, info: _InfoType | None = None, doc: str | None = None, **__kw: Any) Composite[Any]

Return a composite column-based property for use with a Mapper.

See the mapping documentation section Composite Column Types for a full usage example.

The MapperProperty returned by composite() is the Composite.

Parameters:
  • class_ – The “composite type” class, or any classmethod or callable which will produce a new instance of the composite object given the column values in order.

  • *attrs

    List of elements to be mapped, which may include:

    • Column objects

    • mapped_column() constructs

    • string names of other attributes on the mapped class, which may be any other SQL or object-mapped attribute. This can for example allow a composite that refers to a many-to-one relationship

  • active_history=False – When True, indicates that the “previous” value for a scalar attribute should be loaded when replaced, if not already loaded. See the same flag on column_property().

  • group – A group name for this property when marked as deferred.

  • deferred – When True, the column property is “deferred”, meaning that it does not load immediately, and is instead loaded when the attribute is first accessed on an instance. See also deferred().

  • comparator_factory – a class which extends Comparator which provides custom SQL clause generation for comparison operations.

  • doc – optional string that will be applied as the doc on the class-bound descriptor.

  • info – Optional data dictionary which will be populated into the MapperProperty.info attribute of this object.

  • init – Specific to Declarative Dataclass Mapping, specifies if the mapped attribute should be part of the __init__() method as generated by the dataclass process.

  • repr – Specific to Declarative Dataclass Mapping, specifies if the mapped attribute should be part of the __repr__() method as generated by the dataclass process.

  • default_factory – Specific to Declarative Dataclass Mapping, specifies a default-value generation function that will take place as part of the __init__() method as generated by the dataclass process.

  • compare

    Specific to Declarative Dataclass Mapping, indicates if this field should be included in comparison operations when generating the __eq__() and __ne__() methods for the mapped class.

    New in version 2.0.0b4.

  • kw_only – Specific to Declarative Dataclass Mapping, indicates if this field should be marked as keyword-only when generating the __init__().