Для чего нужен relationship в SQLAlchemy?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
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 коде.