Django & Raw SQL: ловушки, угрозы и защита от SQL-инъекций

Django & Raw SQL: ловушки, угрозы и защита от SQL-инъекций

Картинка к публикации: Django & Raw SQL: ловушки, угрозы и защита от SQL-инъекций

Введение в мир сырых SQL-запросов

Представьте себе типичного Django-разработчика: чашка кофе, редактор кода, медитативное постукивание по клавиатуре. Он счастлив, ORM всё делает за него — отправляет запросы в базу, генерирует модели, заботится о целостности данных. В общем, жизнь прекрасна. Но вот наступает тот самый день, когда ORM смотрит на вас с грустным взглядом и разводит руками: «Извини, дружище, это не по моей части». И вы понимаете, что мир сложнее, чем казалось: придётся засучить рукава и пойти на тёмную сторону — писать сырые SQL-запросы.

Причины, по которым разработчики Django начинают использовать raw SQL, бывают разными. Иногда дело в производительности: ORM, хоть и удобен, далеко не всегда способен выжать из базы максимум производительности. Когда запросы становятся сложными и требуют использования специфических возможностей СУБД, ORM начинает генерировать такие кошмарные конструкции, что оптимизировать их вручную оказывается быстрее, чем разбираться с Django. Вспомним ситуацию, когда нужно подсчитать статистику по миллионам строк, объединяя таблицы с хитрыми условиями. Да, можно заставить ORM выполнить этот запрос, но база данных от этого вряд ли будет в восторге, а ваш DevOps-инженер начнёт подозревать вас в саботаже.

Другой распространённой причиной бывает необходимость использовать специфичные для конкретной базы данных функции или конструкции, которые Django ORM просто не поддерживает из коробки. Например, когда вам вдруг понадобилась рекурсивная выборка (recursive CTE) в PostgreSQL, хитрый индекс в MySQL, или же специальная конструкция JSON-функций, которая недавно появилась в вашей базе данных. В этот момент разработчику ничего не остаётся, как воспользоваться старым добрым SQL и аккуратно интегрировать его в своё Django-приложение.

Разумеется, такой подход несёт не только плюсы, но и очевидные минусы. С одной стороны, прямое обращение к базе может радикально улучшить производительность, предоставить доступ к тонким настройкам и продвинутым функциям СУБД. С другой — резко возрастает ответственность разработчика. Ошибки в сырых SQL-запросах, особенно при работе с пользовательскими данными, могут обернуться катастрофой в виде SQL-инъекций, потери данных и компрометации безопасности проекта. ORM даёт абстракцию, которая зачастую спасает разработчиков от роковых ошибок, но при работе с raw SQL эта страховка исчезает — и вся ответственность ложится исключительно на ваши плечи.

Несмотря на все риски, отказ от ORM иногда неизбежен. Важно чётко понимать, когда это оправдано и необходимо, а когда является результатом неопытности или стремления к преждевременной оптимизации. Сырые SQL-запросы — это мощный инструмент, который в руках опытного разработчика может существенно ускорить и улучшить работу приложения. Однако помните: чем больше эта мощность, тем выше ответственность и тем сложнее последствия ошибок.

Далее, мы детально погрузимся в природу SQL-инъекций и посмотрим, почему даже самые опытные разработчики иногда оказываются бессильны перед банальными, но чрезвычайно опасными ошибками безопасности. Будьте внимательны и не забывайте, что ORM создан не просто так, но иногда и он может оставить вас один на один с сырой мощью SQL.

SQL-инъекции, что это такое?

Спросите любого разработчика, чего он боится больше всего — и он наверняка назовёт что-то банальное вроде падения продакшна в пятницу вечером. Но есть вещи пострашнее: одна из них — это SQL-инъекции. Самое пугающее тут даже не то, что инъекции существуют, а то, как просто они возникают и как огромны последствия даже от одной единственной ошибки.

SQL-инъекция, если говорить по-простому, это такой хитрый способ обмана вашего приложения, при котором злоумышленник вставляет вредоносный SQL-код прямо в ваш запрос. Как это вообще возможно? Очень просто: достаточно всего лишь небрежно вставить пользовательский ввод в запрос, и маленькая безобидная строка превращается в цифровой троянский конь.

