Очень коротко. И это мое личное и очень субъективное мнение. Не судите строго.
Хранение SQL-запросов прямо в Python-коде действительно не очень хорошо в большинстве случаев. И вот веские и не очень аргументы против этого и альтернативные решения:
Критические проблемы SQL в коде
1. Нарушение принципа разделения ответственности (SoC)
- Проблема: SQL — это логика работы с данными, Python — бизнес-логика. Их смешивание усложняет поддержку.
- Что бывает: Изменение структуры БД требует правки кода, а не только конфигурации.
- Решение: Выносить SQL в:
.sql
-файлы,- ORM-модели,
- Query Builder (например, SQLAlchemy Core).
2. Уязвимости к SQL-инъекциям
- Проблема: Ручная конкатенация строк в запросах — это риск:
# Опасный код! query = f"SELECT * FROM users WHERE name = '{user_input}'"
- Пример атаки: Если
user_input = "admin'; DROP TABLE users--"
, БД будет повреждена. - Решение: Всегда использовать:
- Параметризованные запросы (
cursor.execute("... WHERE name = %s", (name,)
), - ORM/SQLAlchemy.
- Параметризованные запросы (
3. Сложность тестирования
- Проблема: SQL в коде требует:
- Поднятия тестовой БД,
- Моков курсоров.
- Пример: Тест для такого кода становится монструозным:
def test_get_user(): # Надо замокать БД, хотя тестируется логика, а не SQL! ...
- Решение: Выносить запросы в интерфейсы (например,
UserRepository
), которые можно подменить на заглушки.
4. Трудности с рефакторингом
- Проблема: Поиск всех SQL-запросов при изменении схемы БД:
- Нужно искать по всему коду,
- Легко пропустить запрос, особенно в динамически собираемых строках.
- Пример: Переименование колонки
username
→login_name
потребует ручного аудита. - Решение: Использовать ORM, где схема БД описана централизованно.
5. Отсутствие проверки на этапе компиляции
- Проблема: Ошибки в SQL (опечатки, несуществующие таблицы) обнаруживаются только в рантайме.
- Пример:
# Ошибка проявится только при вызове функции! query = "SELECT * FROM usrs" # Опечатка в имени таблицы
- Решение: ORM и инструменты вроде SQLAlchemy или pydantic проверяют типы и структуру заранее.
6. Проблемы с миграциями БД
- Проблема: При изменении запросов в коде история миграций (Alembic) теряет связь с реальностью.
- Решение: Хранить SQL отдельно и управлять миграциями через скрипты.
Альтернативы: как правильно?
1. ORM (SQLAlchemy, Django ORM)
- Плюсы:
- Нет прямого SQL,
- Автоматическая защита от инъекций,
- Проверка типов.
-
Пример:
from sqlalchemy.orm import declarative_base Base = declarative_base() class User(Base): __tablename__ = 'users' id = Column(Integer, primary_key=True) name = Column(String)
2. SQL-файлы + параметризация
- Плюсы:
- Чистота кода,
- Возможность повторного использования запросов.
- Пример:
-- queries/get_user.sql SELECT * FROM users WHERE id = :user_id
with open("queries/get_user.sql") as f: query = f.read() cursor.execute(query, {"user_id": 123})
3. Query Builder (SQLAlchemy Core)
- Плюсы:
- Безопасность,
- Переносимость между СУБД.
- Пример:
from sqlalchemy import select query = select(users).where(users.c.id == 123)
4. Pydantic для валидации
- Плюсы:
- Контроль данных перед записью в БД,
- Сериализация ответов.
- Пример:
from pydantic import BaseModel class UserCreate(BaseModel): name: str email: str
Когда SQL в коде допустим?
- Прототипирование — если проект маленький и временный.
- Сложные аналитические запросы — которые трудно выразить через ORM.
- Raw-оптимизация — когда ORM не справляется с нагрузкой.
Но даже в этих случаях:
- Используйте параметризованные запросы.
- Выносите SQL в константы или отдельные модули.