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

Как в SQLAlchemy используется модуль typing?

1.7 Middle🔥 151 комментариев
#Python Core#Архитектура и паттерны#Базы данных (SQL)

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

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

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

Использование typing в SQLAlchemy

Модуль typing в SQLAlchemy решает две важные задачи: аннотация типов для IDE и валидация данных. Начиная с SQLAlchemy 2.0, работа с типами стала полноценной и удобной.

1. Базовые аннотации

Декоратор @mapped_column

В SQLAlchemy 2.0+ используется декоратор @mapped_column с явной типизацией:

from typing import Optional
from sqlalchemy import Integer, String
from sqlalchemy.orm import declarative_base, Mapped, mapped_column

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    
    # Обязательное поле
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(100))
    email: Mapped[str]
    
    # Опциональное поле (nullable)
    phone: Mapped[Optional[str]] = mapped_column(String(20), nullable=True)
    age: Mapped[Optional[int]] = mapped_column(Integer, default=None)

Преимущества явной типизации:

  • IDE получает полную информацию о типах (автодополнение)
  • Статический анализатор (mypy, pyright) может проверить корректность кода
  • Самодокументируемый код
  • Меньше ошибок в runtime

2. Отношения между таблицами (Relationships)

Для связей используется Mapped с квадратными скобками:

from typing import List
from sqlalchemy.orm import Relationship

class Author(Base):
    __tablename__ = 'authors'
    
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    
    # One-to-Many: один автор — много книг
    books: Mapped[List['Book']] = relationship('Book', back_populates='author')

class Book(Base):
    __tablename__ = 'books'
    
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str]
    author_id: Mapped[int] = mapped_column(ForeignKey('authors.id'))
    
    # Many-to-One: много книг — один автор
    author: Mapped['Author'] = relationship('Author', back_populates='books')

Ключевые моменты:

  • Mapped[List['Book']] — коллекция, не загружается автоматически
  • Кавычки 'Book' используются для forward references
  • back_populates синхронизирует две стороны отношения

3. Optional vs Required поля

Required поле (NOT NULL)

# Правильно: поле обязательно
name: Mapped[str] = mapped_column(String(100))

# При создании объекта name ТРЕБУЕТСЯ
user = User(name='Alice', email='alice@example.com')

Optional поле (NULLABLE)

# Правильно: поле опциональное
phone: Mapped[Optional[str]] = mapped_column(String(20), nullable=True)

# При создании объекта phone может быть None
user = User(name='Alice', email='alice@example.com', phone=None)

# Статический анализатор поймёт, что phone может быть None
if user.phone is not None:
    print(user.phone.upper())  # OK

4. Генерики для коллекций

from typing import Set

class Post(Base):
    __tablename__ = 'posts'
    
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str]
    
    # Many-to-Many
    tags: Mapped[Set['Tag']] = relationship(
        'Tag',
        secondary='post_tags',  # association table
    )

class Tag(Base):
    __tablename__ = 'tags'
    
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(unique=True)

5. Собственные типы данных

У SQLAlchemy есть типы для специфических данных:

from sqlalchemy import JSON, UUID, DateTime, Boolean, Numeric
from datetime import datetime, timezone
import uuid

class Order(Base):
    __tablename__ = 'orders'
    
    id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True)
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        default=lambda: datetime.now(timezone.utc)
    )
    is_paid: Mapped[bool] = mapped_column(Boolean, default=False)
    metadata: Mapped[dict] = mapped_column(JSON)  # Store JSON
    total_price: Mapped[Decimal] = mapped_column(Numeric(10, 2))

6. Валидация при создании объекта

TYPing помогает IDE ловить ошибки на этапе разработки:

from sqlalchemy.orm import Session

def create_user(session: Session, name: str, email: str) -> User:
    user = User(name=name, email=email)  # IDE проверит типы
    session.add(user)
    session.commit()
    return user

# IDE предупредит о неправильном типе
create_user(session, name=123, email='test@example.com')  # ❌ Ошибка
create_user(session, name='John', email='test@example.com')  # ✅ OK

7. Практический пример с полной типизацией

from typing import Optional, List
from datetime import datetime, timezone
from uuid import uuid4

class User(Base):
    __tablename__ = 'users'
    
    id: Mapped[str] = mapped_column(primary_key=True, default=lambda: str(uuid4()))
    username: Mapped[str] = mapped_column(String(50), unique=True)
    email: Mapped[str] = mapped_column(String(100), unique=True)
    password_hash: Mapped[str]
    is_active: Mapped[bool] = mapped_column(default=True)
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        default=lambda: datetime.now(timezone.utc)
    )
    
    # Relationships
    posts: Mapped[List['Post']] = relationship('Post', back_populates='author')
    profile: Mapped[Optional['Profile']] = relationship('Profile', uselist=False)

class Post(Base):
    __tablename__ = 'posts'
    
    id: Mapped[str] = mapped_column(primary_key=True, default=lambda: str(uuid4()))
    title: Mapped[str] = mapped_column(String(200))
    content: Mapped[str] = mapped_column(String(5000))
    author_id: Mapped[str] = mapped_column(ForeignKey('users.id'))
    
    author: Mapped[User] = relationship('User', back_populates='posts')

8. Проверка типов с mypy

# Проверить весь проект на ошибки типов
mypy src/

# mypy поймёт такие ошибки:
user = User(name='Alice', email=123)  # ❌ Expected str, got int
phrase = user.phone.upper()  # ❌ Optional[str] has no attribute 'upper'

Заключение

Типизация в SQLAlchemy 2.0+ — это не просто украшение кода, а инструмент для ловли ошибок на этапе разработки. Используй:

  • Mapped[T] для полей с явной типизацией
  • Mapped[Optional[T]] для nullable полей
  • Mapped[List['RelatedModel']] для relationships
  • IDE и mypy будут ловить ошибки до production