Как стать автором
Обновить

Комментарии 72

Преимущества ORM

Пока разработчики будут использовать ORM - DBA и инженеры по оптимизации производительности СУБД - без работы не останутся !

Для кардинального решения проблемы инъекций , ORM вообще говоря не единственное решение.

DBA и инженеры по оптимизации

Я когда-то был человеком, который ревьюит все запросы и миграции в проекте. Миграции там писались на SQL и могу сказать, что проблема не в использовании ORM. С ORM просто добавляется еще фактор, что человек мог не проверить какой запрос она генерирует.

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

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

На мой взгляд при использовании ORM работа удваивается.
Мало того, что разработчики могут написать SQL запросы неоптимальные или опасные, так с добавлением ORM теперь еще и ORM начинает спамить базу невероятным трафиком и также неоптимальными запросами.

Мало того, что разработчики могут написать SQL запросы неоптимальные или опасные

Разработчики делают это и без ORM.

с добавлением ORM теперь еще и ORM начинает спамить базу невероятным трафиком и также неоптимальными запросами.

Если только люди не понимают как работает ORM и даже не проверяют какой запрос сгенериться в их коде. Но чаще всего проблема в непонимании внутреннего устройства БД. ORM используют разработчики и это их ответственность, понимать как это работает. Когда я вижу, что запрос будет неэффективен причина не в том, что ORM его не правильно генерирует, а в том, что разработчик не понимает, что это неэффективно. Т. е. он знает какой будет запрос, какой он индекс создал, и просто не понимает что не так. Писал бы он на SQL, сделал бы тоже самое.

Можно ли винить ORM в том, что разработчик не понимает как она работает или не понимает впринципе как писать эффективные запросы и создавать эффективные индексы?

Когда эти знания есть, то нет проблем и не важно ORM разработчик использует или нет.

Как-то не очень раскрыта у вас тема: SQL vs ORM.
Только тестируемость и безопасность затронули. А где сравнение производительности? Ведь это ж один из основных факторов при выборе. Можно также и скорость разработки сравнить и кросс-СУБД совместимость. А то информации хотите много собрать, а взамен даёте, на мой взгляд, недостаточно.

Спасибо за комментарий) По поводу производительности согласен стоит добавить не много. Но не соглашусь, что это основной фактор выбора.

Тут важно понимать что сравнивать производительность запроса в БД с ORM и SQL смысла нет. Все зависит от того, какой SQL в итоге генерируется и на этом уровне все в порядке, если вы всегда проверяете, что генерирует ORM и умеете с ним работать.

У нас в проекте на 20млн+ пользователей ORM никогда не являлась узким местом.

Если говорить про накладные расходы CPU связанные с дополнительной логикой на создание итогового SQL запроса через ORM, но тут оверхед небольшой и даже с нашей нагрузкой он значения не имеет. Даже если бы у нас было сотни миллионов пользователей это экономия условно 1% CPU, что конечно тоже хорошо, но вряд ли оправдает полный отказ от ORM. Даже если запросы в приложении писали бы на SQL, то как минимум для миграций и админки все равно бы использовали ORM, а там производительность не критична. Опять же, какой запрос генериться в миграциях\запросах просто надо проверять всегда.

В работе с БД узкими местами как правило становятся не ORM, а запросы с агрегацией, неверные запросы\индексы, большое колличество индексов\FK и т.д. И тут подходы оптимизации не зависят от того на SQL вы пишите или на ORM.

Если ORM не предоставляет возможность написать запрос так, как вам нужно, то конечно просто пишите на SQL. Часто люди просто ищут причины не изучать ORM.

Но не соглашусь, что это основной фактор выбора.

Я не сказал что он основной, но это один из основных факторов выбора, особенно когда речь идёт о BigData. Посмотрите на описания ORM на сайтах разработчиков или, например, на GitHub — в первых абзацах вы увидите бенчмарки и таблицы сравнения производительности. Как вы думаете, почему они там?

Тут важно понимать что сравнивать производительность запроса в БД с ORM и SQL смысла нет.

Сравнивать скрипты в БД действительно нет смысла, но не забывайте, что ORM - это как правило, инструмент маппинга данных внутри приложения и он неизбежно вносит свои накладные расходы. Это снижает скорость работы кода в обмен на удобство и простоту. Поэтому сравнивать нужно именно производительность внутри приложения.

По опыту могу сказать - обычно начинаешь проект с выбора одного из ORM, но как только решение достигает определенного уровня зрелости, в процессе очередного рефакторинга от этого дополнительного слоя часто приходится отказываться. Иногда по причине недостаточной гибкости, или же к примеру, когда надо ускорить ETL-процесс, чтобы миграция укладывалась в часовой период перерыва персонала и начинаешь бороться за каждый процент прироста производительности.

Посмотрите на описания ORM на сайтах разработчиков или, например, на GitHub — в первых абзацах вы увидите бенчмарки и таблицы сравнения производительности. Как вы думаете, почему они там?

Ответ на этот вопрос очень простой - потому , что маркетинг и PR.

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

В реальности это просто цифры. Каждый кто принимает стратегические решения на основании неких цифр в неких статьях в интернете - должен понимать и принять - попал пальцем в небо . Это его личный выбор, и часто вполне оправданный - топы и пиджаки очень любят красивые графики и цифры . И их вообще не волнует, откуда цифры взялись и в общем то , что означают .

Вы не поняли посыла моего сообщения. Дело не в корректности цифр в принципе, дело в их наличии. Мы говорили о важности производительности как фактора выбора. То что цифры публикуют - говорит о том что разработчики и пользователи считают этот фактор достаточно важным. Вот и все.

Это снижает скорость работы кода в обмен на удобство и простоту.

