Composite Column Types

Gruppen von Spalten können einem einzelnen benutzerdefinierten Datentyp zugeordnet werden, der in der modernen Verwendung normalerweise eine Python dataclass ist. Die ORM bietet ein einzelnes Attribut, das die Gruppe von Spalten unter Verwendung der von Ihnen bereitgestellten Klasse darstellt.

Ein einfaches Beispiel stellt Paare von Integer-Spalten als Point-Objekt dar, mit den Attributen .x und .y. Bei Verwendung einer Dataclass werden diese Attribute mit dem entsprechenden int-Python-Typ definiert.

import dataclasses


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

Nicht-Dataclass-Formen werden ebenfalls akzeptiert, erfordern jedoch die Implementierung zusätzlicher Methoden. Ein Beispiel für die Verwendung einer Klasse, die keine Dataclass ist, finden Sie im Abschnitt Verwendung von Legacy Non-Dataclasses.

Neu in Version 2.0: Das Konstrukt composite() unterstützt Python-Dataclasses vollständig, einschließlich der Fähigkeit, gemappte Spaltendatentypen aus der Composite-Klasse abzuleiten.

Wir erstellen ein Mapping zu einer Tabelle vertices, die zwei Punkte als x1/y1 und x2/y2 darstellt. Die Klasse Point wird mit dem Konstrukt composite() den zugeordneten Spalten zugeordnet.

Das folgende Beispiel veranschaulicht die modernste Form von composite(), wie sie mit einer vollständig annotierten deklarativen Tabelle-Konfiguration verwendet wird. Konstrukte von mapped_column(), die jede Spalte darstellen, werden direkt an composite() übergeben, was null oder mehr Aspekte der zu generierenden Spalten angibt, in diesem Fall die Namen; das Konstrukt composite() leitet die Spaltentypen (in diesem Fall int, entsprechend Integer) direkt von der Dataclass ab.

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

Tipp

Im obigen Beispiel sind die Spalten, die die Composites (x1, y1 usw.) darstellen, auch auf der Klasse zugänglich, werden aber von Typ-Checkern nicht korrekt verstanden. Wenn der Zugriff auf die einzelnen Spalten wichtig ist, können sie explizit deklariert werden, wie in Spalten direkt zuordnen, Attributnamen an Composite übergeben gezeigt.

Das obige Mapping würde einer CREATE TABLE-Anweisung wie folgt entsprechen:

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

Arbeiten mit Mapped Composite Column Types

Bei einem Mapping, wie im oberen Abschnitt dargestellt, können wir mit der Klasse Vertex arbeiten, wobei die Attribute .start und .end transparent auf die von der Klasse Point referenzierten Spalten verweisen, sowie mit Instanzen der Klasse Vertex, wobei die Attribute .start und .end auf Instanzen der Klasse Point verweisen. Die Spalten x1, y1, x2 und y2 werden transparent behandelt.

  • Speichern von Point-Objekten

    Wir können ein Vertex-Objekt erstellen, Point-Objekte als Mitglieder zuweisen, und diese werden wie erwartet gespeichert.

    >>> 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
  • Auswählen von Point-Objekten als Spalten

    composite() ermöglicht es den Attributen Vertex.start und Vertex.end, sich so weit wie möglich wie ein einzelner SQL-Ausdruck zu verhalten, wenn die ORM- Session (einschließlich des Legacy-Objekts Query) verwendet wird, um Point-Objekte auszuwählen.

    >>> 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))]
  • Vergleichen von Point-Objekten in SQL-Ausdrücken

    Die Attribute Vertex.start und Vertex.end können in WHERE-Klauseln und ähnlichem verwendet werden, wobei Ad-hoc- Point-Objekte für Vergleiche verwendet werden.

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

    Neu in Version 2.0: composite()-Konstrukte unterstützen jetzt "Sortiervergleiche" wie <, >= und ähnliche, zusätzlich zur bereits vorhandenen Unterstützung für ==, !=.

    Tipp

    Der obige "Sortiervergleich" mit dem Operator "kleiner als" (<) sowie der "Gleichheitsvergleich" mit == werden, wenn sie zur Erzeugung von SQL-Ausdrücken verwendet werden, von der Klasse Comparator implementiert und nutzen nicht die Vergleichsmethoden der Composite-Klasse selbst, z.B. die Methoden __lt__() oder __eq__(). Daraus folgt, dass die obige Point-Dataclass auch den Parameter order=True von Dataclasses nicht implementieren muss, damit die obigen SQL-Operationen funktionieren. Der Abschnitt Neudefinition von Vergleichsoperationen für Composites enthält Hintergrundinformationen zur Anpassung der Vergleichsoperationen.

  • Aktualisieren von Point-Objekten auf Vertex-Instanzen

    Standardmäßig **muss** das Point-Objekt **durch ein neues Objekt ersetzt werden**, damit Änderungen erkannt werden.

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

    Um In-Place-Änderungen am Composite-Objekt zu ermöglichen, muss die Erweiterung Mutationsverfolgung verwendet werden. Beispiele finden Sie im Abschnitt Mutationsfähigkeit auf Composites etablieren.

