Типы составных колонн

Наборы столбцов могут быть связаны с одним определяемым пользователем типом данных, который в современном использовании обычно представляет собой класс_данных Python. ORM предоставляет единственный атрибут, который представляет группу столбцов с помощью предоставленного вами класса.

Простой пример представляет пары столбцов Integer в виде объекта Point с атрибутами .x и .y. Используя класс данных, эти атрибуты определяются с помощью соответствующего типа int Python:

import dataclasses


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

Формы, не являющиеся классами данных, также принимаются, но требуют реализации дополнительных методов. Пример использования класса, не являющегося классом данных, приведен в разделе Использование унаследованных недатаклассов.

Добавлено в версии 2.0: Конструкция composite() полностью поддерживает классы данных Python, включая возможность выводить сопоставленные типы данных столбцов из составного класса.

Мы создадим отображение на таблицу vertices, которая представляет две точки как x1/y1 и x2/y2. Класс Point связан с отображаемыми столбцами с помощью конструкции composite().

Приведенный ниже пример иллюстрирует самую современную форму composite(), используемую с полностью Annotated Declarative Table конфигурацией. Конструкции mapped_column(), представляющие каждый столбец, передаются непосредственно в composite(), указывая ноль или более аспектов генерируемых столбцов, в данном случае имена; конструкция composite() извлекает типы столбцов (в данном случае 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})"

Приведенное выше отображение будет соответствовать оператору CREATE TABLE в виде:

>>> from sqlalchemy.schema import CreateTable
>>> print(CreateTable(Vertex.__table__))
{printsql}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. Колонки x1, y1, x2 и y2 обрабатываются прозрачно:

  • Существующие точечные объекты

    Мы можем создать объект Vertex, назначить объекты Point в качестве членов, и они будут сохранены, как и ожидалось:

    >>> 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
  • Выбор точечных объектов в качестве столбцов.

    composite() позволит атрибутам Vertex.start и Vertex.end вести себя как одно SQL-выражение в максимально возможной степени при использовании ORM Session (включая унаследованный объект Query) для выбора объектов Point:

    >>> stmt = select(Vertex.start, Vertex.end)
    >>> session.execute(stmt).all()
    {execsql}SELECT vertices.x1, vertices.y1, vertices.x2, vertices.y2
    FROM vertices
    [...] ()
    {stop}[(Point(x=3, y=4), Point(x=5, y=6))]
  • Сравнение точечных объектов в выражениях SQL.

    Атрибуты Vertex.start и Vertex.end могут использоваться в критериях WHERE и подобных, используя специальные объекты Point для сравнений:

    >>> stmt = select(Vertex).where(Vertex.start == Point(3, 4)).where(Vertex.end < Point(7, 8))
    >>> session.scalars(stmt).all()
    {execsql}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)
    {stop}[Vertex(Point(x=3, y=4), Point(x=5, y=6))]

    Добавлено в версии 2.0: Конструкции composite() теперь поддерживают «упорядочивающие» сравнения, такие как <, >= и подобные, в дополнение к уже имеющейся поддержке ==, !=.

    Совет

    Сравнение «порядка» с использованием оператора «меньше чем» (<), а также сравнение «равенства» с использованием == при использовании для генерации SQL-выражений реализуются классом Comparator и не используют методы сравнения самого составного класса, например, методы __lt__() или __eq__(). Из этого следует, что вышеуказанный класс данных Point также не должен реализовывать параметр dataclasses order=True для работы вышеуказанных операций SQL. В разделе Пересмотр операций сравнения для композитов содержится информация о том, как настраивать операции сравнения.

  • Обновление объектов точек на экземплярах вершин.

    По умолчанию объект Point должен быть заменен новым объектом, чтобы изменения были обнаружены:

    >>> v1 = session.scalars(select(Vertex)).one()
    {execsql}SELECT vertices.id, vertices.x1, vertices.y1, vertices.x2, vertices.y2
    FROM vertices
    [...] ()
    {stop}
    
    >>> v1.end = Point(x=10, y=14)
    >>> session.commit()
    {execsql}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")
  • Императивное отображение и императивная таблица

    При использовании отображений 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),
        },
    )

Использование унаследованных недатаклассов

