← Назад к вопросам
Что делать, если в SQLAlchemy возникло исключение при выполнении SQL запроса?
1.0 Junior🔥 221 комментариев
#DevOps и инфраструктура#Django
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Обработка исключений в SQLAlchemy: Правильный подход
Это важный вопрос, потому что неправильная обработка исключений в БД может привести к потере данных или падению приложения.
Типы исключений в SQLAlchemy
SQLAlchemy вызывает несколько типов исключений:
from sqlalchemy.exc import (
SQLAlchemyError, # Базовое исключение всех SQLAlchemy ошибок
DBAPIError, # Ошибка от драйвера БД
IntegrityError, # Нарушение constraints (FK, UNIQUE, NOT NULL)
OperationalError, # Ошибки соединения
DatabaseError, # Ошибки в SQL
InvalidRequestError, # Ошибки использования SQLAlchemy API
ProgrammingError, # Синтаксис SQL или параметры
NotSupportedError, # Операция не поддерживается БД
)
# Иерархия исключений:
# SQLAlchemyError (базовое)
# ├── DBAPIError
# │ ├── IntegrityError
# │ ├── OperationalError
# │ ├── DatabaseError
# │ ├── ProgrammingError
# │ └── NotSupportedError
# ├── InvalidRequestError
# └── ...
Общий подход к обработке
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError, OperationalError
def create_user(db: Session, name: str, email: str):
try:
user = User(name=name, email=email)
db.add(user)
db.commit() # здесь могут быть исключения
return user
except IntegrityError as e:
# Нарушение unique/pk/fk constraint
db.rollback()
raise ValueError(f"User with this email already exists")
except OperationalError as e:
# Проблема с подключением
db.rollback()
raise ConnectionError(f"Database connection failed: {e}")
except Exception as e:
# Ловушка для всех остальных ошибок
db.rollback()
logger.error(f"Unexpected error: {e}")
raise
Практический пример 1: Нарушение unique constraint
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
import logging
logger = logging.getLogger(__name__)
class UserService:
def create_user(self, db: Session, name: str, email: str) -> User:
"""
Создаёт пользователя. Если email уже существует — вызывает исключение.
"""
try:
user = User(name=name, email=email)
db.add(user)
db.commit() # МОЖЕТ БЫТЬ IntegrityError
return user
except IntegrityError as e:
db.rollback() # Откатываем транзакцию
# Определяем, какое именно поле нарушило constraint
if 'email' in str(e):
raise ValueError(f"Email '{email}' already exists")
elif 'username' in str(e):
raise ValueError(f"Username '{name}' already exists")
else:
logger.error(f"Integrity error: {e}")
raise ValueError("User with these credentials already exists")
except Exception as e:
db.rollback()
logger.error(f"Unexpected error creating user: {e}")
raise
Практический пример 2: Проблемы с соединением
from sqlalchemy.exc import OperationalError
from time import sleep
import logging
logger = logging.getLogger(__name__)
class UserRepository:
def get_user(self, db: Session, user_id: str, max_retries=3) -> User:
"""
Получить пользователя с автоматическим retry при ошибках соединения.
"""
for attempt in range(max_retries):
try:
user = db.query(User).filter(User.id == user_id).first()
return user
except OperationalError as e:
# Проблема с БД (например, временно недоступна)
if attempt < max_retries - 1:
# Ждём и пробуем снова
logger.warning(f"DB connection failed, retry {attempt + 1}/{max_retries}")
sleep(1)
continue
else:
# Все попытки исчерпаны
logger.error(f"Failed to connect to database after {max_retries} attempts")
raise ConnectionError("Database is unavailable")
except Exception as e:
logger.error(f"Unexpected error: {e}")
raise
Практический пример 3: Откат и логирование
from sqlalchemy.orm import Session
from sqlalchemy.exc import SQLAlchemyError
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
class OrderService:
def create_order(self, db: Session, user_id: str, items: list) -> Order:
"""
Создать заказ с товарами. Если что-то не так — откатить всё.
"""
try:
# Создаём заказ
order = Order(
user_id=user_id,
created_at=datetime.utcnow(),
status="pending"
)
db.add(order)
db.flush() # Получаем order.id
# Добавляем товары
for item in items:
order_item = OrderItem(
order_id=order.id,
product_id=item['product_id'],
quantity=item['quantity']
)
db.add(order_item)
db.commit() # Коммитим обе операции
logger.info(f"Order {order.id} created successfully")
return order
except SQLAlchemyError as e:
# Откатываем ВСЕЙ транзакцию
db.rollback()
# Логируем ошибку с контекстом
logger.error(
f"Error creating order for user {user_id}",
exc_info=True, # Включит full stack trace
extra={"user_id": user_id, "items_count": len(items)}
)
# Преобразуем в понятное исключение
if "constraint" in str(e):
raise ValueError("One or more items are invalid")
else:
raise RuntimeError("Failed to create order")
Как НЕ обрабатывать исключения
# ❌ ПЛОХО: игнорируем ошибку
def bad_create_user(db: Session, name: str, email: str):
try:
user = User(name=name, email=email)
db.add(user)
db.commit()
return user
except Exception as e:
pass # ❌ Молча игнорируем! Пользователь не знает, что произошло
# ❌ ПЛОХО: слишком общая обработка
def bad_create_user_2(db: Session, name: str, email: str):
try:
user = User(name=name, email=email)
db.add(user)
db.commit()
return user
except: # ❌ Ловим ВСЕ, даже KeyboardInterrupt!
print("error") # ❌ print вместо логирования
# ❌ ПЛОХО: не откатываем транзакцию
def bad_create_user_3(db: Session, name: str, email: str):
try:
user = User(name=name, email=email)
db.add(user)
db.commit()
return user
except Exception as e:
# Не откатали! Сессия в broken state
raise
# ❌ ПЛОХО: двойное логирование
def bad_create_user_4(db: Session, name: str, email: str):
try:
user = User(name=name, email=email)
db.add(user)
db.commit()
return user
except Exception as e:
logger.error(f"Error: {e}") # логируем
raise ValueError(f"Error: {e}") # логируем снова на уровне выше
Правильный паттерн обработки
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError, OperationalError, SQLAlchemyError
from enum import Enum
import logging
logger = logging.getLogger(__name__)
class ErrorCategory(Enum):
VALIDATION = "validation" # user input ошибка
NOT_FOUND = "not_found" # ресурс не найден
CONFLICT = "conflict" # конфликт данных
INTERNAL = "internal" # ошибка сервера
class DatabaseError(Exception):
def __init__(self, message: str, category: ErrorCategory):
self.message = message
self.category = category
super().__init__(message)
class UserRepository:
def create_user(self, db: Session, name: str, email: str) -> User:
try:
user = User(name=name, email=email)
db.add(user)
db.commit()
return user
except IntegrityError as e:
db.rollback()
logger.warning(f"User creation conflict: {e}")
# Преобразуем в semantically correct исключение
raise DatabaseError(
"User with this email already exists",
ErrorCategory.CONFLICT
)
except OperationalError as e:
db.rollback()
logger.error(f"Database connection error: {e}")
raise DatabaseError(
"Database is temporarily unavailable",
ErrorCategory.INTERNAL
)
except SQLAlchemyError as e:
db.rollback()
logger.error(f"Database error: {e}", exc_info=True)
raise DatabaseError(
"An unexpected database error occurred",
ErrorCategory.INTERNAL
)
# Обработка на уровне API
from fastapi import FastAPI, HTTPException
app = FastAPI()
repo = UserRepository()
@app.post("/api/v1/users")
async def create_user(name: str, email: str, db: Session):
try:
user = repo.create_user(db, name, email)
return {"id": user.id, "name": user.name}
except DatabaseError as e:
# Преобразуем в HTTP ответ
if e.category == ErrorCategory.CONFLICT:
raise HTTPException(status_code=409, detail=e.message)
elif e.category == ErrorCategory.INTERNAL:
raise HTTPException(status_code=503, detail=e.message)
else:
raise HTTPException(status_code=400, detail=e.message)
Чек-лист для правильной обработки
# При каждом db.commit() или db.execute() спроси себя:
# ✅ Я откачиваю транзакцию при ошибке? (db.rollback())
# ✅ Я логирую ошибку? (logger.error())
# ✅ Я преобразую technical error в user-friendly error?
# ✅ Я не игнорирую исключение молча?
# ✅ Я не ловлю слишком широкие исключения (Exception, BaseException)?
# ✅ Я различаю разные типы ошибок? (Integrity, Operational, etc.)
# ✅ Я не делаю двойное логирование одной ошибки?
# ✅ Я возвращаю смысловое исключение для caller'а?
Итог
Правильная обработка исключений в SQLAlchemy:
- Откатывай транзакцию при любой ошибке (db.rollback())
- Специфичная обработка для разных типов ошибок
- Логируй, но не дважды — логируй только на том уровне, где ловишь
- Преобразуй в понятное исключение для caller'а
- Никогда не ловишь голый Exception — будешь ловить KeyboardInterrupt
Для интервью: я понимаю, что исключение в БД — это признак того, что транзакция в broken state и нужно откатить. Я знаю разные типы ошибок и как их обрабатывать правильно.