Неверно. Он незначительно снижает скорость работы на удобство, легкость чтения и безопастность. Банально код и запросы не разъедуться если изменить что-то одно (но без миграции разъедиться с базой и это да надо чекать))

Мда... Проблемы с SQL-запросами vs Преимущества ORM. Вы просто гений сравнения...

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

Всё, что "одни уверены, а другие считают" - это в общем вторично. Основной спор - это "логика в приложении" vs "логика в СУБД". А вы рассматриваете (немножко) лишь первый вариант. Посему основная направленность в сторону ORM совершенно неудивительна.

SQL-инъекции

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

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

А если работает команда, то это ошибка лида. Который либо не привлёк в команду SQL-программиста вообще, либо не проследил за тем, что у него люди занимаются чужими делами и манкируют своими.

Сложность формирования SQL-запросов

Допустим, у нас есть фильтрация по нескольким параметрам

Сложность тут от того, что SQL-код формируется через "универсальный интерфейс". А ещё в том, что кто-то поленился написать универсальную хранимую процедуру фильтрации.

а ORM сам оптимизирует его под конкретную БД

Да ничего ORM не оптимизирует! как сляпалось, так и ладно, сервер колом не встал - уже хорошо.

А если вдруг где-то и найдётся пример того, что оно-таки какой-то запрос оптимизирует, то удивления будет столько, что весь эффект пропадёт.

Основной спор - это "логика в приложении" vs "логика в СУБД". А вы рассматриваете (немножко) лишь первый вариант. Посему основная направленность в сторону ORM совершенно неудивительна.

логика в СУБД это не много другое (функции, процедуры и т.д.). Она может быть и при использовании ORM в проекте и без. Да, в статье все про логику в приложении.

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

Бывает что кто-то даже зная про инъекции, все равно совершал такую ошибку просто случайно. Хотя это всегда должно присекаться на код ревью. Ну и конечно навички часто ошибаются тут тоже мало что можно поделать. Научатся, я не был бы так котегоричен :)

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

А вот это уже про логику в СУБД, о чем я возможно напишу отдельную статью)

Да ничего ORM не оптимизирует! как сляпалось, так и ладно, сервер колом не встал - уже хорошо.

Спасибо поменяю формулировку. Хотел сказать, что ORM генерирует SQL-запросы в соответствии с API конкретной СУБД. Оптимизация производительности всегда лежит на плечах разработчика и его понимании, что он делает.

о чем я возможно напишу отдельную статью

Ооо... вот тут я вам категорически сочувствую. Весьма трудная тема.

логика в СУБД

простите но уже лет 20 как все стартующие проекты (из моего опыта, хорошо) используют бд как хранилище данных а не как на заре: в качестве бэка к приложению. Единственное что использует такой подход - это андройд и его файербэйз кажись.

SQL-инъекции

Решениe 1. Включить авторизацию на уровне СУБД, настроить доступ, в т.ч. RLS - тогда пользователь будет делать только то, что позволено. Проблема пока ещё в отсутствии нормальной методологии (культуры) формирования структуры ролей и прав доступа, непроизводительная реализация RLS, но постепенно к разработчикам СУБД понимание приходит.

Решение 2. Параметризованные запросы - зло с точки зрения дизайна sql. SQL имеет на выходе набор(ы) данных. На вход ему также удобно подавать набор данных, к которому обращаться как к таблице, без всяких многочисленных вставок параметров в разных местах запроса. Такого интерфейса СУБД пока не имеют, поэтому как универсальное решение, упаковывать параметры и значения в json далее распаривать их в CTE как табличку из json с одной записью и использовать в разных местах. Вредоносные sql инъекции съедятся на этапе упаковки в json, конечно при условии корректной (один раз) отработки передачи json строки в качестве параметра.

Сложность формирования сложных SQL-запросов

Если параметры раскидывать по SQL, конечно, такой код не читабельный. Все параметры удобно описать вначале в СТЕ (WITH...) затем использовать в запросе, на скорость выполнения запроса критически не повлияет

WITH params AS (
  SELECT 
      COALESCE(:is_a, false) as is_a
      , COALESCE(:a, 0) as a
      
      , COALESCE(:is_b, false) as is_b
      , COALESCE(:b, 0) as b
)

SELECT

...

WHERE
  (SELECT p.is_a FROM params p)
      AND (SELECT p.a FROM params p)=111
  OR
  (SELECT p.is_b FROM params p)
      AND (SELECT p.b FROM params p)=222

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

LOW-CODE

Как-то аналитик быстрее понимает SQL, чем ORM. Поэтому LOW-CODE + SQL, а ORM пусть покурит в сторонке.

Спасибо за комментарий)

Решениe 1. Включить авторизацию на уровне СУБД

Так мы ограничим изменения схемы БД и возможно что-то еще, но проблему это не решает.

Решение 2. упаковывать параметры и значения в json далее распаривать их в CTE

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

Я бы скорее использовал что-то вроде Query Builder, котороый будет в SQL вставлять нужные параметры (маппить по определенному синтаксису). Так мы будет иметь и читаемый SQL (без CTE и подзапросов с OR) и не будет проблем с инъекциями, производительностью.

Пример ниже. Обратите внимате тут нет f-string это аля синтаксис который понимает метод build_query.

def get_books(conn, name, author):
    query = "SELECT * FROM books"
    params = []
    where = []

    if name:
        where.append("name = {name}")
        params.append(name)

    if author:
        where.append("author = {author}")
        params.append(author)

    if where:
        query += " WHERE " + " AND ".join(where)

    query, params = build_query(query, params)
    return conn.execute(query, *params)

Как-то аналитик быстрее понимает SQL, чем ORM.

Всегда можно посмотреть, что генерирует ORM.

Так мы ограничим изменения схемы БД

Это ещё почему? Кто вообще установил такое правило, что миграции при выкатке новой версии используют тот же логин, что и приложение? В alembic, например, своя независимая настройка.

