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

Что такое композиция в ORM?

1.3 Junior🔥 71 комментариев
#Другое

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

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

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

Композиция в ORM

Композиция в ORM (Object-Relational Mapping) — это механизм, позволяющий составлять сложные запросы к базе данных из нескольких простых операций, используя объектно-ориентированный интерфейс. Это отличается от итерации и агрегации, предоставляя более гибкий способ работы с данными через комбинирование операций.

Различие между композицией и другими подходами

1. Итерация (без композиции)

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from dataclasses import dataclass

@dataclass
class User:
    id: int
    name: str
    age: int

engine = create_engine('postgresql://user:password@localhost/db')
Session = sessionmaker(bind=engine)
session = Session()

# ❌ Плохо: много запросов к БД (N+1 problem)
users = session.query(User).all()  # Запрос 1
for user in users:
    if user.age > 30:  # Дополнительная логика в Python
        print(f"Adult: {user.name}")

# Проблема: для каждого пользователя может быть свой запрос

2. Композиция (правильный подход)

from sqlalchemy import and_, or_

# ✅ Хорошо: один оптимальный запрос
query = session.query(User)
query = query.filter(User.age > 30)  # Добавляем условие
query = query.order_by(User.name)    # Добавляем сортировку
query = query.limit(10)              # Добавляем ограничение

adult_users = query.all()  # Один запрос с всеми условиями

Типы композиции в ORM

1. Фильтрация (Query Composition)

from sqlalchemy import and_, or_, not_
from sqlalchemy.orm import Query

class UserRepository:
    def __init__(self, session):
        self.session = session
    
    def get_active_adults(self):
        """Композиция из простых условий"""
        query = self.session.query(User)
        
        # Добавляем условия поэтапно
        query = query.filter(User.age >= 18)
        query = query.filter(User.is_active == True)
        query = query.order_by(User.created_at.desc())
        
        return query.all()
    
    def get_users_by_criteria(self, min_age=None, max_age=None, status=None):
        """Динамическая композиция"""
        query = self.session.query(User)
        
        # Добавляем условия только если нужны
        if min_age:
            query = query.filter(User.age >= min_age)
        
        if max_age:
            query = query.filter(User.age <= max_age)
        
        if status:
            query = query.filter(User.status == status)
        
        return query.all()

repo = UserRepository(session)
users = repo.get_users_by_criteria(min_age=25, status='active')

2. Join Composition (соединения)

from sqlalchemy.orm import relationship, joinedload
from sqlalchemy import ForeignKey

class User:
    __tablename__ = 'users'
    id: int
    name: str
    posts = relationship("Post", back_populates="author")  # Отношение

class Post:
    __tablename__ = 'posts'
    id: int
    title: str
    user_id = ForeignKey('users.id')
    author = relationship("User", back_populates="posts")

# Композиция JOIN'ов
query = session.query(User)
query = query.join(Post)  # Добавляем JOIN
query = query.filter(Post.published == True)  # Фильтруем по посту
query = query.distinct()  # Убираем дубликаты

publishing_users = query.all()  # Один эффективный запрос

# Или с eager loading (избегаем N+1 problem)
query = session.query(User)
query = query.options(joinedload(User.posts))  # Загружаем посты заранее
users_with_posts = query.all()  # Два запроса, но не N+1

3. Subquery Composition (подзапросы)

from sqlalchemy import func
from sqlalchemy.orm import subquery

# Найти пользователей с выше-среднего количеством постов
post_count_subquery = session.query(
    Post.user_id,
    func.count(Post.id).label('post_count')
).group_by(Post.user_id).subquery()

avg_posts = session.query(
    func.avg(post_count_subquery.c.post_count)
).scalar()

# Композиция: подзапрос + основной запрос
productive_users = session.query(User).join(
    post_count_subquery,
    User.id == post_count_subquery.c.user_id
).filter(
    post_count_subquery.c.post_count > avg_posts
).all()

4. Query Builder Pattern

from typing import List, Optional, Dict, Any

class QueryBuilder:
    """Builder для композиции сложных запросов"""
    def __init__(self, session, model):
        self.session = session
        self.model = model
        self.query = session.query(model)
        self._filters: List = []
        self._joins: List = []
        self._orders: List = []
        self._limit_value: Optional[int] = None
    
    def filter(self, condition):
        """Добавить условие фильтра"""
        self.query = self.query.filter(condition)
        return self  # Возврат self для цепочки
    
    def join(self, model):
        """Добавить JOIN"""
        self.query = self.query.join(model)
        return self
    
    def order_by(self, column):
        """Добавить сортировку"""
        self.query = self.query.order_by(column)
        return self
    
    def limit(self, count: int):
        """Ограничить результаты"""
        self.query = self.query.limit(count)
        return self
    
    def build(self):
        """Собрать и вернуть результаты"""
        return self.query.all()

