Pull to refresh

Comments 106

Для того, чтобы работать с ORM, надо знать SQL. Потому что ORM это не замена SQL.

А самое главное, в ваших примерах почему-то стыдливо пропущено раскладывание результата по полям объектов. Сравнивается запрос в sql синтаксисе, который надо ещё экранировать, отправить в заранее подготовленную команду, получить ридер и прочитать его в объекты с готовой конструкцией $users = UserTable::query().

А ведь ORM - это в первую очередь Object-Relational Mapper.

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

Для того, чтобы работать с ORM, надо знать SQL...

Это очень критично, когда работаешь с нагруженной системой. Если ты пишешь на sql тебе надо знать, как то что ты делаешь будет, мапиться на планы запросов (конкретной СУБД, т.к. у каждой свои нюансы).
Если ты работаешь с орм, когнитивная нагрузка резко повышается: ты должен сначала осознавать как то, что ты пишешь, будет мапиться в запрос, который потом будет мапиться в план запроса.

Усложнение приводит к тому, что разработчики с недостаточной квалификацией начинают воспринимать происходящее в БД слое как "магию": "Я вот жахнул и вот такое получилось. позовите ДБА, пусть сделает индекс какой".

Есть довольно простой ответ - не используйте орм с сложной трансляцией кодовой конструкции в sql .

Я понимаю, не все языки позволяют отражать sql в похожих конструкциях языка. Беда-печалька

А если сорным запросом брать только id моделей, а потом Order::with(что надо)->find($ids)?

Ну или копнуть чуть глубже и найти где в твоей orm "hydrator" и его потом на добытые из базы данные применять.

UFO landed and left these words here

Ну мы в универе, кстати, в таком порядке и проходили языки: си после ассемблера.

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

UFO landed and left these words here

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

ну, на самом деле всегда можно получать ответы jsonчиком, и мапить json десериализатором.

Это не шутка

\* Вьетнамские флешбеки от редактирования SQLite БД в (3)X-UI \*

"раскладывание результата по полям объектов" - всё так и есть, кроме JavaScript/TypeScript - там драйвера сразу отдают массив нативных объектов

ORM появился как быстрый и безопасный способ работы с базой (без привязки к конкретной) для людей низкой квалификации.

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

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

Вы говорите про генераторы SQL, но его нельзя тестировать абстрагируясь от кода в котором они работают. С чистым языком запросов все легко писать в DBeaver и аналогах (хоть в консоли), без необходимости дополнительно настраивать чистое окружение для тестирования результатов запроса.

как раз генераторы как и ORM отлично заворачиваются в тесты мокаясь

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

как плюс - перекатка на другую базу как и с ORM не требует лишних телодвижений

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

Напишите тест который выполняет запрос в базу.

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

вы как будто никогда работу "в живую" не видели)

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

как плюс - перекатка на другую базу как и с ORM не требует лишних телодвижений

Это неправда. Ни с генераторами, ни с орм. Если ваш проект больше определённого размера, пересадка между разными движками баз приведёт к сложнопредсказуемому изменению времени прохождения разных запросов.

Разные базы по разному организуют кэширование данных и уровни изоляции транзакций.

А на маленьких объёмах смысла скакать между базами особо нет.

Можно пруф про то, что ORM появился для этого? Насколько я вижу, всяике паттерны в духе data mapper, metadata mapping появились вообще для другого - упрощения работы в сложной системе, где много сущностей

так вы сами ответили на свой вопрос)

Упрощение работы, все верно. Что бы было проще и безопаснее работать со сложными сущностями. Что бы было меньше требований к линейному разрабу (как непосредственно к нему, так и к результатам его труда)

Прошу прощения, что плохо выразил мысль. Под упрощением я имео ввиду не писать в сотый раз update или insert или не следить за тем чтобы имена таблиц после as не повторялись при джойнах на одну и ту же. Это не требует квалификации, это требует аккуратности. ORM же наоборот повышает требования к квалификации сотрудника - теперь ему мало знать SQL, надо ещё разбираться в дополнительной технологии.

ORM появился как решение проблемы object-relational impedance mismatch

Вы видели Cycle ORM? В норм ORM есть query builder. Проблемы покрываются получением sqlStatement. В репозитории загружаешь, что нужно. Есть lazy, eager, аннотации и behavior плагины.

«Я олд я пишу sql на бумажке, а когда вижу нормальный проект на ORM пишу про то, что сырые запросы лучше». Ну да, ноль вопросов, вы можете написать лучше, а можете не написать, а ещё коллега должен ваш sql понять, ознакомиться с билдером, а если вы юзаете голый пдо в 2025 - то вы наверное дальше пет проектов в реальной жизни вообще не находились.

Просто я не понимаю аргументов «сделано для упрощения = для низкой квалификации». Так а вы получаете пишете функционально всё как в олимпиадном программировании? Или вы всё же используете ООП, фреймворки, плагины, автолоадер композера? Или вам норм всё себе велосипедить? Да даже PER CS 2.0 - это кодинг стандарт. И прикиньте, psr вам регламентирует даже реквесты с респонсами. Это вам ок в абстракции, сырые стримы вам не ок. Зато сырой sql - блаженство?

DataMapper сделан для низкой квалификации? Компиляция схемы? Может ну для низкой квалификации нужны сырые запросы, которые вы нейронкой будете генерить, потому что нейронка не знает ORM? Это стрём, когда вообще всё написано сырым sql. Вы буквально этим говорите «мне всё равно на безопасность, я, ради 10 мс и экономии времени на чтение документации к ORM, готов писать не поддерживаемые запросы, которые при изменении бд развалятся полностью, потому что мх в проекте сотни»

Многие вещи и проекты идут на голом SQL.
"а ещё коллега должен ваш sql понять" - это как и любой другой код на любом языке и включая ORM из статьи.