PS: https://habr.com/en/articles/851472/

Конечно используют разный логин. Это мой ответ на предложение выше "Включить авторизацию на уровне СУБД" как решение проблемы SQL-инъекций. Это само собой всегда должно быть.

Ну так логин приложения может не иметь полномочий для выполнения любых DDL. А в некоторых СУБД, вроде бы, даже можно запретить выполнение DML по определенным шаблонам (вроде DELETE FROM без фильтра, правда, обходится WHERE id IN (SELECT id FROM))

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

но проблему инъекций это не решает.

А какие ещё проблемы бывают? Пользователь зашёл под своими правами, делает в любом случае что хочет в рамках выданных прав, хоть БД удаляет, если ему такие права даны, как с инъекциями, так и без. Или чего-то ещё?

читаемый SQL (без CTE...

CTE - это и есть организация структуры запроса. На производительность не влияет, производительность сильнее просаживается, как только данные с носителя начинают читаться. Дополнительно оптимизатор корректно запрос оптимизирует, видя статические данные из СТЕ. Совокупность OR и AND конечно накладные расходы, но выглядит не хуже if или case, когда запрос по двум языкам размазан и для его тестирования в консоле нужно очистить от всех этих накладок

Всегда можно посмотреть, что генерирует ORM

Я несколько о другом, я про LOW-CODE + SQL в котором аналитик сам прикладное решение создаёт, без программиста. Правда полноценных таких решений пока нет. Но мы же обсуждаем противостояния подходов, следовательно косвенно влияем на приоритеты для выбора разработчиков будущих решений.

Решениe 1. Включить авторизацию на уровне СУБД, настроить доступ

А как это вообще должно работать?

Вот у нас есть 100 000 пользователей, допустим мы для каждого пользователя системы завели пользователя в БД (хотя некоторые трудности есть уже на этом этапе!). Вот от имени пользователя пришёл HTTP запрос, как выполнить соответствующий ему SQL запрос от имени этого пользователя?

Ладно если пользователь заходил в приложение указав логин и пароль, тут можно просто передать этот пароль СУБД. Хотя это уже небезопасно, поскольку вынуждает хранить пароль открытым текстом в течении всего сеанса пользователя. Но что делать если был использован какой-то другой способ входа?

Да и та же RLS - недоделанная фигня. Фактически, из всех атрибутов пользователя для проверок доступно только его имя, это мало!

Я думаю человек имел ввиду авторизацию под разными пользователями для приложения и для миграций схемы БД. Чтобы приложение не могло менять схему БД. Это и так само собой всегда должно быть. Если он имел ввиду другое, то так конечно никто не делает. Это сложно и избыточно. Возможно только в каких-то специфичных случаях, но я такого не встречал. Обычно пользователя в БД создают для разграничения прав для приложения или его частей, миграций и т. д.

Ну, в таком режиме RLS не сможет опереться даже на имя пользователя...

...тут можно просто передать этот пароль СУБД. Хотя это уже небезопасно

Передача пароля - совсем уже примитив по сегодняшним технологиям. Рассмотрите как работает Kerberos или OpenID. Это не простые решения по администрированию, но важные подходы для любых систем.

...для проверок доступно только его имя

Никогда, слышите, никогда не привязывайте правила RLS к пользователю напрямую (исключение только для его личных, не относящихся к профессиональной деятельности сообщений).

Скрытый текст

Надеюсь, когда нибудь появится время, влезу в исходный код ядра популярной ОС, чтобы изьять все права на файлы для Owner, обесценивающие все труды по настройке прав по группам пользователей.

Правила RLS привязываются к ролям (группам) пользователей. Все действия пользователь должен совершать в рамках своих ролей в системе. Если это система управления предприятием, то роли совпадают со штатным расписанием предприятия - работники приходят и уходят, а правила доступа остаются неизменными (если не меняются полномочия в самом штатном расписании).

Сопоставление и настройка правил доступа между ролями пользователей и объектами данных - это всегда трудный творческий процесс, независимо от уровня реализации - на уровне интерфейса, сервера приложений или сервера СУБД.

Рассмотрите как работает Kerberos или OpenID

А давно OpenID поддерживается базами данных?

Правила RLS привязываются к ролям (группам) пользователей.

Не вполне понятно как это должно работать.

А давно OpenID поддерживается базами данных?

Уже парочка популярных СУБД есть, но в них нет RLS :-). А вот у Postgres есть PAM, есть статьи, как все связать вместе. Есть ещё неожиданный пример - в платформе 1с реализовали и RLS, и OpenID. Так что направление и тут развивается, был бы спрос.

Не вполне понятно как это должно работать.

Непонятно какую пользователю присвоить роль, а ролям назначит политику rls? Это творческий процесс, зависит от самой задачи.

Логика в БД, идентификация на сервере и настройки прав доступа по ролям в БД. Это то что я использую в разработке на чистом SQL. В качестве клиента есть и nocode (LabVIEW) и Delphi последней версии. Системы сложные. Однa из них описана в моей статье в хабр профиле. Даже мысли нe было использовать костыли вместо нативного SQL. Ну и оптимизация sql по скорости это наше все. Процедуры и функции sql более эффективны и упрощают разработку сложных приложений. К сожалению это понимает не каждый. У меня количество пользователей не миллионы, но задачи компенсируются значительной сложностью бизнес процессов. Поэтому логика в БД позволяет не только разрабатывать , но и относительно просто сопровождать и дорабатывать приложения при необходимости не ломая главного- бизнес модели.

У вас SQL упрощает разработку, потому что Delphi это относительно низкоуровневый язык со статической типизацией, и там ORM сложно сделать и использовать, и вообще похоже у вас двухзвенка. А с трехзвенкой есть сервер, где можно использовать язык с динамической типизацией и классами, аналогичный PL SQL, только без его недостатков. И вот там использовать ORM очень удобно.

