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

Краткое описание основных реляционных шаблонов.

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

from sqlalchemy import Column, ForeignKey, Integer, Table
from sqlalchemy.orm import declarative_base, relationship

Base = declarative_base()

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

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

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)
    children = relationship("Child")


class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey("parent.id"))

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

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)
    children = relationship("Child", back_populates="parent")


class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey("parent.id"))
    parent = relationship("Parent", back_populates="children")

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

Альтернативно, опция relationship.backref может быть использована на одном relationship() вместо использования relationship.back_populates:

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)
    children = relationship("Child", backref="parent")

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

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

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

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

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)
    child_id = Column(Integer, ForeignKey("child.id"))
    child = relationship("Child")


class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key=True)

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

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)
    child_id = Column(Integer, ForeignKey("child.id"))
    child = relationship("Child", back_populates="parents")


class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key=True)
    parents = relationship("Parent", back_populates="child")

Альтернативно, параметр relationship.backref может быть применен к одному relationship(), например Parent.child:

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)
    child_id = Column(Integer, ForeignKey("child.id"))
    child = relationship("Child", backref="parents")

Один к одному

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

Соглашение «один к одному» достигается путем применения значения False к параметру relationship.uselist конструкции relationship() или, в некоторых случаях, конструкции backref(), применяя его на стороне отношения «один ко многим» или «коллекция».

В приведенном ниже примере представлена двунаправленная связь, включающая отношения one-to-many (Parent.children) и many-to-one (Child.parent):

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)

    # one-to-many collection
    children = relationship("Child", back_populates="parent")


class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey("parent.id"))

    # many-to-one scalar
    parent = relationship("Parent", back_populates="children")

Выше, Parent.children - это сторона «один ко многим», относящаяся к коллекции, а Child.parent - это сторона «многие к одному», относящаяся к одному объекту. Чтобы преобразовать это в «один к одному», сторона «один ко многим» или «коллекция» преобразуется в скалярное отношение с помощью флага uselist=False, переименовав Parent.children в Parent.child для ясности:

class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)

    # previously one-to-many Parent.children is now
    # one-to-one Parent.child
    child = relationship("Child", back_populates="parent", uselist=False)


class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey("parent.id"))

    # many-to-one side remains, see tip below
    parent = relationship("Parent", 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 одновременно.

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

from sqlalchemy.orm import backref


class Parent(Base):
    __tablename__ = "parent"
    id = Column(Integer, primary_key=True)


class Child(Base):
    __tablename__ = "child"
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey("parent.id"))
    parent = relationship("Parent", backref=backref("child", uselist=False))

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

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

association_table = Table(
    "association",
    Base.metadata,
    Column("left_id", ForeignKey("left.id")),
    Column("right_id", ForeignKey("right.id")),
)


class Parent(Base):
    __tablename__ = "left"
    id = Column(Integer, primary_key=True)
    children = relationship("Child", secondary=association_table)


class Child(Base):
    __tablename__ = "right"
    id = Column(Integer, primary_key=True)

Совет

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

association_table = Table(
    "association",
    Base.metadata,
    Column("left_id", ForeignKey("left.id"), primary_key=True),
    Column("right_id", ForeignKey("right.id"), primary_key=True),
)

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

association_table = Table(
    "association",
    Base.metadata,
    Column("left_id", ForeignKey("left.id"), primary_key=True),
    Column("right_id", ForeignKey("right.id"), primary_key=True),
)


class Parent(Base):
    __tablename__ = "left"
    id = Column(Integer, primary_key=True)
    children = relationship(
        "Child", secondary=association_table, back_populates="parents"
    )


class Child(Base):
    __tablename__ = "right"
    id = Column(Integer, primary_key=True)
    parents = relationship(
        "Parent", secondary=association_table, back_populates="children"
    )

При использовании параметра relationship.backref вместо relationship.back_populates, обратная ссылка автоматически использует тот же аргумент relationship.secondary для обратного отношения:

