SQL в Python — это антипаттерн?

Очень коротко. И это мое личное и очень субъективное мнение. Не судите строго.

Хранение 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-запросов при изменении схемы БД:
    • Нужно искать по всему коду,
    • Легко пропустить запрос, особенно в динамически собираемых строках.
  • Пример: Переименование колонки usernamelogin_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 в коде допустим?

  1. Прототипирование — если проект маленький и временный.
  2. Сложные аналитические запросы — которые трудно выразить через ORM.
  3. Raw-оптимизация — когда ORM не справляется с нагрузкой.

Но даже в этих случаях:

  • Используйте параметризованные запросы.
  • Выносите SQL в константы или отдельные модули.