Я изучая и работая на голом SQL ужасаюсь от ORM.. может у меня мало опыта во втором, но в первом я напишу быстрее четче и скорее лучше / качественнее.

PS
Oracle 5+ лет

Если брать Java, то там надо использовать что-то типа MyBatis. SQL, который лежит в одном месте, все ясно видно, и которым можно управлять, и мэппинг на объекты. Как-то же все равно надо работать с result-set. Плохо, когда этот SQL размазан аннотациями по всему проекту.

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

...ради 10 мс и экономии времени на чтение документации к ORM, готов писать не поддерживаемые запросы,...

проблема не в 10мс экономии. Проблема, когда вместо 10мс, запрос выполняется 15с (такой "фокус" был буквально на прошлой неделе). И разработчик не понимает, что происходит и как это починить, т.к. воспринимает всё что происходит в БД как магию.

UFO landed and left these words here

Прав во всем.

ORM хорошо для статичных случаев, когда разрабатывается некое решение раз и навсегда , реализуется и высекается в граните навеки.

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

Ровно наоборот. Когда схема данных развивается и меняется, ORM автоматически подхватывает изменения в объектах и запросах. А когда всё сделано на сырых sql запросах - вперёд искать по всему проекту, где используются изменённые поля и менять. А самое подлое - то, что ещё не найдено падает в рантайме. Потому что sql запрос в кавычках всегда будет валидным для вашего яп. А в какой момент исполнение кода наткнётся на пропущенный кусок, где вы забыли user_id поменять на manager_id - никто не знает.

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

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

Стараюсь следить за движением данных

Я такой подход слышал. Называется "нужно просто одновременно аккуратно менять код и базу данных и не ошибаться". Только это не работает )

Работает code-first, работает db-first и ещё одна дичь, про которую я написал статью.

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

Тесты точно также начинают врать при изменении структуры данных. Да и тесты не могут дать гарантию, что данные маппятся правильно. Особенно при добавлении новых. Потому что нет тестов, которые проверяли бы соответствие тестов структуре. Вот ORM может дать гарантию, а тесты - нет.

А самое подлое - то, что ещё не найдено падает в рантайме.

Это проблема отсутствия e2e или интеграционного теста.

Когда схема данных развивается и меняется

В больших компания строго запрещается ломать обратную совместимость в схемах БД.

Это как раз решается путем выноса логических блоков в библиотеки.

То есть вы не дергаете sql-запросы по всему коду, и orm тоже, вместо этого работаете с функциональными объектами и их свойствами: например, принимаете товар на склад или создание клиента - а как именно создаете - зашито в библиотеке.

Если угодно - это тоже можно называть ORM, но на более высоком уровне, это уже не записи в базе, это объекты предметной области: товары, клиенты, грузовики, сервера - то с чем вы работаете.

ORM в таком случае просто ни к чему.

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

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

Спасибо. Как раз с этим сталкивался неоднократно. Особенно остро на это смотришь, когда занимаешься агрегированием данных, которые на SQL делаются гораздо быстрее, а главное понятнее.

Проблема скорее не в ORM как концепции, а в том, что ее реализация на PHP или Python получается просто дико вербозная. Посмотрите ради интереса на LINQ2DB в .NET или Ktorm в Kotlin - там гораздо красивее получается, т.к. лаконично и при этом никаких магических строк

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

Хоть один адекватный комментарий увидел)

Согласен, ORM и голый SQL подходят для разных ситуаций, поэтому нельзя говорить, что что-то лучше или хуже.

Ну и конечно же, ORM - это не только генератор запросов, но и маппинг данных. Гапример, в C#-разработке в EF Core можно передавать голые SQL-запросы, если автогенерируемые не устраивают, а Dapper вообще только маппингом занимается и его часто в хайлоад используют.

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

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

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

А то что вы перевели часть запросов на простой sql - это вполне себе норма. Даже когда используют ORM - не всегда сложные запросы делают через нее. Но говорить что ORM ненужна - это с вашей стороны - недостаток опыта.

Ничего, все придет.

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

Вот это самое прекрасное. В результате получаем кашу из orm и сырого sql. => и следующие плюшки "орм" сразу идут в топку:

  • возможность малотрудозатратной смены БД

  • парой кликов меняем тип/название поля

возможность малотрудозатратной смены БД - если схема не менялась и база реляционная, то sql продолжит работать ибо стандарт, а если менялась (схема или база) то затраты будут в любом случае не малые.

Менять поля - часто сразу запрещено, если в проде, значит в камне.

"sql продолжит работать ибо стандарт"
Увы, возьмём к примеру ms sql и postgresql - разница в синтаксисе существенная, особенно касательно встроенных функций. Это не считая всяких приколов "под капотом" типа вакуума, особенностей реализации временных объектов и т.д. и т.п. - об которые надо будет обязательно удариться головой.

возьмём к примеру ms sql и postgresql - разница в синтаксисе существенная, особенно касательно встроенных функций.

Именно функций, описанных стандартом?

вы часто заглядываете в стандарт что функционал функционал sql, который вы используете соответствует стандарту?

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

Ответ на оба вопроса - да

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

Не бывает "малотрудозатратной смены БД", если только не используются только простые круды. Даже с ORM могу появиться вызовы вида NpgsqlDbFunctionsExtensions.ILike.

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