Ну а смысл этой трехзавенки тогда, если она усложняет разработку? Я сторонник минимализма , если задача позволяет. Зачем делать сложно когда можно сделать проще, быстрее и меньшим составом разработчиков? У меня команда это два человека. А задачи непростые. Это как говорят не "джесоны перекладывать". Delphi меня вполне устраивает, и все приятные мелочи, которые можно одной строчкой реализовать, к примеру переключится автоматом на русский язык если курсор в поле ввода. Попробуйте это сделать на любом другом языке кроме С. Мода на web интерфейс только усложняет и удлиняет разработку и ничего не дает в общем. Delphi ceйчас в общем это не совсем тот Delphi, который был 20 лет назад, Теперь можно писать под все платформы. Появились и динамические массивы к примеру, чего в старых вариантах Delphi не было. Драйверы под любую БД есть. Даже для sqlite имеется драйвер.

Ну а смысл этой трехзавенки тогда, если она усложняет разработку?

Она не усложняет разработку. Это то же самое, что вы делаете на процедурах SQL, только процедуры написаны на более удобном языке. Один из плюсов процедур с приложением на Delphi - не надо перекомпилировать все клиенты при изменении логики в процедуре. Вот сервер в трехзвенке нужен именно для этого. Ну и еще для того, чтобы не давать миллиону пользователей прямой доступ к базе.

Зачем делать сложно когда можно сделать проще, быстрее и меньшим составом разработчиков?

Потому что это с ORM приложение делается проще, быстрее и меньшим составом разработчиков. Для того их и придумали. Если вам проще писать логику на SQL, а не на Delphi, то это зависит от низкоуровневости Delphi, а не от ORM. Я писал на Delphi, именно двухзвенку с логикой в процедурах. На PHP писать гораздо удобнее.

У меня вот есть статья с примером бизнес-требований, там 6 действий, на PHP с ORM их можно сделать часа за 2. Можете попробовать написать и проверить, сколько времени и строк кода это займет на Delphi с SQL процедурами.

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

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

и все приятные мелочи, которые можно одной строчкой реализовать

Ага, только почему-то вы пишете логику в процедурах, а не в Delphi. Видимо для не-мелочей нужно гораздо больше строк. О том и речь.

Для меня SQL более удобный язык чем язык клиента для бизнес логики. В БД свобода, а на клиенте вы вынуждены применять всякие костыли в виде сложения строк запросов, проблемы с отладкой, интерфейсные зависимости переплетаются с логикой бизнес процессов,нет возможности полноценно отлаживать запросы, их оптимизировать по скорости, вся эта лапша с интерфейсом и бизнес логикой и т д и тп. Те кто использует орм все равно вынуждены проверять как работает запрос напрямую на сервере sql, так как нет гарантии что орм правильно работает, особенно в сложных приложениях если есть проблемы. Правильная архитектура приложения, разбиение бизнес процессов на операции, логирование операций, дает возможность избавится от обектно ориентированной логики на клиенте и писать в обычном функциональном стиле с обработкой интерфейсных событий через встроенные процедуры. Другое дело если вы не умеете так строить приложения в БД . в такой двухзвенной архитектуре я еще двадцать пять лет назад написал свою erp c систему в которой были сервисы CRM, заказы, снабжение и комплектация заказов с приоритетами, отгрузка заказов, склад, формирование документов, экспорт данных в 1С бухгалтерию, хранилище файлов в БД по типу файловой системы, поддержка документооборота в хранилище. Система прожила 10 лет у заказчика, пока я ее поддерживал. И это все прекрасно работало на Windows ХР и среднем сервере в то время .

Я еще не написал про легкую поддержку транзакций в хранимых процедурах. Я использую не только хранимыее процедуры, но и функции в sql, последовательности, сте в хранимках, сложную фильтрацию через входные параметры. Специальный триггер в БД следит за dml кодом хранимов и регистрирует все операции dml, вся история dml и код хранимок пишутся в отдельную таблицу специальной БД. Любая компиляция процедуры или функции заносит новую версию в таблицу. Ее можно посмотреть, и откатить при необходимости. Такой своеобразный git. Ну и использовать sql ин'eкции в такой архитектуре практически невозможно. Я уж не говорю про динамический sql.

Да, тоже писал что-то похожее: суем в запрос жсон, тот разворачивает, делает несколько запросов, всякими кейсами проверяет значения на соответствие констрейнтам (пришлось повторить констрейнты, благо это не внешние ключи были), далее те, что проходят, идут в апсерт, затем собираем обратно те, что не прошли (и уронили бы весь запрос) с указанием, где плохие данные в конкретной строке входных параметров, и те, что прошли через апсерт (дергаем RETURNS по необходимости), выплевываем готовый жсон, который фронты уже умеют отображать. Офигенно работало!

в виде сложения строк запросов

Зачем? Не нужно там ничего складывать, даже без ORM.

$db->execute('INSERT INTO article VALUES (:title, :text)', ['title' => $article->title, 'text' => $article->text]);

Для фильтров можно складывать, если не хочется добавлять лишние условия когда фильтр для этого поля не задан во входных данных, но можно делать и универсальные условия с проверкой на NULL, как вы в процедурах обычно делаете.
Но обычно используют Query Builder.

$query = Article::find();
if ($input->title !== null) $query->andWhere(['like', 'title', $input->title]);
if ($input->text !== null) $query->andWhere(['like', 'text', $input->text]);

проблемы с отладкой

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

интерфейсные зависимости переплетаются с логикой бизнес процессов

А зачем вы так пишете? Можно не переплетать, тогда и не будет переплетаться.

нет возможности полноценно отлаживать запросы

