ORMs

Object-Relational Mapping

Работа с БД через объекты

Что такое ORM?

ORM — технология программирования, которая связывает базы данных с объектно-ориентированными языками программирования, создавая "виртуальную объектную базу данных".

🔹 Проблема: SQL vs Objects

# ❌ Работа с SQL напрямую
import psycopg2

conn = psycopg2.connect("dbname=test user=postgres")
cur = conn.cursor()

# Создание
cur.execute("INSERT INTO users (name, email) VALUES (%s, %s)", 
            ("Alice", "alice@example.com"))
conn.commit()

# Чтение
cur.execute("SELECT * FROM users WHERE id = %s", (1,))
row = cur.fetchone()
user = {"id": row[0], "name": row[1], "email": row[2]}

# Много boilerplate кода
# Нет type safety
# Сложно поддерживать

✅ Решение: ORM

# ✅ Работа с объектами
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# Создание
user = User(name="Alice", email="alice@example.com")
session.add(user)
session.commit()

# Чтение
user = session.query(User).filter(User.id == 1).first()
print(user.name)  # "Alice"

# Type safe
# Меньше кода
# Легче поддерживать

Типы баз данных

🔹 Реляционные БД (SQL)

  • PostgreSQL — продвинутая, open source
  • MySQL — популярная, простая
  • SQLite — встроенная, для небольших проектов
  • SQL Server — Microsoft
  • Oracle — enterprise
-- Структура: таблицы, связи, ACID
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(100)
);

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    total DECIMAL(10, 2)
);

Паттерны ORM: Active Record vs Data Mapper

  • Active Record: модель = запись таблицы, в классе и данные, и persistence-методы (напр., Django ORM, Rails).
  • Data Mapper: модели не знают о БД, маппинг и persistence — отдельно (напр., SQLAlchemy Core/ORM).
  • Active Record: быстрее старт, меньше кода; сложнее поддерживать сложный домен (богатые модели, инварианты).
  • Data Mapper: чище домен (DDD), легко тестировать; чуть более «многословно» и выше порог входа.

Как выбирать?

  • Приложение CRUD, быстрый MVP, стандартный веб → Active Record (Django ORM).
  • Сложная предметная область, DDD, микроархитектура → Data Mapper (SQLAlchemy), чёткие границы.
  • Много сложных запросов → допускайте «escape hatch»: сырой SQL там, где оправдан.

🔹 Документные БД (NoSQL)

  • MongoDB — JSON документы
  • CouchDB — документы с версионированием
  • Firestore — Google Cloud
# Структура: коллекции, документы
{
  "_id": "123",
  "name": "Alice",
  "email": "alice@example.com",
  "orders": [
    {"id": 1, "total": 100.0},
    {"id": 2, "total": 200.0}
  ]
}

🔹 Векторные БД (Vector DB)

  • Pinecone — managed векторная БД
  • Weaviate — open source
  • Qdrant — Rust-based
  • Chroma — для embeddings
  • Milvus — для ML
# Хранение векторов для similarity search
# Используется в ML для:
# - Semantic search
# - Recommendations
# - RAG (Retrieval Augmented Generation)

vector = [0.1, 0.2, 0.3, ..., 0.9]  # 768 dimensions
db.insert("document_123", vector, metadata={"text": "..."})

SQLAlchemy (Python)

Самая популярная ORM для Python

🔹 Модели

from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100), nullable=False)
    email = Column(String(100), unique=True, nullable=False)
    
    # Relationships
    orders = relationship("Order", back_populates="user")

class Order(Base):
    __tablename__ = 'orders'
    
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
    total = Column(Numeric(10, 2), nullable=False)
    
    # Relationships
    user = relationship("User", back_populates="orders")

🔹 Сессии и запросы

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# Создание engine
engine = create_engine('postgresql://user:pass@localhost/db')
Session = sessionmaker(bind=engine)
session = Session()

# Создание
user = User(name="Alice", email="alice@example.com")
session.add(user)
session.commit()

# Чтение
user = session.query(User).filter(User.id == 1).first()
users = session.query(User).filter(User.name.like('%Alice%')).all()

# Обновление
user.name = "Bob"
session.commit()

# Удаление
session.delete(user)
session.commit()

🔹 Relationships

# One-to-Many
user = session.query(User).filter(User.id == 1).first()
orders = user.orders  # Автоматически загружает связанные заказы

# Many-to-One
order = session.query(Order).filter(Order.id == 1).first()
user = order.user  # Автоматически загружает пользователя

# Joins
users_with_orders = session.query(User)\
    .join(Order)\
    .filter(Order.total > 100)\
    .all()

# Eager loading
from sqlalchemy.orm import joinedload
users = session.query(User)\
    .options(joinedload(User.orders))\
    .all()  # Загружает пользователей и их заказы одним запросом

🔹 Миграции (Alembic)

# Создание миграции
alembic revision --autogenerate -m "Add users table"

# Применение миграции
alembic upgrade head

# Откат миграции
alembic downgrade -1

# Файл миграции
def upgrade():
    op.create_table(
        'users',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('name', sa.String(100), nullable=False),
        sa.Column('email', sa.String(100), nullable=False),
        sa.PrimaryKeyConstraint('id')
    )

def downgrade():
    op.drop_table('users')

Django ORM

ORM для Django фреймворка