У нас с опытом приходит обратное. Есть два проекта которые +- по сути одно и тоже делают на разных предприятиях, на похожих данных и нагрузках. Можно сказать непрерывный A/B тест решения, вплоть до того что полностью одинаковые задачи от заказчиков приходят. Один древний с кучей техдолга и ужасного легаси о части которого даже никто не знает вызывается ли оно вообще, и просто держат чтобы внезапно не сломалось. Тут SQL с самописной обёрткой и снипетами. Второй свежий, ORM,чистая архитектура и все по феншую. Красивее приятнее. Разрабы любят второй и ненавидят первый и вроде все хорошо, но если хотя бы за полгода посмотреть, что происходит. Первый сильно дешевле обходится при большей удовлетворенности заказчика, команда дешевле, релизы быстрее, инцидентов меньше. Команда второго проекта при необходимости легко закрывает таски по первому, просто скопипастив снипеты и выполнив тесты. В обратную сторону так может только полтора сеньора умеющих в ORM. На вопросы почему долго, всегда в ответ ну вот модель... ORM...сущности... Медленно... Куча мест где ORM просто дергает SQL. Потому что запросы в большинстве своем сложные и тяжелые, а вытянуть всё себе и пересчитать в коде дорого и обычно заканчивается тем, что дба говорит вы там опять охренели говнокодеры соевые? вот вам запрос, выполняйте программисты мамкины. И вопрос зачем нам все эти фантики если потратив день-два впустую, по итогу все равно дёргается SQL, возникает довольно часто.

"...вот вам запрос, выполняйте программисты мамкины... "
А дальше появляется следующий забавный эффект: в коде появляется "магическое заклинание" от ДБА, которое команда разработки не понимает как и работает, и которое трогать без волшебника страшно. А потом ещё и ещё,...

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

Если у вас две разные системы ходят в одну БД - это уже потенциальная проблема. Если есть хоть какая-то бизнес логика, а не простые crud или (не простой) поиск, у вас появляются два конфликтующих набора бизнес правил. Если же там только чтение - что мешало сделать http api вместо прямых запросов в бд?

Что касается, ORM - не все они одинаковы. Есть условно два подхода - data mapper и active record. Но в целом задача ORM - дать средства для синхронизации состояния в памяти приложения с базой данных. Задачи, связанные с тупой отдачей данных и построения для этого сложных выборок они не обязаны решать. При этом, если мы берём sqlalchemy - у него есть билдер запросов, который как раз помогает собирать динамический сложный sql. А если берём другую ORM - Django, то у неё свой подход и если вы хотите загружать что-то, сильно не совпадающее с сущностями бизнес логики, у вас начинаются сложности.

Моё мнение тут:

  1. Избавьтесь от двух разных технологий работающий с одной БД. Обеспечьте стабильное межсервисное API

  2. Разделите задачи изменения состояния системы и просто отдачи среза данных. Выбирайте, нужен вам ORM или нет для каждой из них

  3. Среди ORM тоже есть варианты, посмотрите что есть.

В п.1 вы фактически предлагаете написать три новых системы.

SQL vs ORM

Некорректное противопоставление. Object-Relational Mapping в системе в любом случае присутствует в каком-то виде, если там есть БД и объекты. Другой вопрос: надо ли подключать к проекту комбайн типа хибера, чтобы с этим работать. Мы свой микрофреймворк написали под Vert.x.

Мы свой микрофреймворк написали под Vert.x.

Есть детали какую задачу решали (для Vert.x)? Почему не взяли Quarkus?

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

Я в своих проектах выбрал комбинированный подход. Использую новый DAO объект на каждый запрос в базу данных, и уже в DAO инкапсулирую чистые SQL запросы (когда сложные или отчеты) или запросы через Doctrine DBAL (легки выборки, вставки, удаления, обновления). Из минусов: маппинг данных в объекты приходится делать руками, много классов, много кода для банальных вещей (для ORM). Из плюсов: полная независимость бизнес-логики от реализации хранения данных. В целом можно вообще "угореть" и в DAO инкапсулировать логику работы с ORM, и тогда в бизнес логике будет только работа с DAO.

В последних версиях Django можно без аннотаций

from django.db.models import Count, Q, lookups
users = User.objects.filter(
    lookups.GreaterThan(Count('orders', filter=Q(orders__status='new')), 0),
    orders__status='completed',
)

Выполняем его в raw конструкции и получаем те данные, которые ожидаем

Для аналитических select запросов вы вполне можете, конечно, выполнять сырые запросы и без отказа от ORM, чем то в духе orm->executeRawQuery(*rawSql*), завернув во что угодно. Если еще и тестами все перекрыть наглухо, то вполне даже вариант, если таких запросов немножко и нужны они только в рамках одного домена огромного приложения.

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

Второй невсратый вариант - агрегировать данные отдельно (либо прям перекладывать данные в отдельную бд, либо ходить другим клиентом в readonly режиме и собирать то что нужно), встраивать ETL цепочки поверх.

Это если для аналитики.