Представим классический пример. Допустим, у вас есть обычный метод входа в систему, написанный на Django с сырым SQL-запросом (ведь мы же уже решили, что ORM иногда бывает недостаточно крутым):

def authenticate_user(request):
    username = request.POST['username']
    password = request.POST['password']
    
    query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}';"
    user = User.objects.raw(query)
    # дальше идёт авторизация... 

На первый взгляд, вроде ничего криминального. Но что будет, если вместо пароля пользователь отправит вот такую интересную строчку:

' OR '1'='1

Ваш безобидный запрос превратится в:

SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1';

Ой-ой, у нас тут всегда истинное условие '1'='1'. Поздравляем, теперь ваш хитрый пользователь — «админ» без всяких паролей. И это самый безобидный сценарий. Представьте, что будет, если вместо простого взлома кто-то отправит запрос на удаление всей базы, вроде:

'; DROP TABLE users; -- 

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

Даже опытные разработчики попадают в эту ловушку. Казалось бы, вы знаете о проблеме, знаете правила безопасности, но дедлайны жмут, надо быстро закрыть тикет, и вот уже вы пишете что-то вроде:

query = f"SELECT * FROM products WHERE category = '{category}' AND price < {max_price};"

Знакомо, правда? Особенно когда пользователь вводит категорию «'; --», превращая ваш запрос в весёлую SQL-лотерею.

Почему это происходит? Причин две: самоуверенность и невнимательность. Мы уверены, что никто не будет специально взламывать именно наш маленький стартап, но в интернете полно автоматических скриптов, которые буквально круглосуточно ищут именно такие ошибки. И поверьте, ваш проект в их глазах ни чем не отличается от сайтов крупных корпораций.

SQL-инъекции — это не просто теоретическая проблема или страшилка для новичков. Ежегодно тысячи компаний страдают от таких уязвимостей, теряя данные клиентов и миллионы долларов. И дело даже не в том, что вы можете стать жертвой атаки, а в том, насколько легко её можно предотвратить.

Как именно защититься и как перестать бояться SQL-инъекций, мы разберём чуть позже, но пока запомните главное правило: пользовательскому вводу в сыром SQL-запросе доверять нельзя никогда. Даже если он выглядит невинно и безобидно. Даже если пользователь — ваша собственная бабушка.

«SELECT * FROM ловушка» или типичные ошибки

Каждый разработчик Django рано или поздно попадает в ситуацию, когда нужно быстро решить задачу, а времени разобраться с правильной реализацией просто нет. В такие моменты срабатывает известный подход — «давайте сейчас сделаем так, а потом переделаем нормально». Если вы улыбнулись при чтении этой фразы, поздравляю — вы типичный программист. Но проблема в том, что именно такой подход и приводит к возникновению серьёзных уязвимостей, которые злоумышленники потом с радостью эксплуатируют.

Одна из самых распространённых ошибок Django-разработчиков при работе с сырым SQL — это использование пользовательского ввода напрямую в запросах без какой-либо проверки или экранирования. Знаете ли вы, что самым частым вариантом такого промаха является банальное использование SELECT * вместе с необработанным пользовательским вводом? Рассмотрим пример, который наверняка встретится вам в дикой природе какого-нибудь стартапа:

category = request.GET.get('category')

query = f"SELECT * FROM products WHERE category = '{category}'"
products = Product.objects.raw(query)

На первый взгляд, запрос выглядит совершенно безобидно. Но стоит злоумышленнику отправить значение категории в виде такой строки:

electronics'; DROP TABLE products; -- 

и запрос превращается в такую «радость»:

SELECT * FROM products WHERE category = 'electronics'; DROP TABLE products; --' 

Прекрасно! Буквально одной строчкой злоумышленник стёр всю вашу таблицу товаров. Если бы речь шла о рабочем интернет-магазине, последствия были бы катастрофическими: потеря данных, потеря заказов, гнев клиентов и слёзы директора по маркетингу. И всё это из-за одной небольшой ошибки.

Ещё одна типичная ситуация, которая регулярно становится проблемой: использование строковой интерполяции для формирования запросов с числовыми параметрами. Например, вот такой код — классический пример ошибки новичка:

user_id = request.GET.get('user_id')
query = f"SELECT * FROM user_profiles WHERE id = {user_id}"
profiles = UserProfile.objects.raw(query)

Что здесь может пойти не так? Представьте, если пользователь отправит вместо id такую конструкцию:

0 OR 1=1

В результате мы получаем запрос:

SELECT * FROM user_profiles WHERE id = 0 OR 1=1 

И снова у нас на руках полная выборка всех профилей из базы данных. Вы только что подарили злоумышленнику всю вашу клиентскую базу — поздравляем, вы очень щедрый человек!

Также опасно забывать про ошибки при работе с датами и временем в запросах. Допустим, вы хотите получить пользователей, которые зарегистрировались после какой-то даты:

date_from = request.GET.get('date_from')
query = f"SELECT * FROM users WHERE created_at >= '{date_from}'"
users = User.objects.raw(query)

А теперь представьте, что кто-то отправляет вместо даты такую хитрую конструкцию:

2024-01-01'; UPDATE users SET is_admin = true; -- 

И ваш запрос становится вот таким:

SELECT * FROM users WHERE created_at >= '2024-01-01'; UPDATE users SET is_admin = true; --' 

Теперь каждый пользователь — админ. Отлично, мы только что устроили революцию в системе безопасности компании.

К сожалению, даже опытные Django-разработчики допускают такие ошибки, просто из-за спешки или невнимательности. Важно понимать, что любое включение пользовательского ввода в raw SQL-запросы без параметризации или экранирования неизбежно превращает ваше приложение в потенциальную мишень для хакеров. «SELECT * FROM ловушка» подстерегает всех, кто игнорирует базовые правила безопасности.

Но не всё потеряно — существуют надёжные способы защиты от подобных ситуаций. Как именно правильно готовить сырые запросы и избегать попадания в ловушки, мы подробно разберём в следующей главе. Главное, что стоит запомнить уже сейчас: никогда не вставляйте пользовательский ввод напрямую в SQL-код. Никогда! Даже если вам это кажется «временным решением» — поверьте, злоумышленники будут счастливы воспользоваться вашим гостеприимством.

Правильное приготовление raw SQL

Теперь, когда мы уже достаточно напугали вас ужасными последствиями небрежного использования пользовательских данных в сырых SQL-запросах, пора показать, как делать это правильно. Иначе вы можете начать думать, что единственный безопасный способ работы с raw SQL — вообще его не использовать, а это далеко не всегда возможно. Хорошая новость: писать безопасный код не намного сложнее, чем писать плохой — главное делать это осознанно.

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

Рассмотрим правильный подход на примере. Вместо такого опасного запроса:

category = request.GET.get('category')
query = f"SELECT * FROM products WHERE category = '{category}'"
products = Product.objects.raw(query)

Правильно будет писать вот так:

category = request.GET.get('category')
query = "SELECT * FROM products WHERE category = %s"
products = Product.objects.raw(query, [category])

В этом случае Django самостоятельно обработает параметр, корректно экранируя любые вредоносные вставки. Если злоумышленник попробует передать:

electronics'; DROP TABLE products; -- 

то эта строка будет воспринята как значение для фильтрации, а не как часть запроса. Результат: никакого дропа таблиц, злоумышленник грустно идёт ломать другие сайты, а вы продолжаете спокойно пить кофе.

Точно так же дела обстоят и с числовыми параметрами. Вместо опасного варианта:

user_id = request.GET.get('user_id')
query = f"SELECT * FROM users WHERE id = {user_id}"
users = User.objects.raw(query)

Лучше использовать параметризованный запрос:

user_id = request.GET.get('user_id')
query = "SELECT * FROM users WHERE id = %s"
users = User.objects.raw(query, [user_id])

Если вдруг пользователь попытается подсунуть конструкцию вроде 0 OR 1=1, Django обработает её как текстовый параметр, а не как часть SQL-команды. Итог очевиден — хакер снова в пролёте.

Однако parameterized queries — это не только безопасность, но и небольшая производительность за счёт использования подготовленных выражений (prepared statements). Что это такое? Prepared statement — это предварительно скомпилированный запрос, в котором определены места для параметров. Сервер базы данных заранее «готовится» выполнять такой запрос многократно, подставляя туда только меняющиеся данные. Это и быстрее, и безопаснее, и приятнее. Правда, в Django явная поддержка prepared statements не всегда прозрачна, но при использовании cursor.execute() вы всё ещё можете воспользоваться этим преимуществом:

from django.db import connection

with connection.cursor() as cursor:
    category = request.GET.get('category')
    query = "SELECT id, name, price FROM products WHERE category = %s"
    cursor.execute(query, [category])
    products = cursor.fetchall()

Такой подход даёт ещё больше гибкости и прозрачности, а самое главное — отлично защищает от SQL-инъекций, передавая управление обработкой параметров драйверу базы данных.

Ещё один важный момент — это обработка специальных случаев, таких как списки или массивы параметров. Например, нужно выбрать несколько продуктов по списку ID. Никогда не делайте вот так:

ids = request.GET.get('ids')  # '1,2,3'
query = f"SELECT * FROM products WHERE id IN ({ids})"

Это прямая дорога в ад. Правильно:

ids = [int(x) for x in request.GET.get('ids', '').split(',') if x.isdigit()]  # ['1', '2', '3']
placeholders = ', '.join(['%s'] * len(ids))
query = f"SELECT * FROM products WHERE id IN ({placeholders})"

with connection.cursor() as cursor:
    cursor.execute(query, ids)
    products = cursor.fetchall()

Таким образом, вы избегаете прямой вставки параметров и снова выигрываете бой с хакером, даже не заметив этого.

Вывод прост и предельно ясен: если вы используете raw SQL, никогда не ленитесь и не игнорируйте параметризацию запросов. Это не мелочь и не эстетическая причуда, а ключевое правило, которое позволит вам спокойно спать ночью и не думать о том, кто в этот момент ломает ваш проект.

Инъекциям вход воспрещён

Теперь, когда вы знаете, как избежать самых распространённых ошибок при написании raw SQL в Django, пришла пора немного углубиться и понять, что настоящая защита от инъекций — это не только правильные запросы, но и комплексный подход к безопасности всего приложения.

Первое и самое главное правило (после уже упомянутой нами параметризации): валидация данных на всех уровнях. Да, именно так: проверять нужно не только типы данных, но и их формат, размер и допустимые значения. Почему это важно? Всё просто — чем строже вы проверяете ввод пользователя, тем меньше вероятность, что в вашу базу попадёт что-то подозрительное. Например, если пользователь вводит номер телефона, убедитесь, что там только цифры и допустимые спецсимволы, а если он вводит email — используйте встроенные валидаторы Django, которые значительно упростят вашу жизнь:

from django.core.validators import validate_email
from django.core.exceptions import ValidationError

email = request.POST.get('email')
try:
    validate_email(email)
except ValidationError:
    # Отправляем пользователя обратно с ошибкой
    return HttpResponse("Кажется, вы ввели не совсем email.")

Кроме того, полезно внедрить строгую проверку данных непосредственно перед использованием в SQL-запросах. К примеру, если вы знаете, что параметр должен быть числом, никогда не доверяйте пользователю — всегда проверяйте сами:

product_id = request.GET.get('product_id')
if not product_id.isdigit():
    return HttpResponse("Простите, но это явно не ID продукта.")

query = "SELECT * FROM products WHERE id = %s"
products = Product.objects.raw(query, [product_id])

Такое простое правило значительно затруднит жизнь потенциальным взломщикам.

Следующий важный элемент защиты — настройки ограничений на уровне самой базы данных. Даже если вы ошибётесь где-то в коде, корректная настройка прав доступа к базе может спасти ваш проект. Ограничьте права вашего основного пользователя базы данных. Если приложению нужны только SELECT, INSERT и UPDATE, то не стоит давать ему право на DROP и ALTER таблиц:

GRANT SELECT, INSERT, UPDATE ON database_name.* TO 'app_user'@'localhost';

Таким образом, даже если злоумышленник найдёт дыру в вашем приложении, он не сможет разрушить вашу базу данных полностью. Максимум — внести изменения в данные, а это уже гораздо проще обнаружить и исправить.