Когда из приложения идут только простые SELECT и INSERT, их и не надо отлаживать.

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

Это, извините, ерунда какая-то. Кто-то может и проверяет, но необходимости в этом нет. Вы же сетевую карту не проверяете, которая вас к базе подключает, а то вдруг она меняет ваш запрос.

дает возможность писать в обычном функциональном стиле с обработкой интерфейсных событий через встроенные процедуры

Ну ок, а зачем?

Другое дело если вы не умеете так строить приложения в БД . в такой двухзвенной архитектуре

Так логически у вас есть то же самое третье звено, только встроенное в БД. Разница только в отсутствии сетевого взаимодействия. Вопрос только в том, зачем его туда встраивать.

написал свою erp систему

Ну так я же не сказал, что написать нельзя. Можно, только дольше и сложнее.
Я же вам привел пример с кодом и бизнес-требованиями, напишите свой код, тогда и сравним, как вы умеете строить приложения. Можете сделать только 2 действия "Сохранение товара" и "Отправка на ревью".

Такой своеобразный git

Вот вообще не вижу никаких преимуществ в том, что надо писать свой git.

Я еще не написал про легкую поддержку транзакций в хранимых процедурах.

Ага, вот тут мне человек рассказывал про логику в базе. Оказалось, что в его процедуре без транзакций есть race condition в 2 местах.
А явный старт транзакции в базе и в приложении ничем не отличается.

Ну и использовать sql ин'eкции в такой архитектуре практически невозможно.

Да прекрасно можно, если не закрыть все таблицы вьюшками или за правами на строки не уследить. Это от архитектуры не зависит.
Там даже инъекция не нужна, пользователь может подключиться через любой клиент со своим паролем и смотреть всё что ему доступно.

В Delphi крайне легко сделать ОРМ и их существует уже давно несколько готовых. Существуют и множество бэкенд фреймворков с/без ОРМ. На любой вкус.

Язык Delphi позволяет писать низкоуровневый код, но сам язык не является таковым. Нет никаких ограничений писать на нем так же как на языке с динамической типизацией. И даже вручную постоянно следить за объектами не обязательно.

Пример роута сервера:

type
  TMyObject = class
  private
    FField2: Integer;
    FField3: TArray<integer>;
    FField1: string;
  public
    property Field1: string read FField1 write FField1;
    property Field2: Integer read FField2 write FField2;
    property Field3: TArray<integer> read FField3 write FField3;
  end;

  [Path('test')]
  TTestRoute = class
  public
    [GET, Produces(TMediaType.APPLICATION_JSON)]
    function Get: TMyObject;
  end;

implementation

uses
  MARS.Core.Registry;

{ TTestRoute }

function TTestRoute.Get: TMyObject;
begin
  Result := TMyObject.Create;
  Result.Field1 := 'Hello';
  Result.Field2 := 123;
  Result.Field3 := [1, 4, 5, 7, 9, 0];
end;

initialization
  TMARSResourceRegistry.Instance.RegisterResources([TTestRoute]);

Создаем нужные классы, если нужно, создаем модуль, создаем класс роута, регистрируем роут. Всё это делается независимо от самого сервера (не надо внедрять глубоко нигде в сам сервер). Создали такой модуль, зарегистрировали роут и готово. Обращаемся к серверу localhost/test и получаем JSON объекта TMyObject. На месте типа функции TMyObject может быть как любой совершенной класс, так и примитивный тип, структура (запись) или даже датасет, который будет преобразован в JSON данные или, по желанию, сериализован в чистые данные (стрим) для приема в сыром виде. Названия полей в объекте я могу задать и свои, через атрибуты. Через них же настроить другие действия с полем.

Если в аргументы функции добавить нужные параметры (входные данные из Query/Header/Body и т.д.) он сам их преобразует, даже если это опять целый объект. И следить за памятью мне тут вообще не нужно.

Так же, если результат функции это датасет, то я просто в атрибутах могу задать SQL запрос и не писать реализацию метода.

type
  [Connection('MAIN_DB'), 
   Path('fdresource'), 
   SQLStatement('employee', 'select * from EMPLOYEE order by EMP_NO')]
  THelloWorldResource = class(TMARSFDDatasetResource)
  end;

Вот так создается полноценный CRUD к таблице.

Стоит ли тут говорить, что это на-порядок быстрее работает, чем ОРМ?

Стоит ли тут говорить, что это на-порядок быстрее работает, чем ОРМ?

А за счет чего? Не должно такой разницы быть. Для того же C# тесты на выборку показывают минимальную разницу.

Для того же C# тесты на выборку показывают минимальную разницу.

да там к тесту 4 много вопросов - у EF можно так же не делая выборку шлепнуть запрос просто на апдэйт. Так что 4й мимо
и Specifically, EF retrieves records using TOP 2 instead of TOP 1, which introduces unnecessary overhead in this scenario
Чел походу не понял что значит Single и написал даппер запрос как First. А потом будет в чатиках писать что он на SQL божит и SQL работает быстрее. (А Singe - это именно единственное вхождение в базе. поэтому оно и отрабатывает дольше потому что надо пройтись по всей таблице и убедиться в единственности. с TOP 1 это никогда не получится потому что найдешь только первое вхождение.)

но остальные тесты показательны: генерация запроса и маппинг практически не влияют на скорость получения результата.

Пример роута сервера

Выглядит неплохо, согласен.

Так же, если результат функции это датасет, то я просто в атрибутах могу задать SQL запрос и не писать реализацию метода.
SQLStatement('employee', 'select * from EMPLOYEE order by EMP_NO')]

Ага, а теперь бизнес хочет фильтр на 10 полей. Как вы будете его формировать и помещать в эту строку?

Вот так создается полноценный CRUD к таблице.

