База данных

В этом разделе описывается конфигурация и работа с базой данных в Boilerplate. Вы узнаете, как настроить подключение к базе данных, создавать модели и выполнять запросы.

Обзор

Backend часть Boilerplate использует PostgreSQL в качестве основной базы данных и SQLAlchemy 2.0 в качестве ORM (Object-Relational Mapping) для работы с ней. Для управления миграциями используется Alembic.

Конфигурация подключения

Конфигурация подключения к базе данных определена в файле app/core/config.py. Строка подключения к базе данных берется из переменных окружения:

python
# app/core/config.py
from pydantic import PostgresDsn, field_validator
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    POSTGRES_SERVER: str
    POSTGRES_USER: str
    POSTGRES_PASSWORD: str
    POSTGRES_DB: str
    SQLALCHEMY_DATABASE_URI: PostgresDsn | None = None

    @field_validator("SQLALCHEMY_DATABASE_URI", mode="before")
    def assemble_db_connection(cls, v: str | None, info) -> str:
        if isinstance(v, str):
            return v
        
        values = info.data
        return PostgresDsn.build(
            scheme="postgresql+asyncpg",
            username=values.get("POSTGRES_USER"),
            password=values.get("POSTGRES_PASSWORD"),
            host=values.get("POSTGRES_SERVER"),
            path=f"{values.get('POSTGRES_DB') or ''}",
        )

Определение моделей

Модели данных определены в директории app/models. Вот пример модели пользователя:

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):
    __tablename__ = "users"

    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/db/base_class.py:

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

from sqlalchemy.ext.declarative import as_declarative, declared_attr

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

Инициализация базы данных

Инициализация подключения к базе данных определена в файле app/db/session.py:

python
# app/db/session.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker

from app.core.config import settings

# Создаем асинхронный движок SQLAlchemy
engine = create_async_engine(
    settings.SQLALCHEMY_DATABASE_URI,
    pool_pre_ping=True,
    echo=settings.SQLALCHEMY_ECHO,
)

# Создаем фабрику сессий
AsyncSessionLocal = sessionmaker(
    engine,
    class_=AsyncSession,
    expire_on_commit=False,
    autocommit=False,
    autoflush=False,
)

# Функция-зависимость для получения сессии БД
async def get_db() -> AsyncSession:
    async with AsyncSessionLocal() as session:
        yield session

Миграции с Alembic

Для управления миграциями базы данных используется Alembic. Конфигурация находится в файлеalembic.ini в корне проекта.

Создание новой миграции

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

bash
alembic revision --autogenerate -m 'Описание миграции'

Применение миграций

Для применения всех миграций:

bash
alembic upgrade head

Для применения определенного количества миграций:

bash
alembic upgrade +1  # Применить следующую миграцию

Откат миграций

Для отката последней миграции:

bash
alembic downgrade -1

CRUD операции

CRUD (Create, Read, Update, Delete) операции определены в директории app/crud. Вот пример CRUD операций для пользователей:

python
# app/crud/crud_user.py
from typing import Any, Dict, Optional, Union

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.security import get_password_hash, verify_password
from app.models.user import User
from app.schemas.user import UserCreate, UserUpdate

class CRUDUser:
    async def get(self, db: AsyncSession, id: int) -> Optional[User]:
        query = select(User).where(User.id == id)
        result = await db.execute(query)
        return result.scalars().first()

    async def get_by_email(self, db: AsyncSession, email: str) -> Optional[User]:
        query = select(User).where(User.email == email)
        result = await db.execute(query)
        return result.scalars().first()

    async def create(self, db: AsyncSession, *, obj_in: UserCreate) -> User:
        db_obj = User(
            email=obj_in.email,
            hashed_password=get_password_hash(obj_in.password),
            full_name=obj_in.full_name,
            is_superuser=obj_in.is_superuser,
        )
        db.add(db_obj)
        await db.commit()
        await db.refresh(db_obj)
        return db_obj

    async def update(
        self, db: AsyncSession, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]]
    ) -> User:
        if isinstance(obj_in, dict):
            update_data = obj_in
        else:
            update_data = obj_in.dict(exclude_unset=True)
            
        if update_data.get("password"):
            hashed_password = get_password_hash(update_data["password"])
            update_data["hashed_password"] = hashed_password
            del update_data["password"]
            
        for field in update_data:
            if hasattr(db_obj, field):
                setattr(db_obj, field, update_data[field])
                
        db.add(db_obj)
        await db.commit()
        await db.refresh(db_obj)
        return db_obj

    async def delete(self, db: AsyncSession, *, id: int) -> Optional[User]:
        obj = await self.get(db, id)
        if obj:
            await db.delete(obj)
            await db.commit()
        return obj

user = CRUDUser()

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

CRUD операции используются в API эндпоинтах следующим образом:

python
# app/api/routes/users.py
from typing import Any, List

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession

from app import crud, schemas
from app.api.deps import get_current_superuser, get_db

router = APIRouter()

@router.get("/", response_model=List[schemas.User])
async def read_users(
    db: AsyncSession = Depends(get_db),
    skip: int = 0,
    limit: int = 100,
    current_user: schemas.User = Depends(get_current_superuser),
) -> Any:
    """
    Получить список пользователей.
    """
    users = await crud.user.get_multi(db, skip=skip, limit=limit)
    return users

@router.post("/", response_model=schemas.User)
async def create_user(
    *,
    db: AsyncSession = Depends(get_db),
    user_in: schemas.UserCreate,
    current_user: schemas.User = Depends(get_current_superuser),
) -> Any:
    """
    Создать нового пользователя.
    """
    user = await crud.user.get_by_email(db, email=user_in.email)
    if user:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Пользователь с таким email уже существует",
        )
    user = await crud.user.create(db, obj_in=user_in)
    return user

@router.get("/me", response_model=schemas.User)
async def read_user_me(
    current_user: schemas.User = Depends(get_current_user),
) -> Any:
    """
    Получить текущего пользователя.
    """
    return current_user

Docker-конфигурация базы данных

База данных PostgreSQL запускается в Docker контейнере и определена в файле docker-compose.yml:

yaml
services:
  # ... другие сервисы
  
  db:
    image: postgres:13
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=app
    ports:
      - "5432:5432"

volumes:
  postgres_data:

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

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

Ресурсы