Модели данных

В этом разделе описаны модели данных используемые в Boilerplate. Вы узнаете о структуре моделей SQL Alchemy, Pydantic схемах валидации и конвертации данных между ними.

Обзор моделей данных

В Boilerplate используется два основных типа моделей данных:

  • SQLAlchemy модели - определяют структуру таблиц в базе данных и используются для ORM операций.
  • Pydantic схемы - используются для валидации данных на входе и выходе API, сериализации/десериализации и документирования API.

SQLAlchemy модели

SQLAlchemy модели определены в директории app/models. Все модели наследуются от базового класса Base.

Базовый класс

python
# app/db/base_class.py
from typing import Any

from sqlalchemy.ext.declarative import as_declarative, declared_attr

@as_declarative()
class Base:
    """
    Базовый класс для всех SQLAlchemy моделей.
    Автоматически генерирует имя таблицы из имени класса.
    """
    id: Any
    __name__: str
    
    @declared_attr
    def __tablename__(cls) -> str:
        return cls.__name__.lower()

Пример модели пользователя

python
# app/models/user.py
from sqlalchemy import Boolean, Column, Integer, String
from sqlalchemy.orm import relationship

from app.db.base_class import Base

class User(Base):
    """
    Модель пользователя в базе данных.
    """
    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True, index=True, nullable=False)
    hashed_password = Column(String, nullable=False)
    full_name = Column(String, index=True)
    is_active = Column(Boolean(), default=True)
    is_superuser = Column(Boolean(), default=False)
    
    # Отношения с другими таблицами
    items = relationship("Item", back_populates="owner")

Пример модели с отношениями

python
# app/models/item.py
from sqlalchemy import Column, ForeignKey, Integer, String, Text
from sqlalchemy.orm import relationship

from app.db.base_class import Base

class Item(Base):
    """
    Модель элемента в базе данных.
    """
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True)
    description = Column(Text)
    owner_id = Column(Integer, ForeignKey("user.id"))
    
    # Отношение к пользователю
    owner = relationship("User", back_populates="items")

Pydantic схемы

Pydantic схемы определены в директории app/schemas и используются для валидации запросов/ответов API.

Схемы пользователя

python
# app/schemas/user.py
from typing import List, Optional

from pydantic import BaseModel, EmailStr, Field

# Общие атрибуты
class UserBase(BaseModel):
    """
    Базовая схема пользователя с общими атрибутами.
    """
    email: Optional[EmailStr] = None
    is_active: Optional[bool] = True
    is_superuser: bool = False
    full_name: Optional[str] = None

# Схема для создания пользователя
class UserCreate(UserBase):
    """
    Схема для создания нового пользователя.
    Требует email и пароль.
    """
    email: EmailStr
    password: str

# Схема для обновления пользователя
class UserUpdate(UserBase):
    """
    Схема для обновления пользователя.
    Все поля опциональны.
    """
    password: Optional[str] = None

# Схема для ответа API
class User(UserBase):
    """
    Схема для возврата данных пользователя.
    Включает id и настраивает модель конфигурации ORM.
    """
    id: int
    
    class Config:
        from_attributes = True  # Позволяет преобразовывать ORM модель в Pydantic схему

# Расширенная схема с дополнительными данными
class UserWithItems(User):
    """
    Расширенная схема пользователя, включающая связанные элементы.
    """
    items: List["Item"] = []
    
    class Config:
        from_attributes = True

Схемы элемента

python
# app/schemas/item.py
from typing import Optional

from pydantic import BaseModel

class ItemBase(BaseModel):
    """
    Базовая схема элемента с общими атрибутами.
    """
    title: Optional[str] = None
    description: Optional[str] = None

class ItemCreate(ItemBase):
    """
    Схема для создания элемента.
    Требует заголовок.
    """
    title: str

class ItemUpdate(ItemBase):
    """
    Схема для обновления элемента.
    Все поля опциональны.
    """
    pass

class Item(ItemBase):
    """
    Схема для возврата данных элемента.
    Включает id и информацию о владельце.
    """
    id: int
    owner_id: int
    
    class Config:
        from_attributes = True

Организация моделей и схем

В Boilerplate используется четкая организация моделей и схем:

Структура директорий

text
app/
├── models/                # SQLAlchemy модели
│   ├── __init__.py       # Импорты моделей для удобства
│   ├── user.py           # Модель пользователя
│   ├── item.py           # Модель элемента
│   └── ...               # Другие модели
├── schemas/              # Pydantic схемы
│   ├── __init__.py       # Импорты схем для удобства
│   ├── user.py           # Схемы пользователя
│   ├── item.py           # Схемы элемента
│   ├── token.py          # Схемы JWT токенов
│   └── ...               # Другие схемы
└── db/                   # Код для работы с базой данных
    ├── base_class.py     # Базовый класс для моделей
    ├── base.py           # Импорты всех моделей для Alembic
    └── session.py        # Конфигурация сессии SQLAlchemy