🔹 Модели

from django.db import models

class User(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField(unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.name

class Order(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='orders')
    total = models.DecimalField(max_digits=10, decimal_places=2)
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        ordering = ['-created_at']

🔹 Запросы

# Создание
user = User.objects.create(name="Alice", email="alice@example.com")

# Чтение
user = User.objects.get(id=1)
users = User.objects.filter(name__contains="Alice")
users = User.objects.all()

# Обновление
user.name = "Bob"
user.save()

# Удаление
user.delete()

# Aggregations
from django.db.models import Count, Sum
users_with_orders = User.objects.annotate(
    order_count=Count('orders'),
    total_spent=Sum('orders__total')
)

MongoDB ODM

Object Document Mapping для MongoDB

🔹 PyMongo (низкоуровневый)

from pymongo import MongoClient

client = MongoClient('mongodb://localhost:27017/')
db = client['mydatabase']
users = db['users']

# Создание
users.insert_one({
    "name": "Alice",
    "email": "alice@example.com",
    "orders": [
        {"id": 1, "total": 100.0},
        {"id": 2, "total": 200.0}
    ]
})

# Чтение
user = users.find_one({"email": "alice@example.com"})
users_list = users.find({"name": {"$regex": "Alice"}})

# Обновление
users.update_one(
    {"email": "alice@example.com"},
    {"$set": {"name": "Bob"}}
)

# Удаление
users.delete_one({"email": "alice@example.com"})

🔹 MongoEngine (ODM)

from mongoengine import Document, StringField, EmailField, ListField, EmbeddedDocumentField

class Order(EmbeddedDocument):
    id = IntField()
    total = FloatField()

class User(Document):
    name = StringField(required=True, max_length=100)
    email = EmailField(required=True, unique=True)
    orders = ListField(EmbeddedDocumentField(Order))
    
    meta = {
        'collection': 'users',
        'indexes': ['email']
    }

# Создание
user = User(name="Alice", email="alice@example.com")
user.orders = [Order(id=1, total=100.0)]
user.save()

# Чтение
user = User.objects(email="alice@example.com").first()
users = User.objects(name__contains="Alice")

# Обновление
user.name = "Bob"
user.save()

# Удаление
user.delete()

Векторные БД для ML

🔹 Pinecone

import pinecone
from sentence_transformers import SentenceTransformer

# Инициализация
pinecone.init(api_key="your-api-key", environment="us-west1-gcp")
index = pinecone.Index("my-index")

# Модель для embeddings
model = SentenceTransformer('all-MiniLM-L6-v2')

# Создание векторов
texts = ["This is a document", "Another document"]
vectors = model.encode(texts)

# Вставка
index.upsert([
    ("doc1", vectors[0].tolist(), {"text": texts[0]}),
    ("doc2", vectors[1].tolist(), {"text": texts[1]})
])

# Поиск
query_vector = model.encode(["search query"]).tolist()
results = index.query(query_vector, top_k=5, include_metadata=True)

🔹 Chroma

import chromadb
from sentence_transformers import SentenceTransformer

# Инициализация
client = chromadb.Client()
collection = client.create_collection("documents")

# Модель
model = SentenceTransformer('all-MiniLM-L6-v2')

# Добавление документов
documents = ["This is a document", "Another document"]
embeddings = model.encode(documents).tolist()

collection.add(
    ids=["doc1", "doc2"],
    embeddings=embeddings,
    documents=documents
)

# Поиск
query_embedding = model.encode(["search query"]).tolist()
results = collection.query(
    query_embeddings=query_embedding,
    n_results=5
)

Преимущества ORM

  • Продуктивность — меньше кода, быстрее разработка
  • Безопасность — защита от SQL injection
  • Портативность — легко сменить БД
  • Type safety — проверка типов
  • Миграции — управление схемой БД
  • Relationships — удобная работа со связями

Недостатки ORM

  • Производительность — может быть медленнее сырого SQL
  • Сложные запросы — иногда проще написать SQL
  • Learning curve — нужно изучать ORM
  • N+1 проблема — множественные запросы
  • Ограничения — не все возможности БД доступны

💡 N+1 проблема

# ❌ N+1 запросов
users = session.query(User).all()  # 1 запрос
for user in users:
    print(user.orders)  # N запросов (по одному на каждого пользователя)

# ✅ Решение: Eager loading
from sqlalchemy.orm import joinedload
users = session.query(User)\
    .options(joinedload(User.orders))\
    .all()  # 1 запрос с JOIN

🎯 Когда использовать ORM?

Сценарий Рекомендация
CRUD операции ✅ ORM отлично подходит
Сложные запросы ⚠️ Может быть проще написать SQL
Высокая производительность ⚠️ Сырой SQL может быть быстрее
Быстрая разработка ✅ ORM ускоряет разработку
Миграции схемы ✅ ORM упрощает миграции
"ORM — это инструмент. Используй его там, где он помогает, и не используй там, где мешает.
Для простых операций — ORM. Для сложных запросов — SQL."

📚 ORM для Python

  • SQLAlchemy — самая популярная, гибкая
  • Django ORM — для Django приложений
  • Peewee — легковесная, простая
  • Tortoise ORM — асинхронная
  • MongoEngine — для MongoDB
  • Prisma — современная, type-safe (для TypeScript, но есть Python клиент)