Если же речь про радикальное "снесу всё, ща быстренько делаем красиво без орм" - такие типочки-грибочки трусливо увольняются, когда получают задачу которая без ORM доставляет слишком много проблем (типа сидеть и вручную расставлять INSERT'ы в правильном порядке при сохранении большого агрегата где у каждой связи еще связь, и у нее связь, и у нее связь).

Рефакторинг будет максимально развеселым. Баги, неконсистетность, регрессии... Вы это героически преодолеете? Ок.

(де)Сериализовывать будете как, ручками и дтошками? Вы герой, ок.

Миграции тоже вручную писать будете? Ну, вы герой. Пока что. Скоро запал закончится.

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

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

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

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

Создал проблему -> взял ответственность за решение -> гордится -> страдает -> уходит. Такой путь.

Вы на этапе "гордится".

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

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

Миграции тоже вручную писать будете? Ну, вы герой. Пока что. Скоро запал закончится.

В основном пользуюсь Dapper + FluentMigrator. Запал до сих пор не кончился. Проблемой N+1 не страдаю, все данные в память для агрегации не вытаскиваю.

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

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

Какие аргументы используют сторонники ORM подхода?

Это - какое-то сравнение с тем самым конём сферической формы. Если сравнивать с вполне себе земным SQL, то пункт 1 - это вопрос аккуратности (используйте подготовленные запросы, там тоже "инъекция не пройдёт"), пункт 4 не соответствует действительности (и вы пояснили почему), пункт 6 вообще непонятен, а пункт 3 так и вовсе сродни идеальному газу.

Нативный, продуманный до мелочей, лаконичный синтаксис SQL

Незаметно что-то. Вы вообще в курсе, что SELECT COUNT(*) никогда не возвращает NULL?

Это неправда. в MS SQL по пустой таблице - вернет. По крайней мере, возвращал в предыдущих версиях

SQL Server 2008 возвращает честные нули - только что проверил. За более древние версии не скажу - но найти такой анахронизм живым, думаю, весьма проблематично.

К тому же возвращение NULL при запросе в пустую таблицу - совершенно лишено логики. Ибо с точки зрения SQL значение NULL есть "неизвестно", а количество записей в пустой таблице очень даже известно - и равно нулю.

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

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

В битриксе вообще много чего перемешано. И эти приколы с D7 - когда решили все писать на нем, внезапно в доке натыкаешься - используйте старое ядро. Но при этом битрикс остается фаворитом среди CMS.

ORM - хороший инструмент для простых CRUD операций.

Недавно встречал мнение, что Django ORM годится как раз для простых случаев, тогда как для более сложных лучше подходит SQLAlchemy. Вот с Алхимией я как раз тесно работаю, не скажу, что знаю эту библиотеку досконально, но со сложными запросами она справляется просто замечательно - кучи join, агрегации и прочие замечательные штуки нашей непростой бизнес-логики. Очень сильно облегчают жизнь явное указание связей между таблицами, а так же всякие оптимизации типа joinedload. Запросы отлаживаются, изменяются без проблем, то есть поддерживать их довольно просто, на мой взгляд.

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

Автор статьи просто не использовал нормальные ORM типа RedBeans на php или Entity Core на c#.

RedBeans умеет принимать запросы на sql и выдавать объекты, над миграциями париться не надо, даже классы модели писать не надо, всё сделано для людей.

Почитал, поржал. Ок. Есть скажем у вас на проекте Spanner, MySql, Postrgess, DynamoDB, Datastore и упаси боже какой нить Pervasive или Neoj4 и я представляю команду которая пишет запросы в этот заопарк. ORM хорош когда вам надо как и нарисали выше общий подход к работе. Нет сложности к ORM нарисать и драйвер свой. Непонимаю возникшей сложности. Я на доктрине запросы делал с вложенными и форсом индексов в джойнах. Если использование ОРМ заключаентся в модельках и как красиво запросики делаются, то это ужас.

Автор вроде описал другое разнообразие - база одна, а платформы разные. Т.е. чтобы обеспечить "общий подход к работе" - ему и приходится отказываться от ORM, (который каждый со своим DSLем) - в пользу стандартизированного SQL который на всех платформах одинаков.

Сначала делают прототип, и ОРМ - прекрасный выбор. Не доволен? На ассемблере базу данных пиши.

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

QueryBuilder это абстракция над SQL, и ей можно пользоваться пока она не становится сложнее, чем то что она абстрагирует, у меня есть запросы которые занимают экран в SQL'е, если конвертнуть это в вызов билдера то это крайне сложно читать, возникает вопрос: зачем? Выше писали что есть хорошие билдеры ну...- нет, абстракция как известно всегда течёт, как только появится билдер который покроет мощность SQL он будет такой же сложный как сам SQL, чтобы им пользоваться нужно будет долго изучать, так же долго как SQL, возникает вопрос: зачем? ответ известен, оно же автоматически смапит результаты, а не проще сначала сделать запрос, а потом смапить результаты? в том числе с помощью гидратора ORM, это гораздо более читаемо. Тут выше писали про сложные запросы внутри ORM, когда я такие пишу (иногда приходится) я физически ощущаю, что занимаюсь ерундой, попыткой выразить через абстракцию то для чего она не предназначена, свои простые 80 процентов она выполняет хорошо, но те 20 всегда будут за чистым SQL.

Искренне жаль программистов которым в 2025 году приходится писать на PHP. Это не оскорбления или унижение – это боль. ORM удобен и прекрасен в копмилируемых языках со строгой типизацией и параметризацией. Например в Java, а ещё лучше в Kotlin. Главное вовремя перестать использовать всякие @OneToMany и другие @ManyToMany чтобы контролировать процесс генерации и выполнения запросов. А так же обязательно перестать использовать @Transactional, вместо неё свой враппер для транзакций типа transaction { ...code... } для Kotlin. Много лет использую такой урезанный JPA на многих проектах – вообще бед не знаю.

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

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

['=this.ID' => 'ref.USER_ID', '=ref.STATUS' => new \Bitrix\Main\DB\SqlExpression('?', 'completed')]

когда я вижу что-то подобное — прям жутко становится.

Вторая проблема языков типа PHP & Python - это отсутствие строгой типизации. А при использовании ORM эта проблема возводится в квадрат. Как контролировать размерность числовых типов база данных? Как на уровне кода гарантировать что в поле INT не будет присвоено значение поля BIGINT? Или у вас в базе есть timestamp поле с временной зоной и поле обычного timestamp без временной зоны — PHP код никак это не проверит и запросто позволит вам присваивать одно значение другому. Как работать с ENUM полями, как гарантировать на уровне кода что в это поле будут положены только допустимые значения? Никак, для всего этого приходится писать тесты или ловить ошибки в продакшн.

В Java/Kotlin всё строго — ты определяешь класс, один раз прописываешь таблицу которую он маппит, прописываешь типизированные поля у этого класса и им соответствующие поля в таблице. И всё, больше ты не волнуешься что где-то попытаешься запросить/сохранить несуществующее поле, или попробуешь присвоить значение полю которое не сможет быть сохранено в базу.

Вот так?)