association_table = Table(
    "association",
    Base.metadata,
    Column("left_id", ForeignKey("left.id"), primary_key=True),
    Column("right_id", ForeignKey("right.id"), primary_key=True),
)


class Parent(Base):
    __tablename__ = "left"
    id = Column(Integer, primary_key=True)
    children = relationship("Child", secondary=association_table, backref="parents")


class Child(Base):
    __tablename__ = "right"
    id = Column(Integer, primary_key=True)

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

class Parent(Base):
    __tablename__ = "left"
    id = Column(Integer, primary_key=True)
    children = relationship(
        "Child",
        secondary=lambda: association_table,
        backref="parents",
    )

При использовании декларативного расширения традиционное «строковое имя таблицы» также принимается, соответствуя имени таблицы, хранящемуся в Base.metadata.tables:

class Parent(Base):
    __tablename__ = "left"
    id = Column(Integer, primary_key=True)
    children = relationship("Child", secondary="association", backref="parents")

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

Когда аргумент relationship.secondary передается в виде строки, интерпретируется с помощью функции Python eval(). НЕ ПЕРЕДАВАЙТЕ НЕДОВЕРЕННЫЙ ВВОД В ЭТУ СТРОКУ. Подробности о декларативной оценке аргументов Оценка аргументов в пользу отношений см. в 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(), вместо этого можно использовать каскадные правила для автоматического удаления сущностей в ответ на удаление связанной сущности - см. информацию об этой возможности в Каскады.

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

Шаблон объекта ассоциации является вариантом схемы «многие ко многим»: он используется, когда ваша таблица ассоциации содержит дополнительные столбцы, помимо тех, которые являются внешними ключами левой и правой таблиц. Вместо того чтобы использовать аргумент relationship.secondary, вы сопоставляете новый класс непосредственно с таблицей ассоциаций. Левая сторона отношения ссылается на объект ассоциации по принципу «один ко многим», а класс ассоциации ссылается на правую сторону по принципу «многие к одному». Ниже показана таблица ассоциаций, сопоставленная с классом Association, которая включает столбец extra_data, представляющий собой строковое значение, которое хранится вместе с каждой ассоциацией между Parent и Child:

class Association(Base):
    __tablename__ = "association"
    left_id = Column(ForeignKey("left.id"), primary_key=True)
    right_id = Column(ForeignKey("right.id"), primary_key=True)
    extra_data = Column(String(50))
    child = relationship("Child")


class Parent(Base):
    __tablename__ = "left"
    id = Column(Integer, primary_key=True)
    children = relationship("Association")


class Child(Base):
    __tablename__ = "right"
    id = Column(Integer, primary_key=True)

Как обычно, двунаправленная версия использует relationship.back_populates или relationship.backref:

class Association(Base):
    __tablename__ = "association"
    left_id = Column(ForeignKey("left.id"), primary_key=True)
    right_id = Column(ForeignKey("right.id"), primary_key=True)
    extra_data = Column(String(50))
    child = relationship("Child", back_populates="parents")
    parent = relationship("Parent", back_populates="children")


class Parent(Base):
    __tablename__ = "left"
    id = Column(Integer, primary_key=True)
    children = relationship("Association", back_populates="parent")


