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

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

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import relationship

class User(Base):
    __tablename__ = "user_account"

    # ... mapped_column() mappings

    addresses: Mapped[List["Address"]] = relationship(back_populates="user")

class Address(Base):
    __tablename__ = "address"

    # ... mapped_column() mappings

    user: Mapped["User"] = relationship(back_populates="addresses")

Наш класс User теперь имеет атрибут addresses, в свою очередь Address теперь имеет атрибут user. Конструкция relationship() в связке с Mapped аннотацией может быть использована для работы со связью между User и Address. Поскольку таблица адресов имеет ограничение внешнего ключа (ForeignKeyConstraint), которое ссылается на таблицу user_account, relationship() может автоматичсески определить что это связь типа один-ко-многим к User.adresses из Adress. Или иначе говоря, несколько адресов могут быть связаны с одним пользователем.

Любые связи один-ко-многим могут быть слегкостью инвертированы до многие-к-одному, это настраивается с помощью параметра back_populates в relationship().

Сохраняющиеся и загружающиеся связи

Начнем разбираться с тем как работает relationship(). Если мы создадим новый объект User, то заметим, что атрибуту addresses теперь имеет тип list:

>>> u1 = User(name="pkrabs", fullname="Pearl Krabs")
>>> u1.addresses
[]

Этот объект на самом деле не является списком в чистом виде, это его версия от Алхимии, которая хотя и полностью повторяет его поведение, но всё же также отслеживает изменения, которые с ним происходят. Этот список появится автоматически при любой попытке обратиться к этому атрибуту. Поскольку u1 до сих пор transient и список addressess не был изменен, мы еще не имеем ассоциации с записью в БД.

Давайте теперь добавим в наш список объект Address:

>>> a1 = Address(email_address="[email protected]")
>>> u1.addresses.append(a1)

Теперь, запринтим этот список:

>>> u1.addresses
[Address(id=None, email_address='[email protected]')]

Так как мы еще не добавили объект User в сессию, то и Address тоже является лишь потенциальной строкой в БД. Но при этом мы уже можем увидеть, что объект User находится в Address:

>>> a1.user
User(id=None, name='pkrabs', fullname='Pearl Krabs')

Это происходит поскольку мы определили параметр back_populates для связи между моделями. Обратите внимание, что еще один способ связать два объекта, это не добавлять его в список у объекта User, а, наоборот, указать объект User при создании Address:

>>> a2 = Address(email_address="[email protected]", user=u1)
>>> u1.addresses
[Address(id=None, email_address='[email protected]'), Address(id=None, email_address='[email protected]')]

Ну и конечно мы можем сделать тоже самое следующим образом:

>>> a2.user = u1