← Назад к вопросам

Для чего нужен relationship в SQLAlchemy?

2.0 Middle🔥 191 комментариев
#Базы данных (SQL)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Relationship в SQLAlchemy: назначение и применение

Relationship — это один из ключевых механизмов SQLAlchemy для работы с связями между таблицами. Расскажу о его целях, применении и когда это действительно нужно.

Основная цель: упрощение работы с объектами

Без relationship приходилось бы вручную загружать связанные объекты через отдельные запросы. С relationship это происходит автоматически.

from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    email = Column(String)
    posts = relationship('Post', back_populates='author')

class Post(Base):
    __tablename__ = 'posts'
    
    id = Column(Integer, primary_key=True)
    title = Column(String)
    user_id = Column(Integer, ForeignKey('users.id'))
    author = relationship('User', back_populates='posts')

# Теперь можно легко получить все посты пользователя
user = session.query(User).first()
for post in user.posts:  # Работает как обычный список
    print(post.title)

Нагрузка (Lazy Loading)

Relationship поддерживает различные стратегии загрузки связанных объектов:

from sqlalchemy.orm import selectinload, joinedload, contains_eager

# 1. LAZY (по умолчанию) — загружает данные при первом обращении
# Минус: N+1 проблема
user = session.query(User).first()
for post in user.posts:  # Здесь будет отдельный SQL запрос на каждого пользователя!
    print(post.title)

# 2. SELECTIN — загружает связанные объекты отдельным SELECT запросом
users = session.query(User).options(selectinload(User.posts)).all()
# SQL: SELECT * FROM users; SELECT * FROM posts WHERE user_id IN (...)

# 3. JOINEDLOAD — загружает через LEFT OUTER JOIN
users = session.query(User).options(joinedload(User.posts)).all()
# SQL: SELECT users.*, posts.* FROM users LEFT JOIN posts ON ...

# 4. RAISE — выбросит ошибку, если данные не загружены
from sqlalchemy.orm import selectinload, raiseload
users = session.query(User).options(raiseload(User.posts)).all()

Каскадное удаление

Одна из критических задач relationship — определение поведения при удалении связанных объектов.

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    # cascade='all, delete' — удаляет все посты при удалении пользователя
    posts = relationship('Post', cascade='all, delete', back_populates='author')

class Post(Base):
    __tablename__ = 'posts'
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'))
    author = relationship('User', back_populates='posts')

# Удаление пользователя автоматически удалит все его посты
user = session.query(User).first()
session.delete(user)
session.commit()  # На БД будут выполнены DELETE FROM posts WHERE user_id = ..., затем DELETE FROM users

Типы связей

One-to-Many (Один ко многим) — самая распространённая

class Author(Base):
    __tablename__ = 'authors'
    id = Column(Integer, primary_key=True)
    books = relationship('Book', back_populates='author')

class Book(Base):
    __tablename__ = 'books'
    id = Column(Integer, primary_key=True)
    author_id = Column(Integer, ForeignKey('authors.id'))
    author = relationship('Author', back_populates='books')

Many-to-Many (Много ко многим) — через junction table

association_table = Table(
    'association',
    Base.metadata,
    Column('user_id', Integer, ForeignKey('users.id')),
    Column('group_id', Integer, ForeignKey('groups.id'))
)

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    groups = relationship('Group', secondary=association_table, back_populates='users')

class Group(Base):
    __tablename__ = 'groups'
    id = Column(Integer, primary_key=True)
    users = relationship('User', secondary=association_table, back_populates='groups')

Важный момент: Relationship vs Foreign Key

Where я работаю, используем Goose миграции как source of truth для схемы. Relationship в SQLAlchemy — это сугубо для удобства кода, а не для управления БД.

-- migrations/0001_create_users.sql (Goose миграция)
CREATE TABLE users (
    id UUID PRIMARY KEY,
    email VARCHAR NOT NULL
);

CREATE TABLE posts (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    title VARCHAR NOT NULL
);
# models.py — для ORM и типизации
class User(Base):
    __tablename__ = 'users'
    id: Mapped[UUID] = mapped_column(primary_key=True)
    email: Mapped[str]
    posts: Mapped[List['Post']] = relationship(
        'Post',
        cascade='all, delete',  # Описывает логику в коде для админки/каскадов
        back_populates='author'
    )

class Post(Base):
    __tablename__ = 'posts'
    id: Mapped[UUID] = mapped_column(primary_key=True)
    user_id: Mapped[UUID] = mapped_column(ForeignKey('users.id'))
    title: Mapped[str]
    author: Mapped[User] = relationship('User', back_populates='posts')

Когда НЕ нужен relationship

В сервис-слое (application layer) часто relationship не требуется. Используем явные query.

# ❌ НЕПРАВИЛЬНО в service — зависимость от ORM
class UserService:
    def get_user_with_posts(self, user_id: int):
        user = session.query(User).options(selectinload(User.posts)).get(user_id)
        return user  # Возвращаем ORM объект

# ✅ ПРАВИЛЬНО — явная загрузка, return domain object
class UserService:
    async def get_user_with_posts(self, user_id: UUID) -> UserDTO:
        user = await session.execute(
            select(UserModel)
            .where(UserModel.id == user_id)
            .options(selectinload(UserModel.posts))
        )
        return UserDTO.from_orm(user.scalar_one_or_none())

Итоги

Relationship в SQLAlchemy нужен для:

  • Удобства кода — доступ к связанным объектам как к атрибутам
  • Стратегий загрузки — selectinload, joinedload для избежания N+1
  • Каскадного удаления — автоматическая очистка связанных данных
  • Типизации — IDE помогает с автодополнением

НО: relationship — это исключительно инструмент ORM. Миграции и схему БД определяем в Goose SQL, а relationship используем для удобства работы с объектами в Python коде.

Для чего нужен relationship в SQLAlchemy? | PrepBro