Так ORM нужен не для CRUD, а для более сложной логики. Простые CRUD-ы нужны разве что в админках.
Допустим, для Update нам надо сделать валидацию 10 полей, вернуть ошибки если есть, получить сущность, проверить ее наличие и возможность операции, если нет или нельзя, показать понятные сообщения пользователю, и только потом обновлять данные.
Как это будет выглядеть с SQLStatement?

Стоит ли тут говорить, что это на-порядок быстрее работает, чем ОРМ?

Стоит ли тут говорить, что это на-порядок медленее разрабатывается, чем с ОРМ?
Оно не может работать в 10 раз быстрее ORM, потому что сетевой запрос в базу занимает гораздо больше времени, чем обработка в ORM.

С прямым CRUD да, кастомную фильтрацию не добавишь, но собственно этот механизм и не для этого.

Ниже, в комментариях я показал пример SQL билдера, где формируется запрос (его, кс лову можно и кешировать, изменяя лишь параметры). Он и от ошибок убережет при написании запроса и подскажет где нужно править, если изменилась модель БД. Все переменные становятся параметрами и тоже проверяются на соответствие типов.

И тут уже скорость разработки вполне сравнима с ОРМ, но нет дублирования данных, если не нужно. Т.е. не обязательно сериализовать данные из БД в объекты, а потом в ответ сервера, достаточно вернуть целиком датасет, которые сразу сериализуется в JSON. Здесь скорость появляется именно из-за уменьшения кол-ва выделяемой памяти. Да и датасет может не все данные из запроса забирать, а забирать итеративно по мере запроса данных при сериализации, что ещё уменьшит потребление памяти (но, время ответа может увеличиться).

Другими словами этот механизм более гибкий. Объекты я тоже могу получать как в ОРМ. Сам запрос легко и без лишних конструкций может быть сформирован особым образов в зависимости от запроса клиента.

На самом деле, я могу и билдер указать как атрибут такого CRUD. Где в зависимости от входных данных будет особым образом формироваться запрос. Т.е. описать класс билдера, который будет читать входные параметры запроса клиента. Но сейчас такого нет, можно будет дописать, но нужды в таком пока не было.

Т.е. не обязательно сериализовать данные из БД в объекты, а потом в ответ сервера

Так ORM нужна не для этого. Она нужна для работы с объектами, чтобы указывать типы в аргументах функций, которые с ними работают, и далее для сохранения объектов в базу. Это нужно в первую очередь для изменяющих операций.

def get_user(conn, user_id):
    return conn.execute(
        f"""
        SELECT * FROM users WHERE id = {user_id}
        """
    )

Этот пример с потенциальной проблемой безопасности решается условно так:

def get_user(conn, user_id):
    return conn.execute(
        "SELECT * FROM users WHERE id = :user_id",
        user_id
    )

Вот и все, инъекции не будет не при каких условиях. Код сложнее? Нет. Менее удобен? Нет. Быстрее отработает, чем пока ОРМ сгенерирует не пойми что? Да.

Реальный пример на Delphi

class function TDBUsers.GetUserByKeyId(Context: TRequestContext; const KeyId: string; Transaction: TFDTransaction): TCloudUser;
begin
  Result := Context.DB.QueryOne<TCloudUser>('''
    SELECT id, key_id, date_to,
        ARRAY(SELECT ur.role FROM user_roles ur WHERE u.id = ur.user_id) roles
    FROM users u
    WHERE u.key_id = :key_id
    ''', [Param('key_id', KeyId)], Transaction);
end;

Сорян, Habr не умеет подсвечивать корректно современный синтаксис Delphi

Помимо этого, можно использовать SQL Builder.

procedure Test;
begin
  var Params: TSQLParams;
  var Sel :=
    Select([User, UserRole.Desc.Table('ur').&As('description')]).
    From(User).
    LeftJoin(
      Select('*').
      From(UserRole).Where(UserRole.RoleType = 1), 'ur').
    On(User.RoleId = UserRole.Id.Table('ur')).
    Where(not (User.Id = TGUID.NewGuid) or (User.Status in [1, 2, 3])).
    Where(User.Status and 1 = 0).
    Where(User.Name = 'Dan').
    Where(User.Status in
      Select(User.Status).From(User).Where(User.RoleId in [TGUID.NewGuid, TGUID.NewGuid])).
    OrderBy([User.Name, DESC(User.Status)]).
    GroupBy([User.Id]);

  writeln(Sel.Build(Params));
  writeln;
  for var Param in Params do
    writeln(Param.Key, ': ', Param.Value.TypeInfo.Name, ' = ', Param.ToString);
end;

Здесь проверка SQL идет на уровне компиляции проекты, если схема User/UserRole изменится, то мы везде будем видеть ошибки, даже при передаче параметров.

Результат кода
SELECT user.*, ur.desc description
 FROM user
 LEFT JOIN (
    SELECT *
     FROM user_role
     WHERE user_role.type = :p0) ur ON user.role_id = ur.id
 WHERE (NOT (user.id = :p1)) OR (user.status in (:p2, :p3, :p4)) AND user.status & 1 = :p5 AND user.name = :p6 AND user.status in (
    SELECT user.status
     FROM user
     WHERE user.role_id in (:p7, :p8))
 ORDER BY user.name, user.status DESC
 GROUP BY user.id

p0: Integer = 1
p1: TGUID = {4D8FD3C0-9972-4269-BBB6-34E3925EABE2}
p2: Integer = 1
p3: Integer = 2
p4: Integer = 3
p5: Integer = 0
p6: string = Dan
p7: TGUID = {2A37B58A-3B2F-4320-9850-A70AA70C3971}
p8: TGUID = {3FBC11F6-381F-4443-8592-1E5FC5CDF479}

А как быть с динамическими запросами? Клеить строки то еще занятие :)

А вы про какой подход? Про самый верхний или нижний?