use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'products')]
class Product
{
    #[ORM\Id]
    #[ORM\Column(type: 'integer')]
    #[ORM\GeneratedValue]
    private int|null $id = null;
    #[ORM\Column(type: 'string')]
    private string $name;
    // .. (other code)
}

Проблемы Bitrix'a оставим Bitrix'у. Кажется вы давно не заглядывали в PHP, там есть declare(strict_types=1) которая появилась в 2015 году) Кстати говорят что DoctrineORM аналог спринга, насколько полный у вас есть возможность оценить по документации

Согласен — это большой шаг вперёд. Но вот из их же документации:

$product = $entityManager->getRepository('Product') ->findOneBy(array('name' => $productName));

и снова название сущности и поля — строки. И очевидно никакой проверки ни названий, ни типа данных до момента исполнения кода.

Для генерации запросов в JPA есть несколько подходов, например:

1) interface Repository - это такие интерфейсы в которых ты описываешь только как должны выглядеть функции для select/update/delete, а реализацию этих функций берёт на себя JPA. Например:

@Repository

interface UserInfoRepository : CrudRepository<UserInfo, Long> {

fun findByGuidIn(guids: Iterable<UUID>): List<UserInfo>

}

В этом случае JPA сам создаст реализацию метода findByGuidIn с проверкой наличия поля guid в классе UserInfo и его типа. Притом эти проверки будут сделаны на этапе запуска приложения, а не на этапе вызова метода.

2) Использовать JPA Query Language - это такой аналог SQL в котором вы оперируете не названиями таблиц и полей в них, а названиями классов и полей в классах. Опять же, синтаксис этих запросов будет проверен на этапе запуска программы, и если приложение стартануло (или прошло какие-либо тесты, даже те которые не вызывают этот метод-запрос) — вы можете быть уверены что запрос будет работать как надо.

3) Для Kotlin есть полностью строго-типизированный DSL для построение SQL запросов. Выглядит это вот так:

val users = query {
select(user)
from(user)
where {
user.email eq email
user.status eq status
}
}

тоже самое в Java

List result = new JPAQuery(entityManager)
.select(user)
.from(user)
.where(
user.email.eq(email)
.and(user.status.eq(status))
)
.fetch();

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

Так что да, большая часть запросов описывается декларативно, просто через название метода, его параметры и возвращаемый тип данных ORM сама строит SQL запрос. Почти во всех остальных случаях — JPA Query Language. Конечно иногда остаются единичные запросы которые лучше написать на чистом SQL — но это скорее очень редкое исключение, и покрывая тестами такие запросы вы можете быть спокойны в целом за работу вашего слоя взаимодействия с базой данных.

// $em instanceof EntityManager
// All users that are 20 years old
$users = $em->getRepository('MyProject\Domain\User')->findBy(array('age' => 20));
// All users that are 20 years old and have a surname of 'Miller'
$users = $em->getRepository('MyProject\Domain\User')->findBy(array('age' => 20, 'surname' => 'Miller'));
// A single user by its nickname
$user = $em->getRepository('MyProject\Domain\User')->findOneBy(array('nickname' => 'romanb'));

2) DQL stands for Doctrine Query Language and is an Object Query Language derivative that is very similar to the Hibernate Query Language (HQL) or the Java Persistence Query Language (JPQL).

Доктрина на DataMapper появилась в 2011 году.

да, неплохо. Но вы правда не видите разницы? Названия сущностей — строки. Название полей — строки. Типы данных никак не проверяются.

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

Скрытый текст
<?php

declare(strict_types=1);

namespace App\Auth\Entity\User;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use DomainException;

final readonly class UserRepository
{
    /**
     * @param EntityRepository<User> $repo
     */
    public function __construct(private EntityManagerInterface $em, private EntityRepository $repo) {}

    public function hasByEmail(Email $email): bool
    {
        return $this->repo->createQueryBuilder('t')
            ->select('COUNT(t.id)')
            ->andWhere('t.email = :email')
            ->setParameter(':email', $email->getValue())
            ->getQuery()->getSingleScalarResult() > 0;
    }

    public function hasByNetwork(Network $network): bool
    {
        return $this->repo->createQueryBuilder('t')
            ->select('COUNT(t.id)')
            ->innerJoin('t.networks', 'n')
            ->andWhere('n.network.name = :name and n.network.identity = :identity')
            ->setParameter(':name', $network->getName())
            ->setParameter(':identity', $network->getIdentity())
            ->getQuery()->getSingleScalarResult() > 0;
    }

    public function findByJoinConfirmToken(string $token): ?User
    {
        return $this->repo->findOneBy(['joinConfirmToken.value' => $token]);
    }

    public function findByPasswordResetToken(string $token): ?User
    {
        return $this->repo->findOneBy(['passwordResetToken.value' => $token]);
    }

    public function findByNewEmailToken(string $token): ?User
    {
        return $this->repo->findOneBy(['newEmailToken.value' => $token]);
    }

    public function get(Id $id): User
    {
        $user = $this->repo->find($id->getValue());
        if ($user === null) {
            throw new DomainException('User is not found.');
        }
        return $user;
    }

    public function getByEmail(Email $email): User
    {
        $user = $this->repo->findOneBy(['email' => $email->getValue()]);
        if ($user === null) {
            throw new DomainException('User is not found.');
        }
        return $user;
    }

    public function add(User $user): void
    {
        $this->em->persist($user);
    }

    public function remove(User $user): void
    {
        $this->em->remove($user);
    }
}

ну неплохо, да, и правильно что вы определили методы с типизированными параметрами.

Но вот ваш пример выше - ...->findBy(array('age' => 20, 'surname' => 'Miller')) — тут вы можете передать любой набор условий, любые названия полей и любые типа данных, потому что этот массив совсем никак не типизирован, вы можете вообще туда любой массив запихать. Да, в вашем реальном примере вы подстраховались создав типизированные методы — это хорошо. Но на уровне ORM как не было никакой проверки до реального выполнения кода, так и нет.

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