class Child(Base):
    __tablename__ = "right"
    id = Column(Integer, primary_key=True)
    parents = relationship("Association", 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 предоставляет расширение Доверенность ассоциации. Это расширение позволяет конфигурировать атрибуты, которые будут обращаться к двум «прыжкам» с одним доступом, один «прыжок» к ассоциированному объекту, а второй - к целевому атрибуту.

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

Шаблон объекта ассоциации не координирует изменения с отдельным отношением, которое отображает таблицу ассоциации как «вторичную «.

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

class Association(Base):
    __tablename__ = "association"

    left_id = Column(ForeignKey("left.id"), primary_key=True)
    right_id = Column(ForeignKey("right.id"), primary_key=True)
    extra_data = Column(String(50))

    child = relationship("Child", backref="parent_associations")
    parent = relationship("Parent", backref="child_associations")


class Parent(Base):
    __tablename__ = "left"
    id = Column(Integer, primary_key=True)

    children = relationship("Child", secondary="association")


class Child(Base):
    __tablename__ = "right"
    id = Column(Integer, primary_key=True)

Кроме того, так же как изменения в одном отношении не отражаются в других автоматически, запись одних и тех же данных в оба отношения приведет к конфликтующих операторов INSERT или DELETE, как, например, ниже, где мы устанавливаем одно и то же отношение между объектами Parent и Child дважды:

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

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

Если вы знаете, что делаете, то вполне можно использовать такое отображение, как описано выше, хотя, возможно, было бы неплохо применить параметр viewonly=True к «вторичному» отношению, чтобы избежать проблемы регистрации избыточных изменений. Однако, чтобы получить надежный шаблон, который позволяет использовать простые двухобъектные отношения Parent->Child и при этом использовать шаблон объекта ассоциации, используйте расширение прокси ассоциации, как описано в Доверенность ассоциации.

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

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

class Parent(Base):
    # ...

    children = relationship("Child", back_populates="parent")


class Child(Base):
    # ...

    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 = relationship(
        "Child",
        order_by="desc(Child.email_address)",
        primaryjoin="Parent.id == Child.parent_id",
    )

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

class Parent(Base):
    # ...

    children = 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 = relationship(
        "model1.Child",
        order_by="desc(mymodel1.Child.email_address)",
        primaryjoin="Parent.id == model1.Child.parent_id",
    )

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

Функциональный подход Python может выглядеть следующим образом:

from sqlalchemy import desc


def _resolve_child_model():
    from myapplication import Child

    return Child


class Parent(Base):
    # ...

    children = 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.order_by

  • relationship.primaryjoin

  • relationship.secondaryjoin

  • relationship.secondary

  • relationship.remote_side

  • relationship.foreign_keys

  • relationship._user_defined_foreign_keys

Изменено в версии 1.3.16: До версии SQLAlchemy 1.3.16, основная строка от relationship.argument до relationship() также оценивалась через eval() Начиная с версии 1.3.16 строковое имя разрешается из резольвера классов напрямую без поддержки пользовательских выражений Python.

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

Как было сказано ранее, приведенные выше параметры для 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)

Примечание

присвоение отображаемых свойств декларативно отображаемому классу будет работать правильно только в том случае, если используется «декларативный базовый» класс, который также предусматривает управляемый метаклассом метод __setattr__(), который будет перехватывать эти операции. Это не будет работать, если используется декларативный декоратор, предоставляемый registry.mapped(), и не будет работать для императивно отображаемого класса, отображаемого registry.map_imperatively().

Поздняя оценка для отношений «многие-ко-многим

Отношения «многие-ко-многим» включают ссылку на дополнительный, обычно не отображаемый объект Table, который обычно присутствует в коллекции MetaData, на которую ссылается registry. Система поздней оценки также поддерживает возможность указания этого атрибута в виде строкового аргумента, который будет разрешен из этой коллекции MetaData. Ниже мы указываем ассоциативную таблицу keyword_author, совместно использующую коллекцию MetaData, связанную с нашей декларативной базой и ее registry. Затем мы можем ссылаться на эту Table по имени в параметре relationship.secondary:

keyword_author = Table(
    "keyword_author",
    Base.metadata,
    Column("author_id", Integer, ForeignKey("authors.id")),
    Column("keyword_id", Integer, ForeignKey("keywords.id")),
)


class Author(Base):
    __tablename__ = "authors"
    id = Column(Integer, primary_key=True)
    keywords = relationship("Keyword", secondary="keyword_author")

Дополнительную информацию об отношениях «многие-ко-многим» можно найти в разделе Многие ко многим.

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