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

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

Простой пример представляет пары столбцов в виде объекта Point. Point представляет такую пару как .x и .y:

class Point(object):
    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_values__(), который возвращает состояние объекта в виде списка или кортежа в порядке следования его атрибутов, основанных на столбцах. Он также должен предоставлять адекватные методы __eq__() и __ne__(), которые проверяют равенство двух экземпляров.

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

from sqlalchemy import Column, Integer
from sqlalchemy.orm import composite, declarative_base

Base = declarative_base()


class Vertex(Base):
    __tablename__ = "vertices"

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

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

Классическое отображение выше определило бы каждую 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),
    },
)

Теперь мы можем сохранять и использовать экземпляры Vertex, а также запрашивать их, используя атрибуты .start и .end против специальных экземпляров Point:

>>> v = Vertex(start=Point(3, 4), end=Point(5, 6))
>>> session.add(v)
>>> q = session.query(Vertex).filter(Vertex.start == Point(3, 4))
sql>>> print(q.first().start)
Point(x=3, y=4)

Отслеживание мутаций на месте на композитах

Изменения на месте существующего составного значения не отслеживаются автоматически. Вместо этого композитный класс должен явно предоставлять события своему родительскому объекту. Эта задача в значительной степени автоматизирована с помощью миксина MutableComposite, который использует события для связывания каждого определенного пользователем составного объекта со всеми родительскими ассоциациями. Пожалуйста, посмотрите пример в mutable_composites.

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

Операция сравнения «равно» по умолчанию производит И всех соответствующих столбцов, приравненных друг к другу. Это можно изменить с помощью аргумента comparator_factory в composite(), где мы указываем пользовательский класс Comparator для определения существующих или новых операций. Ниже мы проиллюстрируем оператор «больше чем», реализующий то же выражение, что и базовый «больше чем»:

from sqlalchemy import sql
from sqlalchemy.orm.properties import CompositeProperty


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

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


class Vertex(Base):
    __tablename__ = "vertices"

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

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

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

Составные объекты могут быть определены для работы в простых вложенных схемах, переопределяя поведение внутри составного класса для работы по желанию, а затем отображая составной класс на всю длину отдельных столбцов обычным образом. Как правило, удобно определить отдельные конструкторы для пользовательского использования и для использования генерации из строки. Ниже мы реорганизуем класс Vertex, чтобы он сам стал составным объектом, который затем отображается на класс HasVertex:

from sqlalchemy.orm import composite


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)


class Vertex:
    def __init__(self, start, end):
        self.start = start
        self.end = end

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

    def __composite_values__(self):
        return self.start.__composite_values__() + self.end.__composite_values__()


class HasVertex(Base):
    __tablename__ = "has_vertex"
    id = Column(Integer, primary_key=True)
    x1 = Column(Integer)
    y1 = Column(Integer)
    x2 = Column(Integer)
    y2 = Column(Integer)

    vertex = composite(Vertex._generate, x1, y1, x2, y2)

Затем мы можем использовать приведенное выше отображение как:

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

s.add(hv)
s.commit()

hv = (
    s.query(HasVertex)
    .filter(HasVertex.vertex == Vertex(Point(1, 2), Point(3, 4)))
    .first()
)
print(hv.vertex.start)
print(hv.vertex.end)
Вернуться на верх