Основные модели взаимоотношений

Краткое описание основных реляционных моделей, которые в этом разделе иллюстрируются с помощью отображений в стиле Declarative, основанных на использовании типа аннотации Mapped.

Настройка для каждого из следующих разделов выглядит следующим образом:

from __future__ import annotations
from typing import List

from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship


class Base(DeclarativeBase):
    pass

Декларативные и императивные формы

По мере развития SQLAlchemy появились различные стили конфигурирования ORM. Для примеров в этом разделе и других, использующих аннотированные отображения Declarative с Mapped, соответствующая неаннотированная форма должна использовать желаемый класс или строковое имя класса в качестве первого аргумента, передаваемого в relationship(). Пример ниже иллюстрирует форму, используемую в этом документе, которая является полностью декларативным примером с использованием аннотаций PEP 484, где конструкция relationship() также выводит целевой класс и тип коллекции из аннотации Mapped, которая является самой современной формой декларативного отображения SQLAlchemy:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship(back_populates="parent")


class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
    parent: Mapped["Parent"] = relationship(back_populates="children")

В отличие от этого, использование декларативного отображения без аннотаций является более «классической» формой отображения, где relationship() требует, чтобы все параметры передавались ему напрямую, как в примере ниже:

class Parent(Base):
    __tablename__ = "parent_table"

    id = mapped_column(Integer, primary_key=True)
    children = relationship("Child", back_populates="parent")


class Child(Base):
    __tablename__ = "child_table"

    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(ForeignKey("parent_table.id"))
    parent = relationship("Parent", back_populates="children")

Наконец, используя Imperative Mapping, которая является оригинальной формой отображения SQLAlchemy до создания Declarative (которая, тем не менее, остается предпочтительной для волевого меньшинства пользователей), приведенная выше конфигурация выглядит так:

registry.map_imperatively(
    Parent,
    parent_table,
    properties={"children": relationship("Child", back_populates="parent")},
)

registry.map_imperatively(
    Child,
    child_table,
    properties={"parent": relationship("Parent", back_populates="children")},
)

Кроме того, стилем коллекции по умолчанию для неаннотированных отображений является list. Чтобы использовать set или другую коллекцию без аннотаций, укажите ее с помощью параметра relationship.collection_class:

class Parent(Base):
    __tablename__ = "parent_table"

    id = mapped_column(Integer, primary_key=True)
    children = relationship("Child", collection_class=set, ...)

Подробная информация о конфигурации коллекции для relationship() находится в Настройка доступа к коллекции.

Дополнительные различия между аннотированным и неаннотированным / императивным стилями будут отмечены по мере необходимости.

Один ко многим

Отношения «один ко многим» устанавливают внешний ключ в дочерней таблице, ссылающийся на родительскую. Затем relationship() указывается на родителя как ссылка на коллекцию элементов, представленных дочерней таблицей:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship()


class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))

Чтобы установить двунаправленное отношение один-ко-многим, где «обратная» сторона является отношением многие-к-одному, укажите дополнительный relationship() и соедините их с помощью параметра relationship.back_populates, используя имя атрибута каждого relationship() в качестве значения для relationship.back_populates на другой стороне:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship(back_populates="parent")


class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
    parent: Mapped["Parent"] = relationship(back_populates="children")

Child получит атрибут parent с семантикой «многие к одному».

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

При использовании аннотированных декларативных отображений тип коллекции, используемый для relationship(), получается из типа коллекции, переданного типу контейнера Mapped. Пример из предыдущего раздела может быть написан так, чтобы использовать set, а не list для коллекции Parent.children, используя Mapped[Set["Child"]]:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[Set["Child"]] = relationship(back_populates="parent")

При использовании неаннотированных форм, включая императивные отображения, класс Python для использования в качестве коллекции может быть передан с помощью параметра relationship.collection_class.

См.также

Настройка доступа к коллекции - содержит более подробную информацию о конфигурации коллекций, включая некоторые методы отображения relationship() на словари.

Настройка поведения удаления для одного ко многим

Часто бывает так, что все объекты Child должны удаляться, когда удаляется принадлежащий им объект Parent. Для настройки такого поведения используется опция каскада delete, описанная в удалить. Дополнительная опция заключается в том, что объект Child может сам удаляться, когда он деассоциирован от своего родителя. Это поведение описано в delete-orphan.