Я имел в виду SQL Builder, который внезапно появился в вашем коментарии. Это уже функциональность ORM, вы же теряете контроль за формированием SQL :)

В каком месте теряется контроль? Это прямое написание запроса. 1 к 1.

И к слову ответ на предыдущий вопрос. Мне никто не мешает писать этот запрос с прерываниями на If.

Быстрее отработает, чем пока ОРМ сгенерирует не пойми что

При этом дельфевому SQL Builder вы почему-то доверяете :)

ОРМ генерирует код на основе правил и связей. Билдер же просто пишет текстом то, что ты пишешь кодом. Ничего от себя не добавляет. Метод Select добавит только Select и т.д.

Использование SQL Builder, не исключает использование ORM для миграций и LOW-CODE админки. Если конечно в вашем стеке существует ORM с админкой и вы знаете ORM или готовы изучать. Если такая ORM существует, то не использовать ее, это потеря скорости разработки.

У меня как-то был очень активный спор с командой на эту тему. Не хотели внедрять ORM, так как просто никто кроме меня в команде с ними не работал или очень мало и просто не хотели изучать.

Нужна была админка и команда на полном серьезе хотела писать ее самостоятельно и выделить целый квартал времени для фронта и бэка.

В итоге я за выходные ее сделал всю на LOW-CODE решении с ORM. Показал команде и с тех никто на эту тему не спорит, надо изучать ORM или нет. В запросах в приложении ORM не использовали.

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

Во мне говорит опыт. ОРМ в Delphi тоже есть и не одна. И я их тоже использую. Да и не только в Delphi.

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

ORM может сделать для запроса и оптимизацию, которую руками в SQL никогда не напишешь.

Приведите пример когда ORM что-то оптимизирова лучше, чем это может сделать разработчик? Не должно быть такого, что вы не понимаете SQL, который генерирует ORM. Тогда использование ORM действительно вредно и только для учебных проектов прокатит такой подход. Или маленьких MVP, которые потом кто-то будет переписывать.

Разработчик должен отлично владеть SQL и знать внутреннее устройство БД. Иначе с ORM в руках он сделает все очень плохо.

ORM не избавляет вас от необходимости изучать SQL и БД. Все запросы и миграции написанные с ORM всегда нужно проверять.

Например, EF Core для SQL Server операцию Contains не транслирует в IN напрямую, а использует JSON, так как это быстрее и снимает ограничение на кол-во параметров. Также Insert и Update транслируется в Merge. Много ли разработчиков будут использовать JSON в сырых SQL или писать Merge вместо Insert и Update?

Много ли разработчиков будут использовать JSON в сырых SQL или писать Merge вместо Insert и Update?

А почему нет? Не вижу ничего, что может быть сложным. ORM это хорошо, но каждый должен понимать как оно работает и не нуждаться в ORM как в инструменте для оптимизации. Тем более что оптимизация в ORM это часные случаи а в общем случае все зависит от того, насколько хорошо разработчик понимает, что он делает.

Много ли разработчиков будут использовать JSON в сырых SQL или писать Merge вместо Insert и Update?

Если разработчик не ноль в SQL, то вероятность подобного достаточно высока. Причём понимающий разработчик ещё и подумает, является ли такая версия запроса для операции оптимальной.

так как это быстрее и снимает ограничение на кол-во параметров

Ну насчёт быстрее - совсем не догма. Зависит от кучи параметров, и в первую голову от ожидаемого количества референсных значений. Если их обычно в пределах 3-5 значений, то скорее всего получим даже замедление.

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

чтобы постгрес выплевывал жсон

это можно использовать, если надо вернуть просто список строк вернуть. если протокол будет сложнее, то не уверен, что бд эффективно сможет сформировать ответ.

из базы не дергаются поля, которые не нужны (чего ORM не умеет, если в лоб)

не знаю orm, который не умел бы делать проекции.

всяко быстрее, чем то же самое у, например, питона.

а у питона под капотом json не тот же си работает? про питон мало что знаю, но в dotnet сериализатор json очень быстрый.

если протокол будет сложнее

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

не знаю orm, который не умел бы делать проекции.

Так-то оно так, вот только кто ж проекциями пользуется? Куда ни глянь: Model.objects.query() и погнали перебирать полноценные объекты ради одного поля. И опять же, проекция практически сводится к задаче SQLBuilder.

а у питона под капотом json не тот же си работает? про питон мало что знаю, но в dotnet сериализатор json очень быстрый.

А дело не только в жсон-сериализаторе(впрочем, даже у питона есть отдельный пакет ujson, что намекает на то, что можно быстрее). Дело и в сериализации/десериализации результата запроса. Одно дело дернуть один единственный кортеж и из него одно единственное поле, и другое дело лопатить много кортежей, каждый из которых тянуть с сервера, восстанавливать в приложении до полноценных объектов (пусть даже копеечных таплов), на ходу еще занимаясь боксингом/анбоксингом, а потом это всё обратно сваливать в жсон-сериализатор (опять боксинг/анбоксинг)

хотелось бы пример

{
    "metadata": {...}
    "rows": [...]
}

В metadata описание столбцов, в rows непосредственно данные. Как postgres отдаст такой json? Застримит или сначала полностью сформирует и только потом отдаст клиенту?

Куда ни глянь: Model.objects.query()

Куда не глянь везде select * :)

Одно дело дернуть один единственный кортеж и из него одно единственное поле, и другое дело лопатить много кортежей, каждый из которых тянуть с сервера, восстанавливать в приложении

Не думаю, что именно сериализация и мапинг добавит существенные задержки. С выборкой не сравнивал, но со вставкой был опыт с json в 3 гига, данные из которого надо было просадить в ms sql: базовики смогли это сделать за 20 минут, у меня на dotnet при помощи стандартного парсера и библиотеки linq2db получилось 2 минуты, при этом в программе не было ни строчки sql. Понятно, что в постгресе результаты могут быть другие, но это всё к тому, что нельзя безоговорочно утверждать, что в базе всегда быстрее.