ну хорошо, значит в связке с PHPStan PHP перестаёт быть языком без строгой типизации )) Потому что языки без строгой типизации — это боль. Что собственно я и хотел сказать.

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

См. выше, и еще пример маппинга сущности

Скрытый текст
<?php

declare(strict_types=1);

namespace App\Auth\Entity\User;

use App\Auth\Service\PasswordHasher;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use DomainException;

#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'auth_users')]
final class User
{
    #[ORM\Column(type: IdType::NAME)]
    #[ORM\Id]
    private Id $id;

    #[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
    private DateTimeImmutable $date;

    #[ORM\Column(type: EmailType::NAME, unique: true)]
    private Email $email;

    #[ORM\Column(type: Types::STRING, nullable: true)]
    private ?string $passwordHash = null;

    #[ORM\Column(type: StatusType::NAME, length: 16)]
    private Status $status;

    #[ORM\Embedded(class: Token::class)]
    private ?Token $joinConfirmToken = null;

    #[ORM\Embedded(class: Token::class)]
    private ?Token $passwordResetToken = null;

    #[ORM\Column(type: EmailType::NAME, nullable: true)]
    private ?Email $newEmail = null;

    #[ORM\Embedded(class: Token::class)]
    private ?Token $newEmailToken = null;

    #[ORM\Column(type: RoleType::NAME, length: 16)]
    private Role $role;

    /**
     * @var Collection<int, UserNetwork>
     */
    #[ORM\OneToMany(mappedBy: 'user', targetEntity: UserNetwork::class, cascade: ['all'], orphanRemoval: true)]
    private Collection $networks;

    private function __construct(Id $id, DateTimeImmutable $date, Email $email, Status $status)
    {
        $this->id = $id;
        $this->date = $date;
        $this->email = $email;
        $this->status = $status;
        $this->role = Role::user();
        $this->networks = new ArrayCollection();
    }

    public static function joinByNetwork(
        Id $id,
        DateTimeImmutable $date,
        Email $email,
        Network $network
    ): self {
        $user = new self($id, $date, $email, Status::active());
        $user->networks->add(new UserNetwork($user, $network));
        return $user;
    }

    public static function requestJoinByEmail(
        Id $id,
        DateTimeImmutable $date,
        Email $email,
        string $passwordHash,
        Token $token
    ): self {
        $user = new self($id, $date, $email, Status::wait());
        $user->passwordHash = $passwordHash;
        $user->joinConfirmToken = $token;
        return $user;
    }

    public function confirmJoin(string $token, DateTimeImmutable $date): void
    {
        if ($this->joinConfirmToken === null) {
            throw new DomainException('Confirmation is not required.');
        }
        $this->joinConfirmToken->validate($token, $date);
        $this->activate();
    }

    public function attachNetwork(Network $network): void
    {
        foreach ($this->networks as $existing) {
            if ($existing->getNetwork()->isEqualTo($network)) {
                throw new DomainException('Network is already attached.');
            }
        }
        $this->networks->add(new UserNetwork($this, $network));
        if ($this->isWait()) {
            $this->activate();
        }
    }

    public function requestPasswordReset(Token $token, DateTimeImmutable $date): void
    {
        if (!$this->isActive()) {
            throw new DomainException('User is not active.');
        }
        if ($this->passwordResetToken !== null && !$this->passwordResetToken->isExpiredTo($date)) {
            throw new DomainException('Resetting is already requested.');
        }
        $this->passwordResetToken = $token;
    }

    public function resetPassword(string $token, DateTimeImmutable $date, string $hash): void
    {
        if ($this->passwordResetToken === null) {
            throw new DomainException('Resetting is not requested.');
        }
        $this->passwordResetToken->validate($token, $date);
        $this->passwordResetToken = null;
        $this->passwordHash = $hash;
    }

    public function changePassword(string $current, string $new, PasswordHasher $hasher): void
    {
        if ($this->passwordHash === null) {
            throw new DomainException('User does not have an old password.');
        }
        if (!$hasher->validate($current, $this->passwordHash)) {
            throw new DomainException('Incorrect current password.');
        }
        $this->passwordHash = $hasher->hash($new);
    }

    public function requestEmailChanging(Token $token, DateTimeImmutable $date, Email $email): void
    {
        if (!$this->isActive()) {
            throw new DomainException('User is not active.');
        }
        if ($this->email->isEqualTo($email)) {
            throw new DomainException('Email is already same.');
        }
        if ($this->newEmailToken !== null && !$this->newEmailToken->isExpiredTo($date)) {
            throw new DomainException('Changing is already requested.');
        }
        $this->newEmail = $email;
        $this->newEmailToken = $token;
    }

    public function confirmEmailChanging(string $token, DateTimeImmutable $date): void
    {
        if ($this->newEmail === null || $this->newEmailToken === null) {
            throw new DomainException('Changing is not requested.');
        }
        $this->newEmailToken->validate($token, $date);
        $this->email = $this->newEmail;
        $this->newEmail = null;
        $this->newEmailToken = null;
    }

    public function changeRole(Role $role): void
    {
        $this->role = $role;
    }

    public function remove(): void
    {
        if (!$this->isWait()) {
            throw new DomainException('Unable to remove active user.');
        }
    }

    public function isWait(): bool
    {
        return $this->status->isWait();
    }

    public function isActive(): bool
    {
        return $this->status->isActive();
    }

    public function getId(): Id
    {
        return $this->id;
    }

    public function getDate(): DateTimeImmutable
    {
        return $this->date;
    }

    public function getEmail(): Email
    {
        return $this->email;
    }

    public function getRole(): Role
    {
        return $this->role;
    }

    public function getPasswordHash(): ?string
    {
        return $this->passwordHash;
    }

    public function getJoinConfirmToken(): ?Token
    {
        return $this->joinConfirmToken;
    }

    public function getPasswordResetToken(): ?Token
    {
        return $this->passwordResetToken;
    }

    public function getNewEmail(): ?Email
    {
        return $this->newEmail;
    }

    public function getNewEmailToken(): ?Token
    {
        return $this->newEmailToken;
    }

    /**
     * @return Network[]
     */
    public function getNetworks(): array
    {
        /** @var Network[] */
        return $this->networks->map(static fn (UserNetwork $network) => $network->getNetwork())->toArray();
    }

    #[ORM\PostLoad]
    public function checkEmbeds(): void
    {
        if ($this->joinConfirmToken && $this->joinConfirmToken->isEmpty()) {
            $this->joinConfirmToken = null;
        }
        if ($this->passwordResetToken && $this->passwordResetToken->isEmpty()) {
            $this->passwordResetToken = null;
        }
        if ($this->newEmailToken && $this->newEmailToken->isEmpty()) {
            $this->newEmailToken = null;
        }
    }

    private function activate(): void
    {
        if ($this->isActive()) {
            throw new DomainException('User is already active.');
        }

        $this->status = Status::active();
        $this->joinConfirmToken = null;
    }
}

конечно это моё субьективное мнение — но на метод getNetworks просто больно смотреть. Вроде методы класса нормально типизированы, но тут вдруг раз и getNetworks(): array - и нужны какие-то хаки в комментариях. Я не хочу сказать что это не работает — очевидно это работает, но блин, костыли на костылях.

Дженерики не завезли пока, это много где сложно сделать, не только в php. Формально можно сделать NetworkCollection и возвращать её.

Это всё не преимущество орм

Вот преимущество:

$user->name = 'John';
$user->email = 'new@email.com';
$user->save();  // UPDATE users SET name='John', email='new@email.com' WHERE id=123

Даже 5-класник напишет

Эту статью стоило озаглавить как "Почему я перестал использовать ORM Битрикс и ещё две другие ORM на одном проекте", но тогда скорее было бы интереснее почитать, что за сумасшедшие это могли использовать. А так...

Один из проектов содержал Битрикс + Laravel. Тогда и стали идти мысли о целесообразности использования ОРМ. А вообще, я упоминал, что работаю с разными фреймворками, на разных языках. Почему вы обобщили в кучу - наверное, не правильно поняли.

Да, неправильно понял. Просто начал читать и увидел, что именно вас сподвигло и дальше Битрикса уже воспринимать не видел смысла. Удивительно, что вы до сих пор не пришли к выводу, что пора отказаться от битрикса. Ну, может быть, вам за это платят, конечно. Но тот же ORM Django обычно не является источником такого страдания, чтобы прийти к выводу, что проще сделать сырой запрос (его же всё равно надо готовить, переносить данные из Python в SQL, форматировать, экранировать; с PHP скорее всего такая же проблема, хотя там с БД пока не работал). Если ORM не справляется, то часто можно доделать с помощью того же Python. Запросы SQL в сравнении с ORM и Python, в конечном итоге, часто оказываются ненамного более читаемыми и удобными, их составление именно в сложных случаях это тот ещё паззл, который проще решить в обычном языке программирования. Собственно и проблему с доступом к одной БД из разных фрейморков и языков программирования можно было бы решить через общий API: написание в каждом фреймворке отдельного кода для подготовки SQL-запросов - тоже странная идея. И тогда можно было бы обойтись одной удобной ORM. Вашей исходной проблемы опять бы не было и вы бы не стали изучать SQL. Хотя изучать SQL нужно независимо от того, насколько странные проекты вы создаёте или поддерживаете.

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

Плюс. Проблема с ORM на текущий момент в основном психическая. "SQL пишут старые деды, которые больше ничего не знают." Ну это типа Кобола или Фортрана. Когда надо написать SQL начинается ломка. ORM это психзащита, способ отгородиться от этой гадости.

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

Я с 1996 года с ним года работаю, я Database Architect, но в последнее время в основном java микросервисы. Надо было /s поставить. Я не против orm, то что я написал, это некая обобщенная утрированная причина по которой не любят писать SQL. На самом вообще все плохо, и миллион строк хранимых процедур плохо, и миллион строк мутного джава кода плохо, и dsl не всегда спасает, там свои заморочки. Как обычно, всегда какие-то компромиссы.

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

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

Плюсую. А ещё если появляется join, group by. Начинается веселье - в поля сверху добавить строчку, в середину запроса добавить джойн, дальше в середину where, если надо, внизу в group by поле. На больших фильтрах получаем совершенно нечитаемый код. А с ORM - условно один if, один блок кода, читаемый, в котором всё собираешь в кучку.

В общем как обычно - везде используй подходящие инструменты.

Хочется одним комментарием выразить благодарность сообществу. Благодаря вашим глубоким мыслям, расширил свой кругозор и иначе взглянул на проблему. Хотя остался со своими принципами - использовать ОРМ лишь в простых сценариях, в зависимости от платформы, так как в сложных начинается вырвиглаз.

Комментаторы приводили примеры с несколькими базами данных в проекте. Я никогда не сталкивался с таким, а если бы столкнулся, то задумался о разделении проекта на множество микросервисов. Где каждый сервис работает со своим типом БД и так далее. Хотя бы через докер.

PHP прекрасный язык. Он умирает и возрождается - оставляя огромное количество проектов и разработчиков, которые ими занимаются. В нем многое съедобно, и тот PDO, который упоминался в комментариях, может послужить упрощением пользовательского кода. Когда ты пишешь нативно, просто, без лишних зависимостей, без абстракций написанных "ПРОФЕССИОНАЛАМИ" или "СООБЩЕСТВОМ ПРОФЕССИОНАЛОВ", которые никогда не ошибаются.