Многие к одному

Множество к одному размещает внешний ключ в родительской таблице, ссылающийся на дочернюю. relationship() объявляется на родителе, где будет создан новый атрибут, удерживающий скаляр:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    child_id: Mapped[int] = mapped_column(ForeignKey("child_table.id"))
    child: Mapped["Child"] = relationship()


class Child(Base):
    __tablename__ = "child_table"

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

В приведенном выше примере показано отношение «многие-к-одному», которое предполагает поведение без нуля; следующий раздел, Nullable Many-to-One, иллюстрирует версию с нулем.

Двунаправленное поведение достигается путем добавления второго relationship() и применения параметра relationship.back_populates в обоих направлениях, используя имя атрибута каждого relationship() как значение для relationship.back_populates на другом:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    child_id: Mapped[int] = mapped_column(ForeignKey("child_table.id"))
    child: Mapped["Child"] = relationship(back_populates="parents")


class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List["Parent"]] = relationship(back_populates="child")

Nullable Many-to-One

В предыдущем примере отношение Parent.child не типизировано как допускающее None; это следует из того, что сам столбец Parent.child_id не является нулевым, поскольку он типизирован с помощью Mapped[int]. Если мы хотим, чтобы Parent.child был nullable many-to-one, мы можем установить и Parent.child_id и Parent.child как Optional[], в этом случае конфигурация будет выглядеть так:

from typing import Optional


class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    child_id: Mapped[Optional[int]] = mapped_column(ForeignKey("child_table.id"))
    child: Mapped[Optional["Child"]] = relationship(back_populates="parents")


class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List["Parent"]] = relationship(back_populates="child")

Выше, столбец для Parent.child_id будет создан в DDL, чтобы разрешить значения NULL. При использовании mapped_column() с явными объявлениями типизации, спецификация child_id: Mapped[Optional[int]] эквивалентна установке Column.nullable в True на Column, тогда как child_id: Mapped[int] эквивалентна установке в False. См. раздел mapped_column() берет тип данных и нулевую возможность из аннотации Mapped для получения информации об этом поведении.

Совет

При использовании Python 3.10 и выше синтаксис PEP 604 удобнее обозначать необязательные типы с помощью | None, который в сочетании с PEP 563 откладывает оценку аннотации так, что типы, заключенные в строковые кавычки, не требуются, будет выглядеть так:

from __future__ import annotations


class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    child_id: Mapped[int | None] = mapped_column(ForeignKey("child_table.id"))
    child: Mapped[Child | None] = relationship(back_populates="parents")


class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List[Parent]] = relationship(back_populates="child")

Один к одному

One To One - это, по сути, отношение Один ко многим с точки зрения внешнего ключа, но указывает, что в любой момент времени будет только одна строка, ссылающаяся на определенную родительскую строку.

При использовании аннотированных отображений с Mapped, соглашение «один к одному» достигается путем применения типа не-коллекции к аннотации Mapped с обеих сторон отношения, что будет означать для ORM, что коллекция не должна использоваться с обеих сторон, как в примере ниже:

class Parent(Base):
    __tablename__ = "parent_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    child: Mapped["Child"] = relationship(back_populates="parent")


class Child(Base):
    __tablename__ = "child_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent_table.id"))
    parent: Mapped["Parent"] = relationship(back_populates="child")

Выше, когда мы загружаем объект Parent, атрибут Parent.child будет ссылаться на один объект Child, а не на коллекцию. Если мы заменим значение Parent.child новым объектом Child, то процесс единицы работы ORM заменит предыдущую строку Child новой, установив предыдущий столбец child.parent_id по умолчанию в NULL, если не задано специальное поведение cascade.

Совет

Как упоминалось ранее, ORM рассматривает шаблон «один-к-одному» как соглашение, где он делает предположение, что когда он загружает атрибут Parent.child на объект Parent, он получит только один ряд обратно. Если будет возвращено более одной строки, ORM выдаст предупреждение.