Andere Mapping-Formen für Composites

Das Konstrukt composite() kann die relevanten Spalten über ein Konstrukt von mapped_column(), eine Column oder den Zeichenkettennamen einer vorhandenen gemappten Spalte übergeben bekommen. Die folgenden Beispiele veranschaulichen ein äquivalentes Mapping wie das im Hauptabschnitt oben.

Spalten direkt zuordnen, dann an Composite übergeben

Hier übergeben wir die vorhandenen Instanzen von mapped_column() an das Konstrukt composite(), wie im untenstehenden nicht-annotierten Beispiel, wo wir auch die Klasse Point als erstes Argument an composite() übergeben.

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)

Spalten direkt zuordnen, Attributnamen an Composite übergeben

Wir können das gleiche Beispiel wie oben mit annotierteren Formen schreiben, bei denen wir die Möglichkeit haben, Attributnamen an composite() anstelle von vollständigen Spaltenkonstrukten zu übergeben.

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 und imperative Tabelle

Bei Verwendung von imperativer Tabelle oder vollständig imperativen Mappings haben wir direkten Zugriff auf Column-Objekte. Diese können ebenfalls an composite() übergeben werden, wie im folgenden imperativen Beispiel gezeigt.

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

Verwendung von Legacy Non-Dataclasses

Wenn keine Dataclass verwendet wird, müssen die Anforderungen an die benutzerdefinierte Datentypklasse sein, dass sie über einen Konstruktor verfügt, der Positionsargumente entsprechend ihrem Spaltenformat akzeptiert, und außerdem eine Methode __composite_values__() bereitstellt, die den Zustand des Objekts als Liste oder Tupel zurückgibt, in der Reihenfolge seiner spaltenbasierten Attribute. Sie sollte auch ausreichende __eq__()- und __ne__()-Methoden liefern, die die Gleichheit zweier Instanzen testen.

Um die äquivalente Klasse Point aus dem Hauptabschnitt ohne Dataclass zu illustrieren.

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)

Die Verwendung mit composite() erfolgt dann so, dass die der Klasse Point zuzuordnenden Spalten ebenfalls mit expliziten Typen deklariert werden müssen, wobei eine der Formen unter Andere Mapping-Formen für Composites verwendet wird.

Verfolgung von In-Place-Mutationen an Composites

In-Place-Änderungen an einem vorhandenen Composite-Wert werden nicht automatisch verfolgt. Stattdessen muss die Composite-Klasse explizit Ereignisse an ihr übergeordnetes Objekt senden. Diese Aufgabe wird weitgehend durch die Verwendung des Mixins MutableComposite automatisiert, der Ereignisse verwendet, um jedes benutzerdefinierte Composite-Objekt mit allen übergeordneten Zuordnungen zu verknüpfen. Bitte beachten Sie das Beispiel unter Mutationsfähigkeit auf Composites etablieren.

Neudefinition von Vergleichsoperationen für Composites

Die Vergleichsoperation "gleich" erzeugt standardmäßig ein AND aller entsprechenden Spalten, die miteinander gleichgesetzt werden. Dies kann mit dem Argument comparator_factory zu composite() geändert werden, wobei wir eine benutzerdefinierte Comparator-Klasse angeben, um vorhandene oder neue Operationen zu definieren. Nachfolgend illustrieren wir den Operator "größer als", der denselben Ausdruck implementiert wie der Basisoperator "größer als".

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
    )

Da Point eine Dataclass ist, können wir dataclasses.astuple() verwenden, um eine Tupelform von Point-Instanzen zu erhalten.

Der benutzerdefinierte Comparator gibt dann den entsprechenden SQL-Ausdruck zurück.

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

Verschachtelte Composites

Composite-Objekte können für die Arbeit in einfachen verschachtelten Schemata definiert werden, indem Verhaltensweisen innerhalb der Composite-Klasse neu definiert werden, um wie gewünscht zu funktionieren, und dann die Composite-Klasse normal auf die volle Länge einzelner Spalten abgebildet wird. Dies erfordert, dass zusätzliche Methoden zum Wechseln zwischen "verschachtelten" und "flachen" Formen definiert werden.

Im Folgenden reorganisieren wir die Klasse Vertex so, dass sie selbst ein Composite-Objekt ist, das sich auf Point-Objekte bezieht. Vertex und Point können Dataclasses sein, jedoch fügen wir der Klasse Vertex eine benutzerdefinierte Konstruktionsmethode hinzu, die verwendet werden kann, um neue Vertex-Objekte zu erstellen, die vier Spaltenwerte erhalten, die wir willkürlich _generate() nennen und als Klassenmethode definieren werden, damit wir neue Vertex-Objekte erstellen können, indem wir Werte an die Methode Vertex._generate() übergeben.

