База данных
В этом разделе описывается конфигурация и работа с базой данных в Boilerplate. Вы узнаете, как настроить подключение к базе данных, создавать модели и выполнять запросы.
Обзор
Backend часть Boilerplate использует PostgreSQL в качестве основной базы данных и SQLAlchemy 2.0 в качестве ORM (Object-Relational Mapping) для работы с ней. Для управления миграциями используется Alembic.
Конфигурация подключения
Конфигурация подключения к базе данных определена в файле app/core/config.py
. Строка подключения к базе данных берется из переменных окружения:
# 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
. Вот пример модели пользователя:
# 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
:
# 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
:
# 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
в корне проекта.
Создание новой миграции
Для создания новой миграции выполните команду:
alembic revision --autogenerate -m 'Описание миграции'
Применение миграций
Для применения всех миграций:
alembic upgrade head
Для применения определенного количества миграций:
alembic upgrade +1 # Применить следующую миграцию
Откат миграций
Для отката последней миграции:
alembic downgrade -1
CRUD операции
CRUD (Create, Read, Update, Delete) операции определены в директории app/crud
. Вот пример CRUD операций для пользователей:
# 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 эндпоинтах следующим образом:
# 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
:
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 при необходимости
- Используйте пул соединений для эффективного использования ресурсов базы данных