# Использование цепочки (fluent interface)
builder = QueryBuilder(session, User)
results = (builder
    .filter(User.age >= 18)
    .filter(User.status == 'active')
    .join(Post)
    .filter(Post.published == True)
    .order_by(User.created_at.desc())
    .limit(10)
    .build())

5. Relationship Composition

from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship

class Company:
    __tablename__ = 'companies'
    id: int
    name: str
    departments = relationship(
        "Department",
        back_populates="company",
        cascade="all, delete-orphan"  # Управление жизненным циклом
    )

class Department:
    __tablename__ = 'departments'
    id: int
    name: str
    company_id = ForeignKey('companies.id')
    company = relationship("Company", back_populates="departments")
    employees = relationship(
        "Employee",
        back_populates="department",
        cascade="all, delete-orphan"
    )

class Employee:
    __tablename__ = 'employees'
    id: int
    name: str
    department_id = ForeignKey('departments.id')
    department = relationship("Department", back_populates="employees")

# Композиция отношений
company = session.query(Company).filter(
    Company.name == "TechCorp"
).options(
    joinedload(Company.departments).joinedload(
        Department.employees
    )
).first()

# Доступ к вложенным отношениям
for dept in company.departments:
    for emp in dept.employees:
        print(f"{emp.name} работает в {dept.name}")

Практические примеры

Пример 1: Поиск с фильтрацией

def search_users(session, query_params: Dict[str, Any]):
    """Поиск с динамической композицией фильтров"""
    query = session.query(User)
    
    # Композиция условий
    if query_params.get('name'):
        query = query.filter(
            User.name.ilike(f"%{query_params['name']}%")
        )
    
    if query_params.get('min_age'):
        query = query.filter(User.age >= query_params['min_age'])
    
    if query_params.get('email_domain'):
        query = query.filter(
            User.email.like(f"%@{query_params['email_domain']}")
        )
    
    if query_params.get('status'):
        query = query.filter(User.status == query_params['status'])
    
    # Сортировка
    sort_by = query_params.get('sort_by', 'created_at')
    sort_order = query_params.get('sort_order', 'desc')
    
    if sort_order == 'asc':
        query = query.order_by(getattr(User, sort_by))
    else:
        query = query.order_by(getattr(User, sort_by).desc())
    
    # Пагинация
    page = query_params.get('page', 1)
    per_page = query_params.get('per_page', 20)
    query = query.offset((page - 1) * per_page).limit(per_page)
    
    return query.all()

# Использование
results = search_users(session, {
    'name': 'Alice',
    'min_age': 25,
    'status': 'active',
    'sort_by': 'created_at',
    'sort_order': 'desc',
    'page': 1,
    'per_page': 20
})

Пример 2: Агрегация с композицией

from sqlalchemy import func

# Найти топ-5 авторов по количеству опубликованных постов
top_authors = session.query(
    User.name,
    func.count(Post.id).label('post_count')
).join(Post).filter(
    Post.published == True
).group_by(
    User.id
).order_by(
    func.count(Post.id).desc()
).limit(5).all()

for name, count in top_authors:
    print(f"{name}: {count} постов")

Пример 3: Complex Query with Multiple Conditions

from datetime import datetime, timedelta
from sqlalchemy import and_, or_

# Найти активных пользователей с недавней активностью
recent_date = datetime.now() - timedelta(days=7)

active_recent = session.query(User).filter(
    and_(
        User.status == 'active',
        User.last_login >= recent_date,
        or_(
            User.email_verified == True,
            User.phone_verified == True
        )
    )
).order_by(
    User.last_login.desc()
).all()

Оптимизация с Composition

# ❌ N+1 проблема
users = session.query(User).all()
for user in users:
    posts = session.query(Post).filter(
        Post.user_id == user.id
    ).all()  # Отдельный запрос для каждого пользователя!

# ✅ Решение: композиция с eager loading
from sqlalchemy.orm import contains_eager

users_with_posts = session.query(User).outerjoin(
    Post
).options(
    contains_eager(User.posts)
).all()  # Один или два запроса, не N

Композиция в ORM — это мощный паттерн, позволяющий писать эффективные и гибкие запросы к базе данных. Правильное использование композиции избегает N+1 проблем, улучшает производительность и делает код более читаемым.