Файл base.py для миграций

Для корректной работы Alembic все модели должны быть импортированы вapp/db/base.py:

python
# app/db/base.py
# Импорт базового класса для всех моделей
from app.db.base_class import Base

# Импорт всех моделей
from app.models.user import User
from app.models.item import Item
# ... импорт других моделей

# Этот файл импортируется Alembic для создания миграций
# Он должен содержать импорты всех моделей

Конвертация между моделями и схемами

Для конвертации между SQLAlchemy моделями и Pydantic схемами используется атрибут from_attributes = True в конфигурации схемы:

python
# Пример конвертации из модели в схему
from app.models.user import User as UserModel
from app.schemas.user import User as UserSchema

# Предположим, у нас есть экземпляр модели пользователя
db_user = await session.get(UserModel, user_id)

# Конвертируем его в Pydantic схему
user_schema = UserSchema.from_orm(db_user)  # В Pydantic v1
# ИЛИ
user_schema = UserSchema.model_validate(db_user)  # В Pydantic v2

# Теперь user_schema можно вернуть в ответе API
return user_schema

При обработке входящих данных Pydantic автоматически валидирует их:

python
# Пример приема и валидации входящих данных
from app.schemas.user import UserCreate

@app.post("/users/", response_model=schemas.User)
async def create_user(user_in: UserCreate):
    # user_in уже прошел валидацию Pydantic
    # Если данные не соответствуют схеме, FastAPI автоматически
    # вернет клиенту ошибку 422 Unprocessable Entity
    
    # Создание пользователя
    db_user = UserModel(
        email=user_in.email,
        hashed_password=get_password_hash(user_in.password),
        full_name=user_in.full_name,
    )
    
    # ... сохранение в базу данных

Расширенные возможности моделей

Наследование моделей

python
# app/models/mixins.py
from sqlalchemy import Column, DateTime
from sqlalchemy.sql import func

class TimestampMixin:
    """
    Миксин для добавления временных меток создания и обновления.
    """
    created_at = Column(DateTime, default=func.now())
    updated_at = Column(DateTime, default=func.now(), onupdate=func.now())

# app/models/post.py
from app.models.mixins import TimestampMixin
from app.db.base_class import Base

class Post(Base, TimestampMixin):
    """
    Модель поста в блоге с временными метками.
    """
    id = Column(Integer, primary_key=True)
    title = Column(String, index=True)
    content = Column(Text)
    # Поля created_at и updated_at унаследованы от TimestampMixin

Сложные отношения

python
# app/models/tag.py
from sqlalchemy import Column, ForeignKey, Integer, String, Table
from sqlalchemy.orm import relationship

from app.db.base_class import Base

# Таблица для связи many-to-many между постами и тегами
post_tag = Table(
    "post_tag",
    Base.metadata,
    Column("post_id", Integer, ForeignKey("post.id"), primary_key=True),
    Column("tag_id", Integer, ForeignKey("tag.id"), primary_key=True),
)

class Tag(Base):
    """
    Модель тега.
    """
    id = Column(Integer, primary_key=True)
    name = Column(String, unique=True, index=True)
    
    # Отношение many-to-many с постами
    posts = relationship("Post", secondary=post_tag, back_populates="tags")

# В модели Post добавляем:
# tags = relationship("Tag", secondary=post_tag, back_populates="posts")

Валидация в Pydantic схемах

python
# app/schemas/product.py
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, Field, validator

class ProductCreate(BaseModel):
    """
    Схема для создания продукта с валидацией.
    """
    name: str = Field(..., min_length=1, max_length=100)
    description: Optional[str] = Field(None, max_length=1000)
    price: Decimal = Field(..., gt=0)
    stock: int = Field(..., ge=0)
    
    @validator("name")
    def name_must_not_contain_invalid_chars(cls, v):
        if "/" in v or "\" in v:
            raise ValueError("Имя не должно содержать символы '/' или '\'")
        return v.strip()
    
    @validator("price")
    def price_must_have_two_decimal_places(cls, v):
        if v.quantize(Decimal("0.01")) != v:
            raise ValueError("Цена должна иметь не более двух знаков после запятой")
        return v

Советы и лучшие практики

  • Используйте разные схемы для разных операций (создание, обновление, чтение)
  • Не включайте конфиденциальные данные (например, хешированные пароли) в схемы ответов API
  • Используйте аннотации типов для улучшения проверки типов и автодокументации
  • Создавайте индексы для полей, по которым часто выполняются поиск и фильтрация
  • Используйте кэширование для снижения нагрузки на базу данных
  • Избегайте циклических импортов между моделями и схемами

Ресурсы