SQL против ORM — один из самых горячих споров среди разработчиков. Одни уверены, что писать SQL-запросы вручную — это гарантия контроля и эффективности. Другие считают, что ORM упрощает жизнь и снижает вероятность ошибок. А что, если правда где-то посередине?
В этой статье я разберу плюсы и минусы обоих подходов, разберу реальные примеры уязвимостей, производительности и удобства сопровождения кода. Почему SQL-инъекции до сих пор остаются проблемой? Когда ORM становится тормозом, а когда — спасением? Правда ли, что ORM — это просто «модная игрушка», или же он стал стандартом разработки? Давайте разберемся, а заодно — подискутируем в комментариях.
Содержание
Проблемы с SQL-запросами
SQL-инъекции
Рассмотрим простой SQL-запрос:
def get_user(conn, user_id):
return conn.execute(
f"""
SELECT * FROM users WHERE id = {user_id}
"""
)
В чем проблема? Этот код уязвим для SQL-инъекции. Если user_id содержит вредоносный SQL-код, например:
1; DROP TABLE users;
То запрос превратится в:
SELECT * FROM users WHERE id = 1; DROP TABLE users;
В результате можно потерять всю таблицу!
Конечно, тут дело не в том, что мы используем SQL, а просто в ошибке разработчика, который неверно передал параметр в запрос.
Бывает что кто-то даже зная про инъекции, все равно совершал такую ошибку, просто случайно. Хотя это всегда должно присекаться на код ревью.
Решение – использовать плейсхолдеры:
def get_user(conn, user_id):
return conn.execute(
"SELECT * FROM users WHERE id = ?",
(user_id, )
)
Сложность формирования сложных SQL-запросов
Допустим, у нас есть фильтрация по нескольким параметрам:
def get_books(conn, name, author):
query = "SELECT * FROM books"
where = []
if name:
where.append(f"name = '{name}'")
if author:
where.append(f"author = '{author}'")
if where:
query += " WHERE " + " AND ".join(where)
return conn.execute(query)
Здесь сразу две потенциальные проблемы:
SQL-инъекции (если параметры переданы без экранирования);
Сложность формирования строки запроса.
Чем больше условий, тем сложнее читать и поддерживать код. Я бы предпочел использовать ORM или Query Builder.
Преимущества ORM
Безопасность
ORM автоматически подставляет параметры запросов, снижая риск SQL-инъекций:
def get_user(user_id):
return User.objects.filter(id=user_id).first()
Удобство написания запросов
Пример выборки книг с ORM:
def get_books(name=None, author=None):
query = Book.objects.all()
if name:
query = query.filter(name=name)
if author:
query = query.filter(author=author)
return query
Запрос читается проще, а ORM генерирует SQL-запросы в соответствии с API конкретной СУБД. Аналогично будет с Query Builder.
Автоматизация миграций
Django и SQLAlchemy поддерживают автоматическое создание миграций, упрощая управление схемой БД.
Low-code возможности
ORM позволяет быстро создавать админ-панели, например, Django Admin или Flask-Admin.
Ошибки при работе с ORM
Не всегда ORM-запросы оптимальны. Например:
users = User.objects.all()
for user in users:
print(user.profile)
Это вызовет Query N+1 проблему. Нужно использовать select_related:
users = User.objects.select_related("profile").all()
Чтобы эффективно использовать ORM, нужно хорошо понимать как оно работает. Нет смысла просто ругать ORM. ORM удобен, но иногда лучше вручную оптимизировать SQL-запросы, если ORM не поддерживает нужные конструкции.
Производительность
Тут важно понимать, что сравнивать производительность запроса в БД с ORM и SQL смысла нет. Все зависит от того, какой SQL в итоге генерируется, и на этом уровне все в порядке, если вы всегда проверяете, что генерирует ORM и умеете с ним работать.
У нас в проекте на 20млн+ пользователей ORM никогда не являлась узким местом.
Если говорить про накладные расходы CPU связанные с дополнительной логикой на создание итогового SQL запроса через ORM, но тут оверхед небольшой и даже с нашей нагрузкой он значения не имеет. Даже если бы у нас было сотни миллионов пользователей это экономия условно 1% CPU, что конечно тоже хорошо, но вряд ли оправдает полный отказ от ORM. Даже если запросы в приложении писали бы на SQL, то как минимум для миграций и админки все равно бы использовали ORM, а там производительность не критична. Опять же, какой запрос генериться в миграциях\запросах просто надо проверять всегда.
В работе с БД узкими местами как правило становятся не ORM, а запросы с агрегацией, неверные запросы\индексы, большое колличество индексов\FK и т.д. И тут подходы оптимизации не зависят от того на SQL вы пишите или на ORM.
Отказ от ORM для оптимизации актуально может быть только в специфичных случаях, когда мы строго ограничены в ресурсах.
Была у меня как-то задача оптимизировать ML приложение для работы на обычном ноутбуке, который поедет к заказчику на завод. Вот там нужно было экономить даже на спичках, так как была бизнес причина для этого и ограничение в ресурсах конфигурацией ноутбука. А если нет бизнес причины, то любая оптимизация\работа это оверинженеринг.
Но даже если, у вас есть специфичные причины думать о нескольких мегабайтах памяти и 1% CPU, это не означает, что ORM плохой выбор для других проектов. А в случае миграций и админки это вообще не актуально.
Если ORM не предоставляет возможность написать запрос так, как вам нужно, то конечно просто пишите на SQL. Часто люди просто ищут причины не изучать ORM.
Хорошее решение использовать Query Builder или ORM в большинстве случаев.
Скорость разработки
Использование SQL Builder или SQL, не исключает использование ORM для миграций и LOW-CODE админки. Если конечно в вашем стеке существует ORM с админкой и вы знаете ORM или готовы изучать. Если такая ORM существует, то не использовать ее, это потеря скорости разработки.
У меня как-то был очень активный спор с командой на эту тему. Не хотели внедрять ORM, так как просто никто кроме меня в команде с ними не работал или очень мало и просто не хотели изучать.
Нужна была админка и команда на полном серьезе хотела писать ее самостоятельно и выделить целый квартал времени для фронта и бэка.
В итоге я за выходные ее сделал всю на LOW-CODE решении с ORM. Показал команде и с тех, никто на эту тему не спорит, надо изучать ORM или нет. В запросах в приложении ORM не использовали.
В статье не идет речь о том, что надо всегда и везде использовать ORM. В целом, нельзя даже объективно рассуждать, что лучше, если вы не владеете достаточно хорошо, хотя бы одной ORM, а также SQL и внутренним устройством БД.
Когда стоит использовать чистый SQL?
Иногда SQL предпочтительнее:
Для сложных запросов, которые ORM не поддерживает или это сервис без миграций и админки, а предназначен только для выполнения больших сложных запросов например, для формирования отчетов.
Если ORM генерирует неэффективные запросы и не поддерживает нужные SQL конструкции.
Когда нужно работать с нестандартными SQL-конструкциями.
Комбинирование ORM и SQL
Чистая архитектура
Хорошая практика – использовать слой репозитория, где SQL и ORM-методы сосуществуют.
Возвращать из методов следует DTO-объекты, а не ORM-модели или словари, тогда код будет чистым.
Использование ORM для миграций и админки
Обычно даже в проектах с SQL используют ORM как минимум для миграций и админки.
Как проверять, какие SQL-запросы генерирует ORM?
Django ORM:
В Django можно увидеть, какой SQL-запрос будет выполнен, вызвав метод query:
print(str(Book.objects.filter(name='Django').query))
Также можно включить логирование всех SQL-запросов для локальной разработки.
Если речь о миграциях, можно посмотреть, какой SQL сгенерировала Django ORM:
./manage.py sqlmigrate app_name 0005
Это покажет SQL-код миграции и поможет убедиться, что ORM корректно изменяет схему базы.
SQLAlchemy ORM:
В SQLAlchemy можно посмотреть сгенерированный SQL так:
Чтобы включить логирование всех запросов в SQLAlchemy, можно при создании engine передать флаг echo=True:
from sqlalchemy import create_engine
engine = create_engine("sqlite:///example.db", echo=True)
Тренды: уходит ли ORM в прошлое?
Некоторые разработчики считают, что ORM теряет популярность, но на самом деле он продолжает широко использоваться. ORM предлагает автоматическое управление схемой БД, генерацию миграций и интеграцию с административными панелями.
Часто полный отказ от ORM происходит из-за незнания его возможностей и особенностей. Опытные разработчики, хорошо разбираются в ORM, но знают и сам SQL хорошо.
Выводы
ORM облегчает работу с базой, делая код более читаемым и безопасным и предоставляет LOW-CODE решение для админ-панели.
Чистый SQL нужен в сложных запросах и при оптимизации производительности так где ORM не поддерживает нужные конструкции.
Важно следить за тем, какие SQL-запросы генерирует ORM, используя инструменты отладки и логирования.
Оптимальный вариант — совмещать оба подхода: использовать ORM для удобства и LOW-CODE админ-панели, а SQL там, где ORM неэффективен.
Если сомневаетесь, выбирайте ORM – он безопаснее и удобнее.
Хороший разработчик должен знать и SQL, и ORM. Нельзя даже объяективно рассуждать, что лучше, если вы хорошо знаете только что-то одно.
Стоит ли использовать ORM или писать SQL-запросы вручную? Ответ зависит от контекста. В большинстве случаев ORM ускоряет разработку, уменьшает вероятность ошибок и улучшает читаемость кода. Однако при высоких требованиях к производительности и сложных запросах SQL остается незаменимым.
Лучший вариант — комбинировать оба подхода: использовать ORM для удобства и SQL там, где это необходимо. Главное — изучить оба инструмента и выбирать их осознанно!
Вопросы для обсуждения:
В каких случаях вы предпочли бы SQL вместо ORM?
Как вы решаете проблемы производительности ORM?
Были ли у вас случаи, когда проект переписывали с ORM на SQL (или наоборот)?
Пишите в комментариях – обсудим!