Ещё один важный подход к безопасности — экранирование данных. Здесь важно уточнить: если вы используете Django ORM или корректно используете параметризованные запросы, то дополнительного ручного экранирования обычно не требуется. Django и драйвер базы данных делают это за вас. Однако если вы по какой-то причине формируете сложные динамические запросы, используйте встроенные функции Django или драйвера базы данных для экранирования, никогда не пытайтесь делать это вручную:

from django.db import connection

user_input = request.GET.get('search_text')
with connection.cursor() as cursor:
    cursor.execute("SELECT * FROM articles WHERE title LIKE %s", [f"%{user_input}%"])
    articles = cursor.fetchall()

В этом случае Django самостоятельно экранирует опасные символы.

Теперь про более продвинутый сценарий — комбинирование ORM и raw SQL. Иногда вы можете использовать ORM для простых задач, а более сложные вещи реализовывать на сыром SQL. Например, получение сложной статистики или специфичной для базы функциональности:

from django.db import connection
from myapp.models import Product

with connection.cursor() as cursor:
    cursor.execute("""
        SELECT category, COUNT(*) FROM products 
        WHERE created_at > NOW() - INTERVAL '7 days'
        GROUP BY category
        HAVING COUNT(*) > %s
    """, [10])
    stats = cursor.fetchall()

products = Product.objects.filter(is_active=True)  # Остальное можно сделать через ORM 

В таком подходе вы используете сильные стороны обоих решений: ORM даёт вам удобство и безопасность простых операций, а raw SQL обеспечивает гибкость сложных аналитических задач. Главное — не забывайте применять все ранее описанные правила параметризации и валидации данных и не смешивайте напрямую пользовательский ввод с текстом запроса.

Напоследок ещё одно важное напоминание: безопасность — это процесс, а не разовое мероприятие. Регулярно проверяйте код вашего приложения, проводите code review, следите за обновлениями Django и библиотек, а также используйте инструменты автоматического анализа и сканирования кода. Поверьте, лучше вовремя заметить потенциальную проблему, чем потом разгребать последствия атаки.

О том, как именно проверять и мониторить безопасность приложений, мы подробнее расскажем в следующей главе. А пока помните главное правило: ваш главный враг не злоумышленник, а ваша собственная невнимательность и самонадеянность. Поэтому будьте начеку, и да пребудет с вами безопасный код!

Тестирование безопасности и мониторинг угроз

Если после всех этих предостережений и правил у вас развилась лёгкая паранойя — поздравляю, вы на верном пути. В мире безопасности приложений небольшая паранойя — это не проблема, а скорее правильная рабочая настройка. Тем более когда речь заходит о SQL-инъекциях — одной из самых распространённых и опасных уязвимостей, которые, несмотря на все меры защиты, всё ещё регулярно встречаются даже в продвинутых проектах. Поэтому давайте перейдём от теории к практике и поговорим о том, как правильно тестировать и мониторить ваше Django-приложение, чтобы спать спокойно и не переживать о безопасности данных.

Начнём с тестирования. Самый очевидный и полезный подход — это регулярные ручные и автоматические проверки на наличие уязвимостей. Ручное тестирование, конечно, полезно, но куда эффективнее использовать автоматизированные инструменты, которые быстро и без жалости проверят ваш код на ошибки. Например, один из самых популярных инструментов — это OWASP ZAP (Zed Attack Proxy). Этот бесплатный сканер позволяет за несколько минут провести проверку на SQL-инъекции и другие типичные ошибки безопасности.

Запускается он просто: настраиваете ZAP в качестве прокси и просите его «погулять» по вашему приложению, нажимая кнопки и заполняя формы. ZAP самостоятельно попробует подставить в поля форм различные варианты вредоносных запросов и покажет вам, где конкретно вы забыли использовать параметризацию или допустили другие ошибки.

Другой инструмент, который полюбился многим разработчикам — sqlmap. Этот сканер немного агрессивнее, зато максимально быстро показывает, насколько надёжны ваши запросы. Использовать его можно примерно так:

python sqlmap -u "http://your-site.com/products?id=1" --batch --level=5 --risk=3

Sqlmap попытается взломать указанную вами страницу с максимальным уровнем риска и глубины проверки, и быстро покажет вам, где именно вы забыли про параметризацию. Только помните: использовать sqlmap нужно исключительно на ваших тестовых средах, иначе вы быстро станете главным злодеем собственной истории.

Но однократное тестирование — это ещё не всё. Важно встроить проверки безопасности в ваш CI/CD-процесс, чтобы они проходили регулярно и автоматически, перед каждым релизом. Настроить интеграцию с OWASP ZAP или другими инструментами в вашем пайплайне совсем не сложно, а результат того стоит. Каждый раз при изменении кода ваше приложение будет проходить быстрый скан на уязвимости, и вы будете спокойно выкатывать обновления, зная, что ничего страшного не случится.

Теперь о мониторинге. Даже при самом тщательном тестировании что-то может проскользнуть незамеченным, поэтому мониторинг угроз — ещё одна обязательная мера предосторожности. Самый простой способ — настроить логирование подозрительных действий и попыток SQL-инъекций. Например, в Django вы можете ловить и логировать подозрительный ввод, используя промежуточный слой (middleware):

import re
import logging

logger = logging.getLogger('security')

class SqlInjectionDetectionMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        self.pattern = re.compile(r"\b(UNION|SELECT|INSERT|DROP|UPDATE|DELETE)\b|--|'", re.IGNORECASE)

    def __call__(self, request):
        for source in (request.GET, request.POST):
            for key, value in source.items():
                if value is not None and self.pattern.search(str(value)):
                    logger.warning(
                        f"[SQL-INJECTION] Подозрение: {key}={value} от IP {request.META.get('REMOTE_ADDR')}"
                    )
        return self.get_response(request)

Таким образом, вы будете видеть все потенциально опасные запросы, которые приходят в ваше приложение. Логи безопасности можно регулярно просматривать или отправлять в системы мониторинга вроде ELK Stack, Sentry или даже напрямую в Slack для мгновенного реагирования.

Также полезно подключить системы мониторинга баз данных и приложений — Prometheus или Grafana, которые помогут быстро обнаружить аномальное поведение базы данных. Например, если вдруг резко возросло количество запросов или появилась странная активность, вы сразу получите уведомление и сможете оперативно принять меры. Настроить такие оповещения просто, а пользы от них невероятно много.

Паранойя в умеренных дозах — залог долгой и счастливой жизни вашего приложения. Лучше немного перестраховаться и быть готовым ко всему, чем однажды обнаружить, что ваш проект уже несколько дней тихо и незаметно взламывают.

В следующей и завершающей главе мы с вами рассмотрим ситуации, когда даже ORM бессилен, и без сырых SQL-запросов вам действительно не обойтись.

За гранью ORM

Настало время честно признать неприятную правду: иногда Django ORM смотрит на вас растерянным взглядом и молча сообщает — «Извини, дружище, дальше сам». Конечно, ORM прекрасен и удобен, решает большинство задач и делает жизнь программиста проще, но увы, рано или поздно каждый из нас сталкивается с ситуациями, когда он бессилен. И тут, тяжело вздохнув, приходится возвращаться к старому-доброму raw SQL. Но как это пережить без потерь?

Первый и самый распространённый сценарий, когда ORM «сдаётся» — это сложные аналитические запросы. Да-да, те самые отчёты для финансового директора или маркетологов, которые требуют сложных join’ов, оконных функций (window functions), common table expressions (CTE) и прочих SQL-извращений. Представим реальный пример: нам нужно построить отчёт о средних продажах за последние 30 дней, сравнить их с предыдущими месяцами и показать процент изменения по категориям товаров. Django ORM, конечно, может попытаться это выполнить, но код получится громоздким, медленным и почти нечитаемым.

Тут на помощь приходит сырой SQL с оконными функциями и CTE, который даже читать приятно (если вы достаточно искушены, конечно):

from django.db import connection

with connection.cursor() as cursor:
    cursor.execute("""
        WITH monthly_sales AS (
            SELECT
                category,
                DATE_TRUNC('month', sale_date) AS month,
                AVG(amount) AS avg_amount
            FROM sales
            GROUP BY category, month
        ),
        recent AS (
            SELECT category, avg_amount FROM monthly_sales
            WHERE month = DATE_TRUNC('month', CURRENT_DATE)
        ),
        previous AS (
            SELECT category, avg_amount FROM monthly_sales
            WHERE month = DATE_TRUNC('month', CURRENT_DATE - INTERVAL '1 month')
        )
        SELECT
            r.category,
            r.avg_amount AS current_month,
            p.avg_amount AS previous_month,
            ROUND(((r.avg_amount - p.avg_amount) / p.avg_amount) * 100, 2) AS percent_change
        FROM recent r
        JOIN previous p ON r.category = p.category;
    """)
    report = cursor.fetchall()