Однако, сторона Child.parent вышеуказанного отношения остается как отношение «многие-к-одному» и остается неизменной, и в самом ORM нет никакой внутренней системы, которая предотвращает создание более чем одного объекта Child против одного и того же Parent во время персистенции. Вместо этого в реальной схеме базы данных могут быть использованы такие методы, как unique constraints для обеспечения этого соглашения, где уникальное ограничение на колонку Child.parent_id гарантирует, что только одна строка Child может ссылаться на определенную строку Parent одновременно.

Добавлено в версии 2.0: Конструкция relationship() может вывести эффективное значение параметра relationship.uselist из заданной аннотации Mapped.

Установка uselist=False для неаннотированных конфигураций

При использовании relationship() без использования аннотаций Mapped, шаблон один-к-одному может быть включен с помощью параметра relationship.uselist, установленного на False на той стороне, которая обычно была бы «много», как показано ниже в неаннотированной декларативной конфигурации:

class Parent(Base):
    __tablename__ = "parent_table"

    id = mapped_column(Integer, primary_key=True)
    child = relationship("Child", uselist=False, back_populates="parent")


class Child(Base):
    __tablename__ = "child_table"

    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(ForeignKey("parent_table.id"))
    parent = relationship("Parent", back_populates="child")

Многие ко многим

Many to Many добавляет таблицу ассоциаций между двумя классами. Таблица ассоциаций почти всегда задается в виде объекта Core Table или другого выбираемого объекта Core, такого как объект Join, и указывается аргументом relationship.secondary к relationship(). Обычно в Table используется объект MetaData, связанный с декларативным базовым классом, чтобы директивы ForeignKey могли найти удаленные таблицы, с которыми нужно установить связь:

from __future__ import annotations

from sqlalchemy import Column
from sqlalchemy import Table
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship


class Base(DeclarativeBase):
    pass


# note for a Core table, we use the sqlalchemy.Column construct,
# not sqlalchemy.orm.mapped_column
association_table = Table(
    "association_table",
    Base.metadata,
    Column("left_id", ForeignKey("left_table.id")),
    Column("right_id", ForeignKey("right_table.id")),
)


class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List[Child]] = relationship(secondary=association_table)


class Child(Base):
    __tablename__ = "right_table"

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

Совет

В приведенной выше «таблице ассоциации» установлены ограничения внешнего ключа, которые ссылаются на две таблицы сущностей по обе стороны связи. Тип данных каждого из association.left_id и association.right_id обычно определяется из типа данных ссылающейся таблицы и может быть опущен. Также рекомендуется, хотя SQLAlchemy ни в коем случае не требует, чтобы столбцы, которые ссылаются на две таблицы сущностей, были установлены либо в уникальном ограничении, либо, как правило, в ограничении первичного ключа; это гарантирует, что дубликаты строк не будут сохраняться в таблице независимо от проблем на стороне приложения:

association_table = Table(
    "association_table",
    Base.metadata,
    Column("left_id", ForeignKey("left_table.id"), primary_key=True),
    Column("right_id", ForeignKey("right_table.id"), primary_key=True),
)

Настройка двунаправленных «многие-ко-многим

Для двунаправленного отношения обе стороны отношения содержат коллекцию. Укажите, используя relationship.back_populates, и для каждой relationship() укажите общую таблицу ассоциации:

from __future__ import annotations

from sqlalchemy import Column
from sqlalchemy import Table
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship


class Base(DeclarativeBase):
    pass


association_table = Table(
    "association_table",
    Base.metadata,
    Column("left_id", ForeignKey("left_table.id"), primary_key=True),
    Column("right_id", ForeignKey("right_table.id"), primary_key=True),
)


class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List[Child]] = relationship(
        secondary=association_table, back_populates="parents"
    )


class Child(Base):
    __tablename__ = "right_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List[Parent]] = relationship(
        secondary=association_table, back_populates="children"
    )

Использование формы с поздней оценкой для «вторичного» аргумента

Параметр relationship.secondary в relationship() также принимает две различные формы «поздней оценки», включая строковое имя таблицы, а также лямбда-вызов. Информацию и примеры см. в разделе Использование формы с поздней оценкой для «вторичного» аргумента многие-ко-многим.

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

Конфигурация коллекций для отношения Many to Many идентична конфигурации Один ко многим, как описано в Использование наборов, списков или других типов коллекций для «один ко многим. Для аннотированного отображения, использующего Mapped, коллекция может быть указана типом коллекции, используемой в рамках общего класса Mapped, например set:

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[Set["Child"]] = relationship(secondary=association_table)

При использовании неаннотированных форм, включая императивные отображения, как в случае с one-to-many, класс Python для использования в качестве коллекции может быть передан с помощью параметра relationship.collection_class.

См.также

Настройка доступа к коллекции - содержит более подробную информацию о конфигурации коллекций, включая некоторые методы отображения relationship() на словари.

Удаление строк из таблицы «Многие ко многим

Поведение, уникальное для аргумента relationship.secondary в relationship(), заключается в том, что указанная здесь таблица Table автоматически подвергается операциям INSERT и DELETE, поскольку объекты добавляются или удаляются из коллекции. Нет необходимости удалять из этой таблицы вручную. Акт удаления записи из коллекции будет иметь эффект удаления строки на flush:

# row will be deleted from the "secondary" table
# automatically
myparent.children.remove(somechild)

Часто возникает вопрос, как можно удалить строку во «вторичной» таблице, когда дочерний объект передается непосредственно в Session.delete():

session.delete(somechild)

Здесь есть несколько вариантов:

  • Если есть relationship() от Parent к Child, но нет **** обратной связи, которая связывает конкретный Child с каждым Parent, то SQLAlchemy не будет знать, что при удалении этого конкретного объекта Child нужно сохранить «вторичную» таблицу, которая связывает его с Parent. Удаление «вторичной» таблицы не произойдет.

  • Если существует отношение, связывающее конкретный Child с каждым Parent, предположим, он называется Child.parents, SQLAlchemy по умолчанию загрузит коллекцию Child.parents, чтобы найти все объекты Parent, и удалит каждую строку из «вторичной» таблицы, которая устанавливает эту связь. Обратите внимание, что эта связь не обязательно должна быть двунаправленной; SQLAlchemy строго просматривает каждый relationship(), связанный с удаляемым Child объектом.

  • Более эффективным вариантом здесь является использование директив ON DELETE CASCADE для внешних ключей, используемых базой данных. Если база данных поддерживает эту функцию, то можно заставить саму базу данных автоматически удалять строки во «вторичной» таблице по мере удаления ссылающихся на нее строк в «дочерней». SQLAlchemy можно дать указание отказаться от активной загрузки коллекции Child.parents в этом случае, используя директиву relationship.passive_deletes на relationship(); подробнее об этом см. в Использование каскада внешних ключей ON DELETE с отношениями ORM.

Заметьте еще раз, что это поведение относится только к опции relationship.secondary, используемой с relationship(). Если вы имеете дело с таблицами ассоциаций, которые отображаются явно и не присутствуют в опции relationship.secondary соответствующей опции relationship(), вместо этого можно использовать каскадные правила для автоматического удаления сущностей в ответ на удаление связанной сущности - смотрите Каскады для информации об этой возможности.

Объект ассоциации

Шаблон объекта ассоциации является вариантом шаблона многие-ко-многим: он используется, когда таблица ассоциации содержит дополнительные столбцы, помимо тех, которые являются внешними ключами родительской и дочерней (или левой и правой) таблиц, столбцы, которые лучше всего отобразить на собственный отображаемый класс ORM. Этот сопоставленный класс сопоставляется с Table, которые в противном случае были бы отмечены как relationship.secondary при использовании шаблона many-to-many.

В шаблоне ассоциативного объекта параметр relationship.secondary не используется; вместо этого класс сопоставляется непосредственно с таблицей ассоциаций. Затем две отдельные конструкции relationship() связывают сначала родительскую сторону с сопоставленным классом ассоциации через один ко многим, а затем сопоставленный класс ассоциации с дочерней стороной через многие к одному, формируя однонаправленную связь объекта ассоциации от родителя к ассоциации и от ассоциации к ребенку. Для двунаправленных отношений используются четыре конструкции relationship() для связи сопоставленного класса ассоциации с родительской и дочерней сторонами в обоих направлениях.

В приведенном ниже примере показан новый класс Association, который отображается на таблицу Table с именем association; эта таблица теперь включает дополнительный столбец extra_data, который представляет собой строковое значение, хранящееся вместе с каждой ассоциацией между Parent и Child. Благодаря отображению таблицы на явный класс, рудиментарный доступ от Parent к Child явно использует Association:

from typing import Optional

from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship


class Base(DeclarativeBase):
    pass


class Association(Base):
    __tablename__ = "association_table"
    left_id: Mapped[int] = mapped_column(ForeignKey("left_table.id"), primary_key=True)
    right_id: Mapped[int] = mapped_column(
        ForeignKey("right_table.id"), primary_key=True
    )
    extra_data: Mapped[Optional[str]]
    child: Mapped["Child"] = relationship()


class Parent(Base):
    __tablename__ = "left_table"
    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Association"]] = relationship()


class Child(Base):
    __tablename__ = "right_table"
    id: Mapped[int] = mapped_column(primary_key=True)

Для иллюстрации двунаправленной версии мы добавим еще две конструкции relationship(), связанные с существующими с помощью relationship.back_populates:

from typing import Optional

from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship


class Base(DeclarativeBase):
    pass


class Association(Base):
    __tablename__ = "association_table"
    left_id: Mapped[int] = mapped_column(ForeignKey("left_table.id"), primary_key=True)
    right_id: Mapped[int] = mapped_column(
        ForeignKey("right_table.id"), primary_key=True
    )
    extra_data: Mapped[Optional[str]]
    child: Mapped["Child"] = relationship(back_populates="parents")
    parent: Mapped["Parent"] = relationship(back_populates="children")


class Parent(Base):
    __tablename__ = "left_table"
    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Association"]] = relationship(back_populates="parent")


class Child(Base):
    __tablename__ = "right_table"
    id: Mapped[int] = mapped_column(primary_key=True)
    parents: Mapped[List["Association"]] = relationship(back_populates="child")

Работа с шаблоном ассоциации в его прямой форме требует, чтобы дочерние объекты ассоциировались с экземпляром ассоциации до того, как они будут присоединены к родительскому; аналогично, доступ от родительского объекта к дочернему происходит через объект ассоциации:

# create parent, append a child via association
p = Parent()
a = Association(extra_data="some data")
a.child = Child()
p.children.append(a)

# iterate through child objects via association, including association
# attributes
for assoc in p.children:
    print(assoc.extra_data)
    print(assoc.child)

Чтобы улучшить шаблон ассоциативного объекта таким образом, чтобы прямой доступ к объекту Association был необязательным, SQLAlchemy предоставляет расширение Доверенность Ассоциации. Это расширение позволяет конфигурировать атрибуты, которые будут обращаться к двум «прыжкам» с одним доступом, один «прыжок» к ассоциированному объекту, а второй - к целевому атрибуту.

См.также

Доверенность Ассоциации - позволяет прямой доступ в стиле «многие ко многим» между родительским и дочерним объектами для трехклассового отображения ассоциативных объектов.

Предупреждение

Избегайте прямого смешивания шаблона ассоциативного объекта с шаблоном many-to-many, так как это создает условия, при которых данные могут читаться и записываться непоследовательно без специальных действий; association proxy обычно используется для обеспечения более лаконичного доступа. Более подробную информацию о предостережениях, вносимых этой комбинацией, можно найти в следующем разделе Объединение ассоциативного объекта с шаблонами доступа «многие ко многим.

Объединение ассоциативного объекта с шаблонами доступа «многие ко многим

Как уже упоминалось в предыдущем разделе, модель ассоциативных объектов не интегрируется автоматически с использованием модели «многие ко многим» в отношении одних и тех же таблиц/столбцов в одно и то же время. Из этого следует, что операции чтения могут возвращать противоречивые данные, а операции записи могут также пытаться стирать противоречивые изменения, вызывая либо ошибки целостности, либо неожиданные вставки или удаления.

Для примера ниже настроено двунаправленное отношение «многие-ко-многим» между Parent и Child через Parent.children и Child.parents. В то же время также настраивается объектная связь ассоциации между Parent.child_associations -> Association.child и Child.parent_associations -> Association.parent:

from typing import Optional

from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship


class Base(DeclarativeBase):
    pass


class Association(Base):
    __tablename__ = "association_table"

    left_id: Mapped[int] = mapped_column(ForeignKey("left_table.id"), primary_key=True)
    right_id: Mapped[int] = mapped_column(
        ForeignKey("right_table.id"), primary_key=True
    )
    extra_data: Mapped[Optional[str]]

    # association between Assocation -> Child
    child: Mapped["Child"] = relationship(back_populates="parent_associations")

    # association between Assocation -> Parent
    parent: Mapped["Parent"] = relationship(back_populates="child_associations")


