Модели данных
В этом разделе описаны модели данных используемые в Boilerplate. Вы узнаете о структуре моделей SQL Alchemy, Pydantic схемах валидации и конвертации данных между ними.
Обзор моделей данных
В Boilerplate используется два основных типа моделей данных:
- SQLAlchemy модели - определяют структуру таблиц в базе данных и используются для ORM операций.
- Pydantic схемы - используются для валидации данных на входе и выходе API, сериализации/десериализации и документирования API.
SQLAlchemy модели
SQLAlchemy модели определены в директории app/models
. Все модели наследуются от базового класса Base
.
Базовый класс
# 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()
Пример модели пользователя
# 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")
Пример модели с отношениями
# 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.
Схемы пользователя
# 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
Схемы элемента
# 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 используется четкая организация моделей и схем:
Структура директорий
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
:
# 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
в конфигурации схемы:
# Пример конвертации из модели в схему
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 автоматически валидирует их:
# Пример приема и валидации входящих данных
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,
)
# ... сохранение в базу данных
Расширенные возможности моделей
Наследование моделей
# 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
Сложные отношения
# 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 схемах
# 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
- Используйте аннотации типов для улучшения проверки типов и автодокументации
- Создавайте индексы для полей, по которым часто выполняются поиск и фильтрация
- Используйте кэширование для снижения нагрузки на базу данных
- Избегайте циклических импортов между моделями и схемами