Согласитесь, такой подход намного проще и эффективнее. И ORM совершенно справедливо отступает в сторону, наблюдая, как вы с лёгкостью делаете его работу.

Другой пример — необходимость использования специфичных возможностей конкретной базы данных. Например, PostgreSQL — популярнейшая база данных среди Django-разработчиков, но далеко не все её возможности поддерживаются ORM. Возьмём ту же рекурсию, реализованную через CTE. Допустим, вы делаете дерево категорий или сложную иерархическую структуру сотрудников компании. С Django ORM вы получите либо чудовищные костыли с множеством запросов, либо один элегантный raw SQL-запрос:

with connection.cursor() as cursor:
    cursor.execute("""
        WITH RECURSIVE category_tree AS (
            SELECT id, name, parent_id
            FROM categories
            WHERE parent_id IS NULL
            UNION ALL
            SELECT c.id, c.name, c.parent_id
            FROM categories c
            INNER JOIN category_tree ct ON ct.id = c.parent_id
        )
        SELECT * FROM category_tree;
    """)
    categories = cursor.fetchall()

Красиво? Несомненно. Работает быстро и эффективно? Абсолютно.

Но тут важно не забывать правила безопасности, о которых мы говорили ранее. Никогда не используйте прямое форматирование строк при работе с сырым SQL и всегда используйте параметризованные запросы. Django прекрасно интегрирует такие запросы через connection.cursor() и позволяет легко смешивать raw SQL с привычными моделями и queryset’ами, сохраняя единый стиль работы.

Кстати, хорошей практикой будет инкапсулировать такие запросы в методы ваших моделей или менеджеров. Например, добавьте метод в модель или кастомный менеджер:

from django.db import models, connection

class ProductManager(models.Manager):
    def top_products_by_category(self, category, limit=10):
        with connection.cursor() as cursor:
            cursor.execute("""
                SELECT id, name, sales
                FROM products
                WHERE category = %s
                ORDER BY sales DESC
                LIMIT %s
            """, [category, limit])
            return cursor.fetchall()

class Product(models.Model):
    name = models.CharField(max_length=255)
    category = models.CharField(max_length=100)
    sales = models.IntegerField()

    objects = ProductManager()

Теперь в коде вашего приложения вместо прямого использования курсора будет приятный вызов:

top_products = Product.objects.top_products_by_category('Electronics', 5)

Выглядит красиво, безопасно и интуитивно понятно.

И напоследок о том, как пережить возвращение к сырому SQL психологически. Да, ORM прекрасен, он нас разбаловал простотой и удобством. Но если уж приходится использовать raw SQL, примите это с достоинством, напишите комментарии (да-да, те самые комментарии, которые вы всегда откладываете «на потом») и задокументируйте ваши сложные запросы. Это спасёт вас самих через несколько месяцев, когда вы будете ломать голову, вспоминая, «что же автор этого шедевра хотел сказать». А ещё помните, что использование сырого SQL не делает вас плохим разработчиком. Наоборот, это знак зрелости и профессионализма, если вы умеете грамотно и ответственно его применять.

В итоге ORM и raw SQL — это не конкуренты, а инструменты, которые нужно уметь правильно сочетать. Django ORM делает повседневные задачи простыми и быстрыми, а raw SQL позволяет решать сложные задачи элегантно и эффективно. Самое главное — соблюдать баланс, правила безопасности и помнить: хорошему разработчику не страшны никакие инструменты. Даже те, которые на первый взгляд кажутся страшными или неудобными.

На этом мы завершаем наше погружение в мир Django, raw SQL и безопасности. Удачи в разработке, не бойтесь сложностей и не забывайте о том, что безопасность вашего проекта — это не скучная рутина, а весёлая и полезная игра, в которой всегда можно победить, если подходить к ней осознанно и ответственно.


Читайте также:

ChatGPT
Eva
💫 Eva assistant

Выберите способ входа