class Parent(Base):
    __tablename__ = "left_table"

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

    # many-to-many relationship to Child, bypassing the `Association` class
    children: Mapped[List["Child"]] = relationship(
        secondary="association_table", back_populates="parents"
    )

    # association between Parent -> Association -> Child
    child_associations: Mapped[List["Association"]] = relationship(
        back_populates="parent"
    )


class Child(Base):
    __tablename__ = "right_table"

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

    # many-to-many relationship to Parent, bypassing the `Association` class
    parents: Mapped[List["Parent"]] = relationship(
        secondary="association_table", back_populates="children"
    )

    # association between Child -> Association -> Parent
    parent_associations: Mapped[List["Association"]] = relationship(
        back_populates="child"
    )

При использовании этой модели ORM для внесения изменений, изменения, внесенные в Parent.children, не будут согласованы с изменениями, внесенными в Parent.child_associations или Child.parent_associations в Python; хотя все эти отношения будут продолжать нормально функционировать сами по себе, изменения в одном из них не будут отображаться в другом, пока не истечет срок действия Session, что обычно происходит автоматически после Session.commit().

Кроме того, если вносятся противоречивые изменения, например, добавляется новый объект Association и одновременно добавляется такой же связанный объект Child в Parent.children, это приведет к ошибкам целостности при выполнении процесса промывки единицы работы, как в примере ниже:

p1 = Parent()
c1 = Child()
p1.children.append(c1)

# redundant, will cause a duplicate INSERT on Association
p1.child_associations.append(Association(child=c1))

Прямое добавление Child к Parent.children также подразумевает создание строк в таблице association без указания значения для столбца association.extra_data, который получит значение NULL.

Если вы знаете, что делаете, то вполне можно использовать такое отображение, как приведенное выше; в случае нечастого использования шаблона «объект ассоциации» могут быть веские причины для использования отношений «многие-ко-многим», которые заключаются в том, что проще загружать отношения по одному отношению «многие-ко-многим», что также может немного лучше оптимизировать использование «вторичной» таблицы в SQL-запросах по сравнению с использованием двух отдельных отношений с явным классом ассоциации. По крайней мере, хорошей идеей является применение параметра relationship.viewonly к «вторичному» отношению, чтобы избежать проблемы возникновения конфликтующих изменений, а также предотвратить запись NULL в дополнительные столбцы ассоциации, как показано ниже:

class Parent(Base):
    __tablename__ = "left_table"

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

    # many-to-many relationship to Child, bypassing the `Association` class
    children: Mapped[List["Child"]] = relationship(
        secondary="association_table", back_populates="parents", viewonly=True
    )

    # association between Parent -> Association -> Child
    child_associations: Mapped[List["Association"]] = relationship(
        back_populates="parent"
    )


class Child(Base):
    __tablename__ = "right_table"

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

    # many-to-many relationship to Parent, bypassing the `Association` class
    parents: Mapped[List["Parent"]] = relationship(
        secondary="association_table", back_populates="children", viewonly=True
    )

    # association between Child -> Association -> Parent
    parent_associations: Mapped[List["Association"]] = relationship(
        back_populates="child"
    )

Приведенное выше отображение не будет записывать изменения в Parent.children или Child.parents в базу данных, предотвращая конфликтующие записи. Однако, чтение из Parent.children или Child.parents не обязательно будет соответствовать данным, которые читаются из Parent.child_associations или Child.parent_associations, если изменения в этих коллекциях производятся в той же транзакции или Session, в которой читаются коллекции viewonly. Если использование отношений ассоциативных объектов нечастое и тщательно организовано против кода, который обращается к коллекциям «многие ко многим», чтобы избежать неактуальных чтений (в крайнем случае, прямое использование Session.expire(), чтобы заставить коллекции обновляться в пределах текущей транзакции), паттерн может быть осуществим.

Популярной альтернативой приведенной выше схеме является та, в которой прямые отношения «многие-ко-многим» Parent.children и Child.parents заменяются расширением, которое будет прозрачно проксировать класс Association, сохраняя все последовательным с точки зрения ORM. Это расширение известно как Association Proxy.