Wir implementieren auch die Methode __composite_values__(), ein fester Name, der vom Konstrukt composite() erkannt wird (zuvor unter Verwendung von Legacy Non-Dataclasses eingeführt) und eine standardmäßige Methode zum Empfangen des Objekts als flaches Tupel von Spaltenwerten anzeigt, was in diesem Fall die übliche Dataclass-orientierte Methodik überschreiben wird.

Mit unserem benutzerdefinierten Konstruktor _generate() und der Serialisierungsmethode __composite_values__() können wir nun zwischen einem flachen Tupel von Spalten und Vertex-Objekten wechseln, die Point-Instanzen enthalten. Die Methode Vertex._generate wird als erstes Argument an das Konstrukt composite() als Quelle für neue Vertex-Instanzen übergeben, und die Methode __composite_values__() wird implizit von composite() verwendet.

Für die Zwecke des Beispiels wird der Vertex-Composite dann auf eine Klasse namens HasVertex abgebildet, wo sich letztendlich die Table befindet, die die vier Quellspalten enthält.

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

Das obige Mapping kann dann im Hinblick auf HasVertex, Vertex und Point verwendet werden.

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

Objektname Beschreibung

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

Gibt eine Composite-Spalten-basierte Eigenschaft für die Verwendung mit einem Mapper zurück.

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]

Gibt eine Composite-Spalten-basierte Eigenschaft für die Verwendung mit einem Mapper zurück.

Siehe den Mapping-Abschnitt Composite Column Types für ein vollständiges Nutzungsbeispiel.

Die von composite() zurückgegebene MapperProperty ist die Composite.

Parameter:
  • class_ – Die "Composite-Typ"-Klasse oder jede Klassenmethode oder aufrufbare Funktion, die eine neue Instanz des Composite-Objekts unter Angabe der Spaltenwerte in der richtigen Reihenfolge erzeugt.

  • *attrs

    Liste der abzubildenden Elemente, die enthalten können

    • Column-Objekte

    • mapped_column()-Konstrukte

    • Zeichenkettennamen anderer Attribute der gemappten Klasse, die beliebige andere SQL- oder objektgemappte Attribute sein können. Dies kann zum Beispiel einen Composite ermöglichen, der sich auf eine Many-to-One-Beziehung bezieht.

  • active_history=False – Wenn True, gibt an, dass der "vorherige" Wert für ein Skalarattribut geladen werden soll, wenn es ersetzt wird, falls es noch nicht geladen ist. Siehe das gleiche Flag bei column_property().

  • group – Ein Gruppennamen für diese Eigenschaft, wenn sie als verzögert markiert ist.

  • deferred – Wenn True, ist die Spalteneigenschaft "verzögert", was bedeutet, dass sie nicht sofort geladen wird, sondern erst, wenn auf das Attribut auf einer Instanz zugegriffen wird. Siehe auch deferred().

  • comparator_factory – eine Klasse, die Comparator erweitert und benutzerdefinierte SQL-Klauselerzeugung für Vergleichsoperationen bereitstellt.

  • doc – optionaler String, der als Docstring für den klassengebundenen Deskriptor angewendet wird.

  • info – Optionales Datenwörterbuch, das in das Attribut MapperProperty.info dieses Objekts eingefügt wird.

  • init – Spezifisch für Deklaratives Dataclass-Mapping, gibt an, ob das gemappte Attribut Teil der von der Dataclass generierten Methode __init__() sein soll.

  • repr – Spezifisch für Deklaratives Dataclass-Mapping, gibt an, ob das gemappte Attribut Teil der von der Dataclass generierten Methode __repr__() sein soll.

  • default_factory – Spezifisch für Deklaratives Dataclass-Mapping, gibt eine Funktion zur Generierung von Standardwerten an, die als Teil der von der Dataclass generierten Methode __init__() ausgeführt wird.

  • compare

    Spezifisch für Deklaratives Mapping von Datenklassen, gibt an, ob dieses Feld bei der Generierung der Methoden __eq__() und __ne__() für die zugeordnete Klasse in Vergleichsoperationen einbezogen werden soll.

    Neu ab Version 2.0.0b4.

  • kw_only – Spezifisch für Deklarative Dataclass-Zuordnung, gibt an, ob dieses Feld beim Generieren von __init__() als nur-Schlüsselwort gekennzeichnet werden soll.

  • hash

    Spezifisch für Deklaratives Mapping von Datenklassen, steuert, ob dieses Feld bei der Generierung der __hash__()-Methode für die zugeordnete Klasse einbezogen wird.

    Neu in Version 2.0.36.