Я программирую на python, go, javascript и php. Все языки имеют свои особенности. Часто очень даже бесячие, в сравнении с php, где многое упрощено и достаточно функционально для большинства задач. Иногда, использую PDO чтобы не тянуть ничего лишнего. Мне это не доставляет труда, потому что я работаю с данными через интерфейс DBeaver.

И ни в коем случае SQL нельзя сравнивать с программированием компиляторов или языком программирования ассемблер. Его обязательно нужно изучать, хотя бы на базовом уровне. Что к сожалению не все готовы принять, опираясь на "знание ОРМ своего фреймворка". Думаю, что использовать ОРМ без знания SQL, так же вредно, как использование tilewind без знания CSS. Скопировать/вставить. А потом появляются вакансии с опытом оптимизации запросов к БД.

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

Ваш запрос решается примерно так:

class User(DBTable):
    name: str

class Order(DBTable):
    user: User
    status: str

...

q1 = (
    User.query('users_with_completed_orders'). # name is just for SQL decoration
    select('pk').
    filter(lambda x: x.orders.status == 'completed')
)
q2 = User.query()
sub = q2.with_query(q1)
(
    q2.select_objects(). # trick to select '*'
    select(new_orders_count=lambda x: x.orders.pk.count).
    filter(lambda x: (x << sub) & (x.orders.status == 'new')).
    sort_by('name')
)

for x in q2:
    print(x)

это генерирует немного другой запрос, но с аналогичной логикой

WITH
users_with_completed_orders AS  (
SELECT
	"user".id AS "id"
FROM "public"."user" AS "user"
LEFT JOIN "public"."order" AS "user__orders"
	ON "user".id = "user__orders".user
WHERE
	"user__orders".status='completed'
)
SELECT
	"user".name AS "name",
	"user".id AS "id",
	count("user__orders".id) AS "new_orders_count"
FROM "public"."user" AS "user"
LEFT JOIN "public"."order" AS "user__orders"
	ON "user".id = "user__orders".user
WHERE
	"user".id IN (SELECT * FROM users_with_completed_orders) AND "user__orders".status='new'
GROUP BY
	1,
	2
ORDER BY
	"user".name

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

Row(name='User #03', id=3, new_orders_count=2)
Row(name='User #04', id=4, new_orders_count=2)
Row(name='User #05', id=5, new_orders_count=2)
Row(name='User #07', id=7, new_orders_count=2)
Row(name='User #10', id=10, new_orders_count=1)

Либо, можно сделать немного менее производительный запрос, зато более короткий:

q3 = (
    User.query().
    select_objects().
    select(new_orders_count=lambda x: x.orders('new').pk.count_distinct).
    filter(lambda x: x.orders('new').status == 'new').
    filter(lambda x: x.orders('completed').status == 'completed')
)

for x in q3:
    print(x)

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

SELECT
	"user".name AS "name",
	"user".id AS "id",
	count(DISTINCT "user__orders_new".id) AS "new_orders_count"
FROM "public"."user" AS "user"
LEFT JOIN "public"."order" AS "user__orders_new"
	ON "user".id = "user__orders_new".user
LEFT JOIN "public"."order" AS "user__orders_completed"
	ON "user".id = "user__orders_completed".user
WHERE
	"user__orders_new".status='new'
	AND "user__orders_completed".status='completed'
GROUP BY
	1,
	2

Можно оформить ещё через `SUM(CASE ...)`, но получится более громоздко...

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

Тем не менее, если фреймворк вам понравится, буду рад челленджам на любые нестандартные запросы ;)

В мире Java - есть JPA и CriteriaQuery, еще Hibernate - давно ими не пользовался для написания запросов. Мне трудно было написать что-то сложнее JOIN, приходилось ломать голову долго, хотя написание того же запроса на SQL занимало 3 минуты. В итоге, JPA удобно использовать для быстрого и удобного маппинга, плюс Spring Data для несложных критериев. Для остального уже SQL.

Главная фича ORM это ченж трекинг и маппинг. И особенно удобно это в CQRS на стороне Command. Достаете из репозитория агрегат (который может быть сущностью вроде Order с вложенным OrderItems), за счёт маппинга агрегат собирается автоматически по довольно простым правилам маппинга описанным декларативно; далее мутируете агрегат, соблюдая инварианты, и далее вызываете "Сохранить". Все, ОРМ сам высчитает diff, постарается его применить к конкретной базе которая используется на проекте, применит оптимистичную конкуренцию и тд.

При этом использование ORM на стороне Query опционально и зачастую лишнее. Сторона Query в базе настолько многогранна, что буквально есть 100 способов сделать одно и то же, и в этом случае ORM всегда будет отставать от драйвера базы и будет ещё слой своих багов накидывать.

Если я использую тактические паттерны DDD на проекте, выделяю явно слой домена, агрегаты и ТД, то у меня всегда используется ОРМ, потому что это очень удобно.

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

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

Например поменять схему через DDL бывает проще. Но потом повторяй аналогичное в модели. Или чем ломать голову, написать SQL запрос, отладить его, а потом модель преобразует его в кодовое представление. Разных сочетаний множество. Прозрачно переключаться туда-сюда, не думая как удобнее сделать, а осмысливая что и зачем — ORM и SQL, через минуту SQL и ORM, еще через пять — снова обратно, код вперед. Синхронизация.

Например держать автогенерируемые модели в отдельном модуле, куда будут извлекаться все структуры, типы, связи, свойства (pk, uq, nullable, default, check). Надо надстроить — удобный механизм наследования. IDE проверит контракты и визард при необходимости поможет разрешить конфликты.

Sign up to leave a comment.

Information

Website
betboom.ru
Registered
Founded
2011
Employees
1,001–5,000 employees
Representative
v_domanin