См.также

Доверенность Ассоциации - позволяет прямой доступ в стиле «многие ко многим» между родительским и дочерним объектами для трехклассового отображения ассоциативных объектов.

Поздняя оценка аргументов о взаимоотношениях

Большинство примеров в предыдущих разделах иллюстрируют отображения, в которых различные конструкции relationship() ссылаются на свои целевые классы, используя строковое имя, а не сам класс, например, при использовании Mapped генерируется прямая ссылка, которая во время выполнения существует только в виде строки:

class Parent(Base):
    # ...

    children: Mapped[List["Child"]] = relationship(back_populates="parent")


class Child(Base):
    # ...

    parent: Mapped["Parent"] = relationship(back_populates="children")

Аналогично, при использовании неаннотированных форм, таких как неаннотированные декларативные или императивные отображения, строковое имя также поддерживается непосредственно конструкцией relationship():

registry.map_imperatively(
    Parent,
    parent_table,
    properties={"children": relationship("Child", back_populates="parent")},
)

registry.map_imperatively(
    Child,
    child_table,
    properties={"parent": relationship("Parent", back_populates="children")},
)

Эти строковые имена разрешаются в классы на этапе разрешения отображения, который является внутренним процессом, происходящим, как правило, после определения всех отображений и обычно запускается при первом использовании самих отображений. Объект registry является контейнером, в котором хранятся эти имена и разрешаются в сопоставленные классы, на которые они ссылаются.

Помимо основного аргумента класса для relationship(), другие аргументы, которые зависят от столбцов, присутствующих в еще неопределенном классе, также могут быть указаны либо как функции Python, либо, что более распространено, как строки. Для большинства этих аргументов, кроме основного, строковые входы оцениваются как выражения Python с помощью встроенной функции eval() Python, поскольку они предназначены для получения полных выражений SQL.

Предупреждение

Поскольку функция Python eval() используется для интерпретации строковых аргументов с поздней оценкой, передаваемых в конфигурационную конструкцию маппера relationship(), эти аргументы не должны быть перепрофилированы таким образом, чтобы они получали ввод недоверенного пользователя; eval() не защищена от ввода недоверенного пользователя.

Полное пространство имен, доступное в рамках этой оценки, включает все классы, сопоставленные для этой декларативной базы, а также содержимое пакета sqlalchemy, включая функции выражения desc() и sqlalchemy.sql.functions.func:

class Parent(Base):
    # ...

    children: Mapped[List["Child"]] = relationship(
        order_by="desc(Child.email_address)",
        primaryjoin="Parent.id == Child.parent_id",
    )

Для случая, когда более одного модуля содержат класс с одинаковым именем, строковые имена классов также могут быть указаны как пути с квалификацией модуля в любом из этих строковых выражений:

class Parent(Base):
    # ...

    children: Mapped[List["myapp.mymodel.Child"]] = relationship(
        order_by="desc(myapp.mymodel.Child.email_address)",
        primaryjoin="myapp.mymodel.Parent.id == myapp.mymodel.Child.parent_id",
    )

В примере, подобном приведенному выше, строка, переданная в Mapped, может быть отделена от конкретного аргумента класса путем передачи строки расположения класса непосредственно в relationship.argument. Ниже показан импорт только набором для Child в сочетании со спецификатором времени выполнения для целевого класса, который будет искать правильное имя внутри registry:

import typing

if typing.TYPE_CHECKING:
    from myapp.mymodel import Child


class Parent(Base):
    # ...

    children: Mapped[List["Child"]] = relationship(
        "myapp.mymodel.Child",
        order_by="desc(myapp.mymodel.Child.email_address)",
        primaryjoin="myapp.mymodel.Parent.id == myapp.mymodel.Child.parent_id",
    )

Квалифицированный путь может быть любым частичным путем, который устраняет двусмысленность между именами. Например, чтобы устранить неоднозначность между myapp.model1.Child и myapp.model2.Child, мы можем указать model1.Child или model2.Child:

class Parent(Base):
    # ...

    children: Mapped[List["Child"]] = relationship(
        "model1.Child",
        order_by="desc(mymodel1.Child.email_address)",
        primaryjoin="Parent.id == model1.Child.parent_id",
    )