Если не используется класс данных, то требования к пользовательскому классу типа данных заключаются в том, чтобы он имел конструктор, который принимает позиционные аргументы, соответствующие формату столбцов, а также предоставлял метод __composite_values__(), который возвращает состояние объекта в виде списка или кортежа в порядке следования его атрибутов, основанных на столбцах. Он также должен предоставлять адекватные методы __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, который использует события для связывания каждого определенного пользователем составного объекта со всеми родительскими ассоциациями. Пожалуйста, посмотрите пример в Установление мутабельности композитов.

Пересмотр операций сравнения для композитов

Операция сравнения «равно» по умолчанию производит И всех соответствующих столбцов, приравненных друг к другу. Это можно изменить с помощью аргумента comparator_factory в composite(), где мы указываем пользовательский класс 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))
{printsql}vertices.x1 > :x1_1 AND vertices.y1 > :y1_1

Композиты для раскроя

Составные объекты могут быть определены для работы в простых вложенных схемах путем переопределения поведения внутри составного класса для работы по желанию, а затем отображения составного класса на всю длину отдельных столбцов обычным образом. Это требует определения дополнительных методов для перехода между «вложенной» и «плоской» формами.

Ниже мы реорганизуем класс Vertex, чтобы он сам был составным объектом, который ссылается на объекты Point. Vertex и Point могут быть классами данных, однако мы добавим к Vertex пользовательский метод построения, который может быть использован для создания новых объектов Vertex, заданных значениями четырех столбцов, которые мы произвольно назовем _generate() и определим как метод класса, чтобы мы могли создавать новые объекты Vertex, передавая значения в метод Vertex._generate().

Мы также реализуем метод __composite_values__(), который представляет собой фиксированное имя, распознаваемое конструкцией composite() (представленной ранее в Использование унаследованных недатаклассов), указывающее на стандартный способ получения объекта в виде плоского кортежа значений столбцов, что в данном случае заменит обычную методологию, ориентированную на работу с классами данных.

С помощью нашего пользовательского конструктора _generate() и метода сериализатора __composite_values__() мы теперь можем перемещаться между плоским кортежем столбцов и объектами Vertex, содержащими экземпляры Point. Метод 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")

Приведенное выше отображение можно использовать в терминах HasVertex, Vertex и Point:

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

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)

Возвращает составное свойство на основе столбцов для использования с Mapper.

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

Возвращает составное свойство на основе столбцов для использования с Mapper.

Полный пример использования см. в разделе документации по отображению Типы составных колонн.

MapperProperty, возвращаемый composite(), является Composite.

Параметры:
  • class_ – Класс «составной тип» или любой класс-метод или вызываемый объект, который создаст новый экземпляр составного объекта, учитывая значения столбцов по порядку.

  • *attrs – Список отображаемых элементов, которые могут включать: * Column объекты * mapped_column() конструкции * строковые имена других атрибутов сопоставленного класса, которые могут быть любыми другими SQL или объектно-сопоставленными атрибутами. Это может, например, позволить композиту, который ссылается на отношения «многие-к-одному».

  • active_history=False – При True указывает, что при замене скалярного атрибута должно быть загружено «предыдущее» значение, если оно еще не загружено. См. тот же флаг для column_property().

  • group – Имя группы для этого свойства, когда оно помечено как отложенное.

  • deferred – При значении True свойство колонки является «отложенным», то есть оно не загружается немедленно, а загружается при первом обращении к атрибуту экземпляра. См. также deferred().

  • comparator_factory – класс, расширяющий Comparator, который обеспечивает генерацию пользовательских предложений SQL для операций сравнения.

  • doc – необязательная строка, которая будет применяться в качестве doc для дескриптора, связанного с классом.

  • info – Необязательный словарь данных, который будет заполнен в атрибут MapperProperty.info этого объекта.

  • init – Специфично для Декларативное отображение классов данных, указывает, должен ли сопоставленный атрибут быть частью метода __init__(), создаваемого процессом класса данных.

  • repr – Специфично для Декларативное отображение классов данных, указывает, должен ли сопоставленный атрибут быть частью метода __repr__(), создаваемого процессом класса данных.

  • default_factory – Специфично для Декларативное отображение классов данных, определяет функцию генерации значения по умолчанию, которая будет иметь место как часть метода __init__(), генерируемого процессом dataclass.

  • compare – Специфично для Декларативное отображение классов данных, указывает, следует ли включать это поле в операции сравнения при генерации методов __eq__() и __ne__() для сопоставленного класса. … versionadded:: 2.0.0b4

  • kw_only – Специфично для Декларативное отображение классов данных, указывает, должно ли это поле быть помечено как предназначенное только для ключевого слова при генерации __init__().

Вернуться на верх