Вот и все, инъекции не будет не при каких условиях. 

В статье есть об этом и в комментах уже отвечал. Я сам не раз видел на ревью, что человек даже знаная про SQL-инъекции, иногда случайно вставляет переменную через форматирование строки. Человеческий фактор всегда будет стрелять. Чем больше проект и разработчиков, тем чаще. В маленьком проекте на 5 человек за этим следить относительно просто, а когда у вас большой проект особенно монолит над которым работает 100+ программистов уже сложнее. Один раз на ревью не заметят и уже могут быть огромные финансовые и\или репутациаонные потери для проекта.

Второй подход с билдером это исключает

SQL Builder хорошее решение для запросов в приложении, но не повод отказываться от ORM для миграций и админ панели. У нас во многих сервисах используется SQL Builder (не самописный) для запросов и все равно используем ORM для описания моделей, миграций и админки.

Код сложнее? Нет. Менее удобен? Нет. Быстрее отработает, чем пока ОРМ сгенерирует не пойми что? Да.

По поводу произовдительности уже отвечал, что оверхед там не большой. Даже на больших нагруках (у нас 20+ млн пользователей) он не станет узким местом. Позже дополню статью своими ответами в комментариях. А сложнее становится, когда запрос с фильтрами, а какие именно фильтры зависит от логики. И тут начинается склейка строк и т.д. Разработчики хорошо знающие SQL используют ORM не просто так.

Используют, потому что так проще и не надо писать запросы. В нашей компании был такой разработчик на Питон. Который только ОРМ умел использовать. Запросы вообще не успел писать. От слова совсем. Как итог, недавно начал переписывать его бэкенд на Делфи.

В нашей компании был такой разработчик на Питон. Который только ОРМ умел использовать.

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

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

Используют, потому что так проще

Каждому проще делать так, как умеет. Поэтому так много противников ORM и так же много их адептов, ведь это что-то, что тоже нужно уметь использовать. Найти причину не изучать не сложно. Вместо ORM можно подставить SQL в этом утверждении суть не меняется.

Люди хорошо владеющие ORM извекают максимум из его эффективности и отлично знают сам SQL, и используют его там, где ORM не эффективен.

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

ОРМ заставляет тратить память на объекты, в то время как можно было получить сырые данные в датасет и уже его сериализовать напрямую. Вот этот момент сильно влияет на производительность.
В то время как ОРМ сначала получит данные, наплодит объектов и только потом будет сериализовать.

Вот этот момент сильно влияет на производительность.

Сильно это как? Есть бенчмарк вашего проекта? На сколько быстрее время ответа? На сколько дешевле сервера? Без конкретики "сильно влияет" это абстрактная информация ни о чем. Такая же абстрактная как и бенчмарки в вакууме из интернета.

Можно не писать запросы на ORM, а использовать Query Builder это хорошее решение, но не отменяет одновременного использования ORM для миграций и админки. Если конечно вы не считаете, что Query Builder тоже "сильно влияет"

В ОРМ ты получаешь объект, который создала ОРМ и потом сериализауешь его в JSON. Здесь два раза выделяется память: сначала из бд в объекты, потом объекты в json. Я же опускаю шаг создания объектов и данные сериализуются из датасетов напрямую (из бд в json).

Бенчмарков чего? С чем в итоге сравнивать-то? Могу примерно сказать сколько требуется памяти серверу для обслуживания 20к пользователей: где-то 10-20 мб при нагрузке в 1000-2000 запросов в секунду. Но из этих 10-20 мб, 6мб требуется на старте сервера. Но это цифры по сукти ни о чем не говорят.

Миграция у нас тоже есть, но пишется почти руками можно сказать. Для каждой версии бд пишется код миграции/отката. Строк там не много, но все же руками, а не на основе анализа моделей.

Могу примерно сказать сколько требуется памяти серверу для обслуживания 20к пользователей: где-то 10-20 мб при нагрузке в 1000-2000 запросов в секунду. Но из этих 10-20 мб, 6мб требуется на старте сервера. Но это цифры по сукти ни о чем не говорят.

Нагрузка маленькая. Экономия около нулевая. Актуально может быть только в специфичных случаях, когда мы строго ограничены в ресурсах.

Была у меня как-то задача оптимизировать ML приложение для работы на обычном ноутбуке, который поедет к заказчику на завод. Вот там нужно было экономить даже на спичках, так как была бизнес причина для этого и ограничение в ресурсах конфигурацией ноутбука. А если нет бизнес причины, то любая оптимизация\работа это оверинженеринг.

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

Вот и все, инъекции не будет не при каких условиях. 

В статье есть об этом и в комментах уже отвечал. Я сам не раз видел на ревью, что человек даже знаная про SQL-инъекции, иногда случайно вставляет переменную через форматирование строки. Человеческий фактор всегда будет стрелять. Чем больше проект и разработчиков, тем чаще. В маленьком проекте на 5 человек за этим следить относительно просто, а когда у вас большой проект особенно монолит над которым работает 100+ программистов уже сложнее. Один раз на ревью не заметят и уже могут быть огромные финансовые и\или репутациаонные потери для проекта.

Статья довольно слабая, по большей части в качестве примера видимо взята Django ORM, которая действительно много чего автоматизирует, если взять SqlAchemy, он позволяет писать практически любые запросы без ограничений и без знания SQL им пользоваться нормально по сути невозможно. Поэтому я бы сказал так - на ORM сложные запросы сложнее писать, но проще читать и поддерживать. С Raw-запросами ровно наоборот, вот собственно и весь спор.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий