Комментарии (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 проблем, улучшает производительность и делает код более читаемым.