Конструкция relationship() также принимает функции или лямбды Python в качестве входных данных для этих аргументов. Функциональный подход Python может выглядеть следующим образом:

import typing

from sqlalchemy import desc

if typing.TYPE_CHECKING:
    from myapplication import Child


def _resolve_child_model():
    from myapplication import Child

    return Child


class Parent(Base):
    # ...

    children: Mapped[List["Child"]] = relationship(
        _resolve_child_model,
        order_by=lambda: desc(_resolve_child_model().email_address),
        primaryjoin=lambda: Parent.id == _resolve_child_model().parent_id,
    )

Полный список параметров, которые принимают функции/ламбды Python или строки, которые будут переданы в eval(), таков:

Предупреждение

Как было сказано ранее, приведенные выше параметры для relationship() оцениваются как выражения кода Python с помощью eval(). НЕ ПЕРЕДАВАЙТЕ В ЭТИ АРГУМЕНТЫ НЕДОВЕРЕННЫЕ ВХОДНЫЕ ДАННЫЕ.

Добавление отношений к сопоставленным классам после объявления

Следует также отметить, что аналогично тому, как это описано в Добавление дополнительных столбцов к существующему декларативному сопоставленному классу, любая конструкция MapperProperty может быть добавлена к декларативному базовому отображению в любое время (следует отметить, что аннотированные формы не поддерживаются в этом контексте). Если бы мы хотели реализовать эту relationship() после того, как класс Address был доступен, мы могли бы применить ее и после этого:

# first, module A, where Child has not been created yet,
# we create a Parent class which knows nothing about Child


class Parent(Base):
    ...


# ... later, in Module B, which is imported after module A:


class Child(Base):
    ...


from module_a import Parent

# assign the User.addresses relationship as a class variable.  The
# declarative base class will intercept this and map the relationship.
Parent.children = relationship(Child, primaryjoin=Child.parent_id == Parent.id)

Как и в случае сопоставленных колонок ORM, аннотационный тип Mapped не может участвовать в этой операции; поэтому связанный класс должен быть указан непосредственно в конструкции relationship(), либо как сам класс, либо как строковое имя класса, либо как вызываемая функция, возвращающая ссылку на целевой класс.

Примечание

Как и в случае с отображенными колонками ORM, присвоение отображенных свойств уже отображенному классу будет работать правильно только в том случае, если используется «декларативный базовый» класс, то есть определяемый пользователем подкласс DeclarativeBase или динамически создаваемый класс, возвращаемый declarative_base() или registry.generate_base(). Этот «базовый» класс включает метакласс Python, реализующий специальный метод __setattr__(), который перехватывает эти операции.

Присвоение атрибутов класса сопоставленному классу не будет работать, если класс сопоставлен с использованием декораторов типа registry.mapped() или императивных функций типа registry.map_imperatively().

Использование формы с поздней оценкой для «вторичного» аргумента многие-ко-многим

В отношениях «многие-ко-многим» используется параметр relationship.secondary, который обычно указывает на ссылку на обычно не отображаемый объект Table или другой объект Core selectable. Поддерживается поздняя оценка с помощью лямбда-вызова или строкового имени, где разрешение строк работает путем оценки данного выражения Python, которое связывает имена идентификаторов с одноименными объектами Table, которые присутствуют в той же коллекции MetaData, на которую ссылается текущий registry.

Для примера, приведенного в Многие ко многим, если мы предположили, что объект association_table Table будет определен в более поздней точке модуля, чем сам отображаемый класс, мы можем написать relationship(), используя лямбду as:

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship(
        "Child", secondary=lambda: association_table
    )

Или для иллюстрации нахождения одного и того же объекта Table по имени, в качестве аргумента используется имя Table. С точки зрения Python, это выражение Python, оцениваемое как переменная с именем «association_table», которая разрешается по именам таблиц в коллекции MetaData:

class Parent(Base):
    __tablename__ = "left_table"

    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship(secondary="association_table")

Предупреждение

При передаче в виде строки аргумент relationship.secondary интерпретируется с помощью функции Python eval(), хотя обычно это имя таблицы. НЕ ПЕРЕДАВАЙТЕ НЕДОВЕРЕННЫЙ ВВОД В ЭТУ СТРОКУ.

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