Основные модели взаимоотношений¶
Краткое описание основных реляционных шаблонов.
Импорт, используемый для каждого из следующих разделов, выглядит следующим образом:
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")
Дополнительную информацию об отношениях «многие-ко-многим» можно найти в разделе Многие ко многим.