Pull to refresh

Comments 192

Как обычно, «Истина где-то рядом»…
Идеальным мне лично представляется ORM с возможностью писать чистые запросы, и далее в коде —
«select * from table where id = ?» — через ORM,
«select a.field, b.field as field2 from… left join on… where a.id in(....) or b.name like ....» — всё же писать на чистом SQL
UFO just landed and posted this here

Как и Eloquent, как впрочем и большинство ORM

Я пару лет назад понял и принял одно очень простое правило — списков объектов нет, есть только отчёты и там проще и быстрее в поддержке м развитии sql. Для выборки одного объекта на карточку этого объекта скорее всего orm может прокатить до определенного момента, в некоторых случаях, все равно на sql и там перейдешь. Для добавления или редактирования одного объекта тоже orm прокатит. А вот для тех, кто статистику с 100к записями в модели сериализует вместо hashmap или пр. есть отдельный котёл. )

А что не так со списком объектов? Вот прямо сейчас пишу вывод отчёта в виде шаблонизации и простенькой агрегации (суммирования итогов) в приложении списка объектов CashflowMonthlyReportEntry. Можно было бы и агрегацию в БД делать, но это два тяжелых запроса, отличающихся только наличием одного поля и GROUP BY по нему — не уверен, мягко говоря, в способности СУБД понять это и использовать результат предыдущего запроса их кэша в качестве основы для второго. Можно было бы и на массивах это сделать, но с объектами как-то удобнее в плане автодополнения, навигации, рефакторинга и т. п. в IDE.

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

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

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


а размазывание логики одного уровня по разным слоям приложения.

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

В 90+% случаев об SQL даже не "подозреваю" :) И в 99+% случаев логики в СУБД не хранится, только данные их схема (если не считать логикой автоматически вставляемые ограничения по ключам). Формирование SQL запроса динамически я не отношу к размазыванию логики. А вот уже вьюшки хранить в базе — размазывание по сути.

Ну в целом я с вами согласен.

Можно было бы и на массивах это сделать, но с объектами как-то удобнее в плане автодополнения, навигации, рефакторинга и т. п. в IDE.

нормальная IDE это все умеет и для SQL.

Я не про SQL, а про оперирование результатами запроса и(или) формирование его параметров.

А просто мэппер не подходит? без обратной конвертации (для этого ORM есть).

Просто мэппер — это половина ORM (или ORM в режиме read only)?

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

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

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

Для формирования запроса можно использовать QueryBuilder.

Редкий QueryBuilder позволяет формировать вложенные запросы...

Ну те что мне доводилось использовать это умели.


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

Что такое "типовые объекты-строки"?
Если у нас данные о сущности хранятся в разных таблицах, то есть связи.

Абстрагирование оно получается из-за архитектуры — потому что orm должна поддерживать много баз и она строит запрос сама — в результате получается что все равно делается драйвера с одним интерфейсом.

Вот именно маппинг на объекты, а потом обновление всего после того как поменяли объекты.
Как-бы orm могут разбивать объекты и таблицы и автоматически подтягивать вложенные по связям.
В 90% легких запросов у нас уже есть готовый код, а в случае тяжелых — ну выявятся и оптимизируются.
ORM позволяет абстрагироваться от конкретной базы, но не от хранилища данных в целом. Вот только совместимые различия баз, вероятнее всего можно сгладить просто используя QueryBuilder, который будет переводить унифицированные запросы в специфичные.
По поводу маппинга на объекты, а точнее управления их состоянием, может быть, вот только польза от этого видна лишь в stateful приложениях. На PHP, где 90% эндпоинтов либо только читают из базы, либо только пишут в неё, держать пул объектов и следить за их идентичностью (IdentityMap) бесполезно.
Конечно, бывают случаи повторного чтения/записи, сам с таким сталкивался. Но бывает это редко, и наверное оно не стоит того чтобы постоянно иметь немалый оверхед от ORM.
Да, ORM предлагают красивое решение для простых случаев. Но как только начинается что-то посложнее, начинается борьба с ORM, когда ты понимаешь как можно написать запрос на sql, но ORM заставляет тебя совать друг в друга лямбды, которые строят куски запросов с помощью того-же QueryBuilder'a.
ORM позволяет абстрагироваться от конкретной базы, но не от хранилища данных в целом.

Позволяет и от хранилища в целом, если приложение сильно не завязано на конкретную ORM. Вот давеча вынес модуль из монолитного приложенияс активным использованием Doctrine в REST-like HTTP-сервис почти без изменения остального кода. Основные изменения (кроме собственно выделение модуля в отдельное приложение т. п.):


  • замена class DoctrineCashflowRepository implements CashflowRepositoryInterface на class HttpCashflowRepository implements CashflowRepositoryInterface
  • удаление из контроллеров $om->flush();
  • замена Doctrine relations на ручное заполнение и, самое сложное, сохранение с помощью нового сервиса (есть идея написать свою имплементацию доктриновского ObjectManagerInterface с использованием её же UnotOfWork, IdentityMap и т. д., но надо разобраться стоит ли овчинка выделки и может имеет смысл собирать информацию из нескольких источников исключительно на этапе отдачи ответа клиенту )
Это заслуга не столько ORM, сколько того, что вы заранее ввели абстракцию в виде интерфейса.
Того же эффекта можно было добиться и без ORM, главное тут — чтобы клиентский код обращался строго к интерфейсу и не знал подробности того как этот интерфейс работает внутри.

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

UFO just landed and posted this here
Миграции к ORM отношения не имеют. Механизм миграций может быть реализован в рамках ORM, как и построитель запросов (для сахара, да и для защиты от дурака), но это не значит, что они не могут жить отдельно от него.

Смотря с какой стороны смотреть. Просто как инструмент накатывания (и откатывания) последовательных инкрементных изменений на схему базу — не имеют. А вот если говорить об автоматической генерации SQL-кода этих изменений на основании различий в двух версиях декларативного описания схемы базы, то можно использовать для этого заметную часть универсальных ORM-библиотек: генерация SQL-кода на основе изменений в графе объектов — это одна из двух основных задач ORM.

ORM, а точнее популярные ORM-библиотеки всё же делают упор на работу с графами объектов данных и соответственно на генерацию DML запросов.
Генерация же DDL запросов, как и их применение к базе это задача механизма миграций.

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

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

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

Согласен. Но тогда получается, что люди выбирают не столько ORM, сколько хорошо притёртый набор библиотек в составе ORM. Что же тогда отличает ORM от набора библиотек?

Именно. Многие люди основной функцией ORM (по крайней мере ActiveRecord или DataMapper) пользуются как небольшим бесплатным бонусом, используя объекты, контролируемые ORM, просто как структуры данных.


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

Маппинг строк таблицы на объекты? Ну тоже не оч. Зачастую бизнес-сущность хранится сразу в нескольких таблицах, и вместо того чтобы оперировать одним объектом, который мы вручную написали как нам надо, мы жуём кактус, состоящий из типовых объектов-строк.

ORM как раз пзволяет представлять «несколько таблиц» как один объект. По крайней мере (не упомянутый в статье) AR из Yii2. И работать с результатом как с одним объектом, а не городить огород из вложенных циклов.
В целом же, как верно заметили выше, ORM очень экономит время на простых (и, как показывает практика, самых частых в написании) запросах типа CRUD. Мне не надо думать как там называется таблица, в какой схеме/базе она лежит, какие поля там PR… я просто вызываю метод delete/save у объекта…
Вложенные циклы устраняются не использованием ORM, а использованием объектов для выстраивания структуры обрабатываемых данных.
Не думать как там называется таблица можно введя дополнительный слой абстракции над слоем, который выполняет запросы к базе. Более того, выделение слоя, который выполняет запросы к базе, позволяет таки… выполнять запросы к базе. Т.е. ты пишешь sql запрос, и тебя совесть не мучает за то что ты делаешь хак в обход ORM.
Мне кажется, лучше руками писать простые запросы и иметь возможность так же быстро написать сложный, чем экономить время на простых запросах и безбожно тратить его на сложных.
Мне, возможно, не хватает какого-то опыта с ORM, но что мешает писать sql запрос через ORM и чтоб при этом «не мучала совесть»? В том же упомянутом Yii2 — ничто не мешает подсунуть голый SQL, если требуется, при этом пользоваться фишками ORM, если требуется (indexBy, подключение к базе, параметры запроса, из того что первое пришло в голову). Не понимаю что мешает и «экономить время на простых запросах» и при этом не париться со сложными.

Вводить дополнительные сущности и изобретать заного то что уже много лет используется в проде — это по меньшей мере странно, и как минимум — контрпродуктивно. Введя «дополнительный слой абстракции над слоем», нужно понимать что этот код может в итоге поддерживать не разработчик который напридумывал «выделение слоя, который выполняет запросы к базе», то есть нужно писать документацию и комментировать каждый шаг. и всё это вместо того чтобы НЕ использовать ORM потому что… почему?
Когда вы используете ORM и пишете SQL код в сложных случаях, вы теряете независимость от конкретной СУБД, т.е. один из «плюсов» использования ORM.
В случаях, когда вам необходимо абстрагироваться от СУБД, вам придётся вводить слой абстркации. Да и без такой необходимости, выделение работы с базой делает код чище.
«Фишки ORM» совсем не фишкки ORM:
  • indexBy — работа с коллекциями
  • подключение к базе — фишка драйвера, такого как PDO, ну или библиотеки, облегчающей работу с базой
  • параметры запроса — аналогично

то есть нужно писать документацию

Да ладно? Вы считаете что это оверхед? А ничего что документация должна быть в любом случае?

Я не приводил аргументов за «независимость от конкретной СУБД», не считаю вообще это плюсом.
indexBy — работа с коллекциями
Эту работу за меня сделал ORM, я этого не писал, мне не нужно этот код поддерживать, например.
подключение к базе — фишка драйвера, такого как PDO, ну или библиотеки, облегчающей работу с базой
Этой библиотекой и является ORM. Просто библиотека-надстройка над PDO.
Да ладно? Вы считаете что это оверхед? А ничего что документация должна быть в любом случае?
Кол-во документации различается, нет? Таки да, я считаю оверхэдом писать документацию в своём проекте для стандартных модулей моего фреймворка. Или вы мне сейчас ещё скажете что и фреймворки — зло?

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

По поводу документации. Так или иначе, сущности и логику работы приложения как-то описать надо, не важно, используете ли вы ORM или пишете запросы руками. Слой абстракции не подразумевает создание собственного сложного фреймворка для доступа к данным, достаточно писать запросы не в самом объекте предметной области, а в отдельном классе, названия методов которого вполне самодокументируемы по названию. Что может быть непонятного в методе findNewsByTitle(title) внутри которого одной-двумя строчками делается запрос и отдаётся массив объектов? Для этого не требуется обширной документации.

Ну и напоследок про sql через ORM.
Всё-таки ORM сам по себе является слоем абстракции. Работа с базой должна быть скрыта в этом слое. А когда мы пишем sql через ORM — мы вытаскиваем работу с базой наружу.
Что может быть непонятного в методе findNewsByTitle(title) внутри которого одной-двумя строчками делается запрос и отдаётся массив объектов?

Так этот метод и есть часть механизма ORM — он же осуществляет маппинг реляционных данных на объекты?


А когда мы пишем sql через ORM — мы вытаскиваем работу с базой наружу.

Мы пишем sql не через ORM, а внутри ORM. Клиенту того же репозитория Doctrine без разницы используется внутри репозитория полностью автоматическая генерация запросов средствами Doctrine, ручками написанный DQL-запрос, или вылизанный SQL-запрос, если результат один и тот же.

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

Будет более чем уместно, если правильно сделан маппинг (M) между базой данных и объектами.

Если у вас маппинг 1:1 то все хорошо и ORM уместен. Иначе приходится делегировать маппинг SQL запросам или делать это на уровне приложения получая риск проблем с производительностью.

Я бы сказал с точностью до наоборот. Если в приложении и так используется реляционная модель (пускай и на объектах), то толку от ORM будет мало, маппинг 1:1 простой. ORM хороша именно когда маппинг не 1:1, например в случае связей 1:N. В реляционной модели результат это N сущностей одного типа, половина (в широком смысле слова) значений в которых дублируется, а объектной (графовой) — одна сущность одного типа, связанная с N сущностями другого типа без дублирования. А уж в связи N:M в реляционной модели мы получим N*M сущностей одного типа, а в объектной — N+M.

Не совсем понимаю, что вы подразумеваете под маппингом 1:N. Можете привести пример? Могу лишь предположить, что вы получаете от ORM «N» промежуточных объектов и агрегируете их в один объект бизнес модели

Маппинг 1:1 — это когда строка таблицы (или даже шире — результата запроса) маппится в объект с однозначным соответствием столбцов таблицы и свойств объекта.

Однако, если данные хранятся не так как обрабатываются, то использование ORM, скорее всего, будет неуместно.

А как же тогда подход EventSourcing, который вполне себе нормально ложится на ORM?
который вполне себе нормально ложится на ORM?

Это как простите?


При event sourcing у нас нет ORM. Ну то есть она может быть где-то на стороне read model если вам так удобно, но это уже никакого отношения к event sourcing не имеет.


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


ORM же это когда у нас есть реляции (таблички) и мы их мэпим на объекты. Операция полностью относится к слою персистентности и абстрагирована от бизнес логики.

Ну лично мне удобно было вбросить doctrine для:
а) DataMapping
b) Абстракции от СУБД(ибо было большое желание поиграть с разными)

Но никакого мэппинга не происходит

Мне кажется вы заблуждаетесь. Далеко не всегда нам нужны конечные состояния объектов. Часто бывают ситуации, когда я хочу анализировать массивы эвентов. И часто бывает ситуация, когда хочется анализировать их в PHP/Python-коде, работая как с объектами. Они иммутабельны, но это вовсе не означает, что я после сохранения не захочу мапить их на объекты.
Ну и плюс построение read-model из событий все же является частью подхода под названием event-sourcing, а там ORM — очень удобно.
Далеко не всегда нам нужны конечные состояния объектов.

А я где-то говорил про конечное состояние? В этом же и прелесть ES, проигрывайте события, генерьте ветки событий что бы посмотреть "а что было бы"… но это все равно не мэппинг а вычисление. Вопрос зоны ответственности.


И часто бывает ситуация, когда хочется анализировать их в PHP/Python-коде, работая как с объектами.

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


Ну и плюс построение read-model из событий все же является частью подхода под названием event-sourcing, а там ORM — очень удобно.

вы можете и без event sourcing строить read model. Как никак ES не обязательная штука для CQS/CQRS. Потому если уж обобщать случаи где ORM удобно — то это OLTP. Когда нам нужно поработать с небольшим графом объектов а потом сохранить этот граф в базу.

ORM же это когда у нас есть реляции (таблички) и мы их мэпим на объекты

Я думаю, что про реляции тоже не совсем правда. Буква R в аббревиатуре скорее характеризует тип СУБД нежели обязательное требование к наличию реляций. Иначе как же быть с ODM, который по сути является тем же паттерном. А ведь в документо-ориентированных БД реляции не такое уж и частое явление.
Буква R в аббревиатуре скорее характеризует тип СУБД нежели обязательное требование к наличию реляций.

Вопрос терминологии. Я не случайно добавил пометку рядом со словом "реляции" — таблички.


В целом все так, R про тип СУБД. Что мол мы берем нормализованное представление данных и мэпим их на наши объекты.


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

но вы же заметили что буковки R там нет. В чем разница между ODM и ORM так это в том что в первом нам не нужно (как правило) менять структуру. Мы храним документы как отображение состояния наших агрегатов и все. В ORM же мы получаем в результате SQL запроса денормализованные данные и должны их замэпить, что уже задачка посложнее и сам мэппинг происходит сильно по другому. Потому я бы все же разделял эти два паттерна, хоть они и очень похожи по назначению.

ORM вполне может быть и при ES, как минимум, для хранения собственно событий, плюс снэпшотов агрегатов и прочих сущностей, в том числе и для write model. Число релейшенов наоборот растёт, добавляется как минимум SomeAggregate one-to-many SomeAggregateEvent. Да и в целом, ES не подразумевает хранение всех событий в одной табличке. Вполне может быть множество типов (читай — классов) событий, как-то маппящихся на базу.

UFO just landed and posted this here

От задач зависит. Поиск по данным события вполне распространенный кейс.

Да, но опять же для этого удобнее одна коллекция ивентов. Нет?

Вполне может быть множество типов (читай — классов) событий, как-то маппящихся на базу.

если у вас есть несколько абсолютно не пересекающихся стримов событий — то да. Но хранить их по типам плохая идея. Как минимум потому что выполняться они как один неделимый стрим событий.


для хранения собственно событий

я бы не рекомендовал использовать реляционные базы для этого для начала. Можно но не очень много смысла.


плюс снэпшотов агрегатов и прочих сущностей

ну вот тут вполне можно, но я бы тут использовал все же ODM и превращал бы весь процесс в сериализацию.

если у вас есть несколько абсолютно не пересекающихся стримов событий — то да. Но хранить их по типам плохая идея. Как минимум потому что выполняться они как один неделимый стрим событий.

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


я бы не рекомендовал использовать реляционные базы для этого для начала.

Хранилищ, заточенных на ES, раз-два и обчёлся (по крайней мере FOSS), да и те сыроваты похоже. По любому нужно брать какую-то существующую модель и приспосабливать её к ES. В пользу реляционной — огромная и отлаженная экосистема. Особенно это важно, если происходит плавный перевод существующего продукта на ES.

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

Простите, а откуда там "наследования на таблицы" и прочие мэппинги? Мы храним события. По сути что оно будет — айдишка события, айдишка агрегата возможно, тэймстэмп, тип, и жирный json с пэйлоадом. Далее по вкусу. Никаких связей с другими табличками. Никаких наследований и прочее.


Хранилищ, заточенных на ES, раз-два и обчёлся да и те сыроваты похоже.

Тут вопрос что вы подразумеваете под "заточенных на ES". Я правильно понимаю что это некая штука которая помимо хранения ивентов возьмет на себя построение проекций, снэпшеты и т.д.? знаю одно неплохое — https://geteventstore.com/ но как по мне это не очень удобно.


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

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


И маппинг может быть (в теории) сколь угодно сложным — сводить несколько записей разных таблиц в объект, разбивать одну строку таблицы на несколько объектов и т. п.


И не надо путать ORM как архитектурный паттерн с универсальными ORM-библиотеками. Если вы в приложении по результатам SELECT-запросов формируете граф объектов, а по результатам изменения этого графа формируете INSERT/UPDATE/DELETE-запросы, то вы уже используете паттерн ORM в той или иной разновидности. Реализация сторонняя или своя, универсальная или только то, что нужно, с абстракцией от СУБД или без — это нюансы.

А вот интересно, какие вообще плюсы от использования ORM?

Смотря какой.


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

Если вы про приемы вроде наследования таблиц или еще чего такое — то это все как бы умеют ORM. А если вы про случаи когда одну сущность мы по каким-то причинам поделили на две таблицы — я не знаю зачем так делать. Этим мы только себе в ногу выстрелили.


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


Для меня профит от ORM в операциях на запись!, когда у нас есть небольшой граф объектов и мы что-то с ним делаем. В этом случае на уровне приложения у меня кучка объектов, которые обмениваются сообщениями. Вся логика спокойно покрывается юнит тестами и тд. Ну и за счет механизмов вроде unit-of-work я могу "закоммитить" изменения всего графа в базу. И это реально удобно и реально круто!


НО на выборки для операций чтения ORM не нужны. То есть я согласен с комментарием выше — любой список это вид репорта. Там ORM не нужны.

Прошу прощения за офтоп


А если вы про случаи когда одну сущность мы по каким-то причинам поделили на две таблицы — я не знаю зачем так делать. Этим мы только себе в ногу выстрелили.
Полностью согласен про выстрел в ногу, но как бы вы поступили в следующей ситуации:
у нас есть сущность "Экскурсия" со своим списком полей и реляций и мы её продавали как товар. Грубо говоря одна экскурсия — одна строчка из таблицы "excursion" + по одной/несколько строк из связанных таблиц.

Потом кто-то захотел ввести дополнительно сущность "Экскурсия с фиксированной датой" и эта сущность от базовой отличается только наличием полей "дата проведения" и "квота". Теперь у нас товар не просто строчка из таблицы "excursion", а строчка из таблицы "excursion_date" плюс привязанная к ней строчка из таблицы "excursion".


Т.е. просто для какого-нибудь процесса вывода информации а-ля "вывести в какие даты доступна экскурсия N" можно использовать простую реляцию вида $excursionWithDate->getExcursionDates(). Но если нужно к примеру добавить экскурсию в корзину — то мне уже нужен объект собранный из двух таблиц.


Как бы вы поступили в этой ситуации?
P.S. знаю что бизнес-объекты рассматривать как строчки из таблицы нельзя, это было сделано для наглядности.

пардон, с разметкой немного ошибся

По описанию "Экскурсия с фиксированной датой" должна быть наследником "Экскурсия", что легко решается нормальными ORM несколькими способами, в том числе созданием таблицы excursion_date с тремя полями, но по тому же описанию объекты класса "Экскурсия" — это не товар, а наименование товара, а "Экскурсия с фиксированной датой" — ограничения на покупку товаров данного наименования: есть запись с датой и квотой — есть ограничения, нет — нет. То есть не "Экскурсия с фиксированной датой", а "Квоты на Экскурсию по датам", ссылающаяся на "Экскурсия" и в логике добавления экскурсию в корзину добавляется проверка на ограничения, а так всё остаётся тем же самым.


Структура таблиц одна и та же, но вот маппинг их на объекты кардинально разный.

По описанию "Экскурсия с фиксированной датой" должна быть наследником "Экскурсия"

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


p.s. не люблю наследование.

Исхожу исключительно из описания:


Потом кто-то захотел ввести дополнительно сущность "Экскурсия с фиксированной датой" и эта сущность от базовой отличается только наличием полей "дата проведения" и "квота".

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

Возможно я не совсем понятно объяснил разницу между этими двумя сущностями. На живом примере:
Обычная экскурсия "Прогулочный маршрут по центру столицы" можно купить билет и использовать его 1 раз в любой день в течении полугода с момента покупки.


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


Контролем использования билетов занимается отдельная система, так что эта часть процесса в рамках задачи не важна

вот, чуть конкретнее, ок.


То есть наши "экскурсии" на самом деле ничем не отличаются. Просто для некоторых видов экскурсий у нас есть еще отдельно билеты с разными датами.


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

надо логику понимать лучше.

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

В книжках по DDD очень много внимания этому уделено) Разговаривайте со своими заказчиками больше. То что они сами, как правило, не знают свою предметную область — факт. И есть методы, благодаря которым и Вы и заказчик сформируете ее понимание. При том общее. И придете к единому языку и будет вам счастье

Понимать они понимают, обычно. По крайней мере если брать заказчика как организацию в целом :)


Проблема в основном в том, что либо представитель заказчика, который формирует задание не знает каких-то нюансов, особенно не формализованных типа "перед выставлением счёта Галя из бухгалтерии звонит в СБ и интересуется не будет ли проблем с финмониторингом", либо очень многое считает очевидным, не требующего отдельного пояснения, хотя бы различные правила округления при различных операциях типа "а мы покупаем или продаем?", либо и то, и другое :(

Экскурсия с фиксированной датой

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


Предположим что это не отдельная сущность а лишь опциональная характеристика экскурсии. Типа дэйт рэйндж за который оно действует. У остальных по умолчанию будет null-object с null-вым рэйнджем. Эту характеристику в силу ограничений СУБД я запихну в отдельную таблицу excursion_date. И в объектной модели будет соответствующая пропертя которую я буду использовать на запись.


вывода информации а-ля "вывести в какие даты доступна экскурсия N"

А тут я сделаю SQL запрос и замэплю данные сразу на DTO которое плюну во view. Если мне не надо сохранять изменения стэйта мне не нужен ORM. Хотя если под ORM мы подразумеваем именно паттерн а не какую-то реализацию — то этим мэппингом SQL -> DTO и будет заниматься мой ORM.

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

Жизненный цикл фактически один и тот же. На практике разница выливается только в два момента:


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

Предположим что это не отдельная сущность а лишь опциональная характеристика экскурсии. Типа дэйт рэйндж за который оно действует. У остальных по умолчанию будет null-object с null-вым рэйнджем. Эту характеристику в силу ограничений СУБД я запихну в отдельную таблицу excursion_date. И в объектной модели будет соответствующая пропертя которую я буду использовать на запись.

Т.е. вы предлагаете сделать все экскурсии "как бы" с фиксированной датой, но у обычных экскурсий вместо даты/квоты будет null-object?

Т.е. вы предлагаете сделать все экскурсии "как бы" с фиксированной датой, но у обычных экскурсий вместо даты/квоты будет null-object?

да, иначе мы нарушим LSP (если вдруг решили наследоваться). Да и с точки зрения отображения это будет проще и логичнее. А те экскурсии для которых нет фиксированных дат — ну они всегда будут возвращать true при вызове isAvailableAt какого-нибудь.


Если же есть какие-то другие детали — то их нужно учитывать и это может поменять мое мнение о том как это нужно реализовывать.


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

понял. спасибо за разъяснения.

Как на PHP не знаю, но на C# крайне полезными являются язык выражений, который ближе к реляционной алгебре чем SQL, и проверка корректности запросов компилятором.


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

Абстрагирование от конкретной БД в них так себе, да и кому оно надо?

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

Когда вы говорите про просадку перфоманса от WHERE IN или от *. То забываете упомянуть что обычно в приложении оперировать приходится не всеми хранимыми сущностями. Пагинация или бесконечная прокрутка или обработка чанками идет практически всегда. И эти факторы превращаются в экономию на спичках. А удобство и простота кода гораздо более приоритетные.

А так же что можно стандартные селекты (например книги со всеми данными кроме текста) зашить например в скоупах (в элоквенте) и "пользовательский" код в контроллере или сервисе будет простым.

Я никогда не говорил, что голые запросы надо делать прямо в контроллере. Нужно выносить, конечно. Просто сложные запросы легче конструировать на SQL
UFO just landed and posted this here
UFO just landed and posted this here

Выбор ORM — это выбор между способами отображения объектной модели данных приложения на их реляционное хранилище. Прежде чем выбирать ORM людям нужно думать, а нужна ли она им вообще, нужны ли им объектная модель одновременно с реляционным хранилищем. Оптимальным вариантом может оказаться отказ от объектной модели в пользу более классических структур данных (или наоборот, каких-то новомодных) или отказ от реляционного хранилища в пользу более подходящего под объектную модель.


А уж если выбрали объектную модель и реляционное хранилище, то остаётся, если хочется минимальной поддерживаемости кода, только выбирать между разными реализациями ORM, включая написания своей собственной, возможно универсальной, а может и тупо захрадкаженным вимператином стиле маппингом типа $contract->number = $sqlQueryResult['contract_number']

UFO just landed and posted this here

Вместо объектов — массивы, списки, хэш-таблицы и т. п.

UPDATE authors
SET name = 'Жорж'
WHERE id in (
    SELECT id
    FROM authors
    ORDER BY id DESC
    LIMIT 2
);

и
UPDATE authors
SET name = 'Жорж'
ORDER BY id DESC
LIMIT 2;

зачем подзапрос?
UPDATE… LIMIT не работает в посгресе, например
с посгрес не работал, не знал, моя ошибка значит.
Было бы интересно сравнить не с классическими ORMмами, а с (относительно) новыми решениями типа jOOQ, когда и гибкость SQL остается, и модели есть, и типобезопасность присутствует.
Насколько я вижу, JOOQ — это query builder. Т.е. по сути тот же SQL, только вместо пробелов скобочки и точки. Нет никакого абстрагирования от базы
UFO just landed and posted this here
Ну я ж и написал «типа jOOQ». Или под PHP такого не делают?

не надо путать DBAL и ORM. Если что-то не умеет мэпить результат SQL на объекты и обратно — значит это не ORM.

UFO just landed and posted this here
Я не могу согласится с тем, что jOOQ это query builder. Он в нем есть, но кроме него есть еще и модели, и кодогенерация и т.д. Это реализация object-per-table патерна. А такой код не похож на квери билдер, согласитесь:
BookRecord book1 = create.newRecord(BOOK);
book1.setTitle("1984");
book1.store();


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


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

Если нужно получить получить информацию одним запросом, то используем EntityRepository.

class BooksRepository extends EntityRepository
{

    public function getBooksWithAuthors ()
    {
    	 $result = $this->createQueryBuilder('u')
            ->select('u, a')
            ->leftJoin('u.authors', 'a')
            ->getQuery()
            ->getResult();

            return $result;
            
	}
}


Уточню, что authors — ManyToMany связь для Books и entity класс для книг создан правильно, а не так как в примере.

class BooksController
{
	$doctrine = $this->getDoctrine();
	$books = $doctrine->getRepository(Books::class)->getBooksWithAuthors();
}


И получаем все в одном запросе. Книги с их авторами.

dump($books)
> ManyToMany связь для Books и entity класс для книг создан правильно, а не так как в примере
а что именно неправильно?
Как по мне, SQL vs ORM некорректное сравнение у своего основания.

sql — это язык, ORM — это оверхэд над sql, как большинство языков это оверхэд над процессором.

Задача ORM — абстрагироваться от базы данных. Каждый объект ОRM служит определенной цели в бизнес-логике. Мы перестаем думать о базе данных в принципе, а начинаем мыслить объектом и целью для которой он создан.

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

Так что бы получить всех пользователей которые привязаны к одному банку мы сделаем, что-то вроде: $banks = $user->getBanks()->getUsers();

Получить адреса банка пользователя: $banksAddr = $user->getBanks()->getAddrs();

Теперь мы хотим, чтобы пользователь мог, что-то заказать с сайта, если у него указан хотя бы один банк:
if($user->hasBanks()){}


Так же, в каждый момент времени мы должны быть уверены, что кто-то не допишет sql, который уберет нам половину данных или наоборот запишет в базу потеряв в половину.
Или кто-то добавил обязательное поле в таблицу, чтобы все запросы работали корректно, нам нужно поправить 1 entity class и все запросы продолжат работать.

Всю ответственность за целостность данных берет на себя берет ORM.

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

class Book
{

  /**
     * @ORM\ManyToMany(targetEntity="Author", inversedBy="books", fetch="EXTRA_LAZY")
     * @ORM\JoinTable(name="books_author")
     */
    private $authors;
}

class Author
{
 /**
     * @ORM\ManyToMany(targetEntity="Book", mappedBy="authors")
     */
    private $books;
}


Теперь мы можем получить для книги всех ее авторов, а для автора — книги

$idBook = 10;
$authors = $em->getRepository('Books::class')->find($idBook)->getAuthors();


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

class createBook
{
function createAction(){
  $idAuthor = 10;
  $author = $em->getRepository('Books::Author')->find($idAuthor);
  $book = new Book();
  $book->setAuthor($author);
  $em->persist($book);
  $em->flush($book);

}
}

В моем примере все то же самое примерно, только в yaml, а не в аннотациях. Через консольную команды я создал yaml на основе foreign keys базы
Как по мне, SQL vs ORM некорректное сравнение у своего основания.

Согласен. Но только в этом.


ORM — это оверхэд над sql

ORM — это механизм прозрачной трансформации сущностей ООП-языка в язык sql и обратно. Да, универсальные механизмы вносят некоторый оверхед по сравнению с захардкоженными преобразованиями, но если для работы с данными в приложении мы выбираем объекты (особенно полноценные, а не подобные структурам C), а для их хранения SQL-базы, то нельзя говорить об ORM как об оверхеде — это необходимый для этого выбора механизм. Можно говорить о том, сколько приносят (и приносят ли) оверхеда конкретные реализации ORM по сравнению с идеальными, но не более.


Задача ORM — абстрагироваться от базы данных.

Всё же, абстрагироваться от реляционной сущности хранилища, а не от его наличия вообще.


Мы перестаем думать о базе данных в принципе

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

Задача ORM — абстрагироваться от базы данных.

тут могут быть недопонимания. Не от базы данных а в принципе от способа хранения данных. Разделение ответственности банальное. Это не означает что мы можем взять одну СУБД и заменить другой на раз два. Это не является целью.

Допустим, стоит задача обновить двум последним авторам имя на «Жорж».

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


Вот так, например

public function get2LastAuthors ()
    {
    	 $result = $this->createQueryBuilder('u')
            ->select('a')
            ->addOrderBy('a.id', 'DESC')
            ->setMaxResults( 2 );
            ->getQuery()
            ->getResult();
            return $result;
            
	}



class AuthorsController
{
	$doctrine = $this->getDoctrine();
	$em = $this->getDoctrine()->getManager();
	//используем QueryBuilder
	$authors = $em->getRepository(Books::class)->get2LastAuthors();
	//или так
	$authors = $em->getRepository('Books::class')->findBy([], ['id' => 'ASC'], 2);

	$name = "Жорж";
	foreach($authors as $val){
            $val->setName($name);
	}
	$em->flush();
}


Довольно негативная получилась заметка в отношении ORM) Давно работаю с ORM на Perl и в защиту этого подхода могу привести пример решения последних двух задач на местном ORM — DBIX::Class. Решения должны выглядеть примерно так:

* Книги с авторами (специфичный json_agg я заменил на GROUP_CONCAT)
$schema->resultset('Book')->search( undef, 
  { select => [
    'me.id', 'me.title', 
    { 'GROUP_CONCAT' => 'author.name', -as => 'authors_list' }
   ],
   join => 'authors',
   group_by => 'me.id'
  } );


* Правка имен авторов
$schema->resultset('Author')->search( undef,
  { order_by => { -desc => 'id' }, rows => 2 }
 )->update({ name => 'Жорж' });

Вот я не понял что предполагалось во втором примере сделать. Последним двум строкам поменять автора? И что сделает на практике ORM — два запроса или один? Когда это будет понятно? Не придётся ли включать вывод каждого сгенерированного запроса, чтобы понять что выполняется и не надо ли выполнить чистый SQL, не уйдёт ли на это всё сэкономленное время?


Была интересная статья — сравнение перловых ORM, в частности Rose::DB::Object и DBIC. По производительности разница была до 20-ти-кратной. То есть SQL(Rose к нему очень близок) в 20 раз быстрее некоторые операции выполняет. Это цена ОРМ, не считая времени на поиск проблем при разработке.

Мне интересно посмотреть на ORM если нужно сделать временную таблицу в БД, а потом с ней дальше то то делать)
:) Да много чего еще можно на SQL.
Можно функций понаписать и в запросе к полям использовать эти функции прямо в запросе.
ORM такого не может да, но он не для этого.
UFO just landed and posted this here

Как раз для CRUD ORM нафиг не нужен, особенно в PHP.

А какие PHP-объекты этой таблице будут соответствовать?

SQL vs ORM — из одной крайности в другую :)

Я на ORM и генераторы запросов смотрю как на хорошую подсказку IDE и компилятору, что у нас из базы возвращается не какой-то сферический ResultSet, а некоторый объект с вполне определёнными полями и методами.
Это отсекает такой класс ошибок, как написать запрос, отладить его в IDE для БД (она как правило отдельная), перенести его в код и потом с удивлением ловить ошибки в рантайме, из-за того, что поле в результате или называется не как надо, или просто забыли добавить поле в выборку.
Хорошие IDE/ORM ещё могут слазить в базу за схемой, проверить имена маппингов, сгенерить миграции итд.


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

Кейс 1 и 2 в Yii2 такой же как в ларавеле, а вот 3 и 4:

Кейс 3 Yii2
$books = Book::find()
    ->with('authors')
    ->all();
    foreach ($books as $book) {
        print $book->name . "\n";
        foreach ($book->authors as $author) {
            print $author->name . ";";
        }
    }

Тут главное что будет всего два запроса, вне зависимости от числа книг. Но тут конечно ещё должен быть прописан relation через `viaTable()`.

Кейс 4 Yii2
Author::updateAll(
    ['name' => 'Жорж'],
    ['id' => Author::find()->select('id')->orderBy(['id' => SORT_DESC])->limit(2)->column()]
);

UFO just landed and posted this here

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

UFO just landed and posted this here

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


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

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

UFO just landed and posted this here
где без ORM никуда и осознаю весь дзен ORM.

Мне кажется что весь конфликт в разном трактовании этих трех букв. Что вы подразумеваете под ORM? Ибо из того что вы пишите выглядит так как будто бы это некий монстр вроде Hybrenate который должен использоваться всегда и везде без компромиссно и без учета того что он как бы позволяет мэпить результаты SQL на объекты.


Я, признаюсь, плохо отношусь к технологиям, которые нельзя объяснить простым языком.

Есть хороший доклад Грэга Янга под названием "8 lines of code". Вам должно понравиться.

UFO just landed and posted this here

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

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

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

Почему вы отбрасываете вариант "самому сразу написать на ORM как надо"? :-)

Если только для себя и только для конкретного проекта/задачи и эффект от этого понятен (упростится написание кода и т.п.), то можно. Но как только эту ORM кто-то другой попробует применить для своих целей, он столкнется с этими же проблемами. Т.к. "как надо" — это не какая-то математическая правильность, а лишь факт соответствия конкретной ситуации.

То же самое я могу сказать и вам.


А это лишняя работа, проще самому сразу написать на SQL "как надо"

Если только для себя и только для конкретного проекта/задачи и эффект от этого понятен (упростится написание кода и т.п.), то можно. Но как только этот запрос кто-то другой попробует применить для своих целей, он столкнется с этими же проблемами. Т.к. "как надо" — это не какая-то математическая правильность, а лишь факт соответствия конкретной ситуации.

Наверное все популярные универсальные ORM-библиотеки появились из "для себя" и у них есть вполне определенная область применения для реализации как функциональных, так и нефункциональных (прежде всего ресурсных) требований по взаимодействию приложения с СУБД. Если кто-то упорно пихает ORM туда, где она даже себя не заявляет как решение, то кто в этом виноват?

С ORM всё кажется красиво, пока есть четыре таблички. Как только их 400, сразу возникает проблема автоматизации генерации описания для ORM, потом оказывается что генератор делает не всё так как надо и после генерации приходится что-то поправлять, потом оказывается что всё-таки надо указывать 10 нужных столбцов из сотни в таблице, потом вдруг появляется какая-то непонятная ошибка, потом вдруг обнаруживаешь что ORM работает в 20 раз медленнее чем SQL, потом через пару лет оказывается что современныый обновлённый ORM совсем не так совместим со старым… и появляются мысли что зачем я вложился в этот ОРМ? Он требует знать всё об ОРМ, всё об SQL, а в качестве плюшек — только синтаксический сахар, так как обычный SQL на самом деле выдаёт точно такой же результат — как видно из примеров выше — и требует примерно столько же кода. Причём диалектов ОРМ много, а SQL один.


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

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

В качестве основной плюшки — маппинг объектов на базу и наоборот. И если на объекты с базы ещё можно как-то маппить 1:1 малой кровью чем-то вроде PDOStatement::fetchObject, то для сохранения объектов вам понадобится ORM, чтоб приложение было мало-мальски поддерживаемым.

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

В вызовах типа find/get (с базы на объекты) и save/persist/flush (с объектов на базу).


О "тут же изменится" в целом речи нет, с одной стороны, с другой — в рамках ORM и сопутствующих технологий типа UoF, IdM и т. п. предполагается по умолчанию, что в базе не может быть изменений неинициированных ORM и каждая сессия работы СУБД выполняется в отдельной транзакции.


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

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


Мой опыт говорит как раз обратное — когда таблиц в базе 400, только ORM и позволяет не сойти с ума.

только ORM и позволяет не сойти с ума.

тут скорее разделение ответственности, но ORM как раз об этом.

Как-то написал что-то типа ORM для себя, потому что надоело писать сотни одинаковых запросов. Про то как оно назывется даже и не знал тогда, что вылилось в велосипед. Просто сделал класс, представляющий таблицу в базе, где экзепляр класса — это запись в таблице, со стандартными функциями получения списка/удаления/сохранения, а так же функции построения кусочков sql-запроса (типа where, order by), которые можно переопределить. Наследуясь от этого класса можно переопределить таблицу, особенности полей и добавить методы с какими-то нетипичными/оптимизированными запросами. А сам класс-предок использует позднее связывание и строит sql с учетом особенностей потомка. Для больших результатов делал ленивую подгрузку (доп. класс с интерфейсом массива). В итоге удобно получилось и намного меньше писанины. С голым SQL теперь сталкиваюсь только для необходимой оптимизации и сильно нестандартных запросов. В общем быстрее получается написать свой кустарный ORM, чем писать сотни однообразных SQL-запросов и методов.
> сотни однообразных SQL-запросов и методов.
Я посмотрел запросы в одном большом старом проекте, и знаете, там нет однообразных SQL-запросов. Т.е. если мы берем список юзеров, то обязательно с какой-то статистикой и т.д. На 90% запросы кастомные и не очень простые
В общем конечно же зависит от проекта. У меня это были в основном CRUD с фильтрами. А для отчетов конечно же отдельные классы с SQL. Тут ORM лишнее, согласен.

Результаты SELECT-запросов представлялись в виде объектов предметной области? UPDATE/INSERT-запросы зависели от таких объектов? Если да, то просто у вас была своя ORM, которая маппила объекты на SQL и обратно.

UFO just landed and posted this here
Cистема — что-то между CRM и 1С. Cущностей то около пол сотни. А вот запросов на каждую по несколько. Чтобы не писать генерацию SQL на каждый случай, зависящий от аргументов, я сделал один генератор SQL, который бы подходил в большинстве простых случаев.
По мне, так это как взять крутую и полезную штуку, которая имеет свои проблемы, выкинуть и оставить только проблемы.
Удобство написания кода и его читаемость безусловно важны, но как быть с производительностью7 Честно говоря прежде всего анализа производительности ожидал, когдда переходил сюда по ссылке.
О подобном дискутировали с michael_vostrikov в моей статье «Реализация бизнес-логики в MySQL»
https://habrahabr.ru/post/312134/#comment_9850218
с примерами кода и оценкой производительности

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

Скорее наоборот для большинства приложений: натянуть императивные команды SQL на декларативно описанный класс.

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

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

Но нам так и так надо на выходе получить какой-то граф, нормализовать денормализованный результат SQL запроса. Будем мы гидрировать объекты сущности или использовать какие-то динамические структуры — это уже детали.

Перефразирую: построение сложного графа может занимать значительно больше времени чем выполнение простого запроса :)

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

Проблема ОРМ что он пытается натянуть ООП парадигму на декларативный язык SQL

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


Как пример — например у нас есть данные и все хорошо. Но вот нам надо составить по этим данным граф связей кто с чем. Что проще:


  • извращаться с процедурами и SQL что бы добиться желаемого
  • добавить доменные ивенты в наши объекты по которым мы будем собирать проекцию данных в neo4j какой?
Сила СУБД как раз не просто в хранении данных, а именно в их обработке. И на мой взгляд, язык SQL идеально для этого подходит. Это как раз язык манипулирования и обработки данных, специально для этого созданный.
Перекладывая функции СУБД в приложение посредством ОРМ, мы потенциально роем себе могилу.

Про граф связей и в чём извращение с SQL не понял, но в БД для этого есть внешние ключи.
Сила СУБД как раз не просто в хранении данных, а именно в их обработке.

агрегации. С этим согласен. Все остальное, в том числе поддержание консистентности и т.д. тоже но только пока у нас данные влазят на одну машину. А далее это тупой сторадж. Вы можете использовать функционал СУБД для упрощения жизни, все что связано хранением данных. Конвертирование одного представления данных в другое например, вьюшки те же… но бизнес логике там не место.


Перекладывая функции СУБД в приложение посредством ОРМ, мы потенциально роем себе могилу.

Мы роем себе могилу когда доводим все до абсолюта. А так — все прекрасно. Юзаем ORM когда задача подходит под OLTP, и не юзаем когда не подходит.


Про граф связей и в чём извращение с SQL не понял, но в БД для этого есть внешние ключи.

У нас есть таблица с пользователями. Есть внешний ключ на introducer_id. Задача — вам нужно отобразить для анализа полное дерево рефералов. Количество уровней не ограничено.


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

Прям представил себе проект, где несколько десятков разработчиков, сотни сущностей и т.д. и т.п.
Интересно будет искать SQL код, писать его и еще что бы все разработчики отлично знали SQL.
Как по мне, ORM для больших проектов просто необходим. Собственно как мы и делаем.
Программисту проще оперировать объектами чем запросами. А маппинг поможет в сложных ситуациях.
Интересно будет искать SQL код

Если эти десятки разработчиков нормально умеют в декомпозицию то проблем не будет.


Программисту проще оперировать объектами чем запросами.

В каких-то случаях — да. А в каких-то декларативный SQL намного удобнее. Репорты — хороший пример.


А маппинг поможет в сложных ситуациях.

и будет вставлять палки в колеса если слишком универсальны и мы натыкаемся на специфику.

Да, про палки согласен.
При реализации DDD/CQRS/CommanBus… etc. очень много времени ушло на доработку архитектуры по работе с маппингом/доктриной и всем слоем персистенций. Коммиты отправили им.
В конечном счете оно того стоило.
очень много времени ушло на доработку архитектуры по работе с маппингом/доктриной и всем слоем персистенций.

Либо вы делали просто command bus (без CQRS) то скорее всего у вас могли быть проблемы со слоем персистентности. А с CQRS же мы бы имели чисто объектную модель которая вычисляет свой стэйт по ивентам (делаем мы event sourcing или нет — это детали, я сейчас в общем про eventual consistency). То есть в модели на запись у нас ORM по сути нет. Есть только ивенты.


Далее эти ивенты должны ловиться отдельными штуками и писаться с тем представлением данных которое вам нужно для конкретной задачи. Иначе толку от CQRS нет.


Но все же интересно что именно вы дорабатывали.

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

А вакансий на программистов со знанием популярных ORM-библиотек становится все больше. Более того, эти знания подразумеваются, даже если ORM не упоминаются: пишут Symfony — имеют в виду Doctrine, пишут Laravel — имеют в виду Eloquent, пишут Yii — имеют в виду ActiveRecord, пишут Rails — имеют в виду ActiveRecord (но другой), пишут .Net — имеют в виду EntityFramework, пишут Spring — имеют в виду Hibernate и т. д. Да и просто, если упоминается какой-то ООП-язык типа PHP, Ruby, Java, C#, C++ и какая-то SQL-база в одной вакансии, то чаще всего имеется в виду, что соискатель должен уметь отображать объектную модель на реляционную и обратно, а не редко и без популярных универсальных ORM-библиотек, то есть должен уметь написать свой ORM.

ORM — зло, т.к. пытается «впихнуть не впихуемое».
Т.е. создать «универсальный преобразователь» из РМД в ООМД.
Пока модели простые, все замечательно, по мере усложнения модели начинают вылазить не стыковки.
Т.к. бизнес сущность обычно не может быть выражена один в один в виде таблицы РМД.

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

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

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

Для этого и нужны, прежде всего, ORM, чтобы выражать не один в один. Использовать универсальный преобразователь as is, расширять его для частных случаев или писать свой неуниверсальный — это вопрос выбора преобразователя, а не вопрос его необходимости. Необходим он стал, когда вы выбрали использовать РМД и ООМД не просто одновременно, а отображая друг в друга.

ORM и SQL это разные инструменты для работы с данными. Соответственно, применять их надо по назначению. Строить отчёты и делать выборки, это задача SQL, для этого он разработан. ORM нужен для записи и DDD.
Read model — SQL, write model — ORM.
Мне кажется автор, не много напутал в понятиях. Тут же не используется голый sql а все примеры на pdo и специфичных ORM для каждого фреймворка. Он не делает escape_string, и прочее он все те же самые функции которые делает orm для каждой специфичной базы пусть то Oracle, Postgreql переложил на PDO. ГДЕ ТУТ ГОЛЫЕ ЗАПРОСЫ SQL. От куда беруться такие умники.

PDO работает с голыми SQL-запросами. Он не модифицирует запросы под базу, только предоставляет единый API для разных баз.

Вся беда возникает, когда инструмент используют не только когда он что-то облегчает, а для всего подряд, т.е. пытаются сделать из конкретной тулзы инструмент на все случаи жизни. А потом идут длинные споры про то что инструмент не идеален, потому что он может не всё на свете. Так не бывает. Автоматизация всегда подразумевает какие-то ограничения. Инструмент применяют там, где он удобен. Остальное — уже какое-то извращение.

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


Можно другие примеры рассмотреть.


// ------

select id, name, field1, field2, field3, field4 ...
from books
inner join author_book ab on b.id = ab.book_id
inner join authors a on ab.author_id = a.id
where <фильтр по authors>

Book::find()->joinWith('authorBook')->joinWith('authorBook.author')->where(<фильтр по authors>)

// ------

select id from books where id = X
// показать 404 если не найдено
// $queryString = <куча конкатенаций>;
update books set id = ..., name = ..., field1 = ..., field2 = ..., field3 = ..., field4 = ... where id = X

$book = Book::find()
// показать 404 если не найдено
$book->load($data);
$book->save();

// ------

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

Есть ещё одно решение по мимо ORM, не использовать ОО языки или просто ОО возможности — тогда оно (ORM) не нужно. Просто работаете со списками, отображениями, векторами.
Собственно ими(списками, отображениями, векторами) и представлены различные структуры в большинстве БД(как в реляционных так и nosql) и протоколы передачи данных(например JSON).

то есть полностью отказаться от преимуществ actor model и начать обмазываться монадами?

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

Она хоть и имеет общие с ООП корни но различий очень много

Ну как вам сказать, если мы пороемся и вспомним что подразумевалось под термином ООП (message passing, late binding) то как бы мы будем иметь просто определение actor model. Так что "общие корни" это мягко сказано.


хотя бы начиная с того что акторы обмениваются сообщениями(асинхронно) а объекты вызывают методы друг у друга(синхронно).

Да, сами акторы по хорошему существуют независимо друг от друга, но это совершенно не значит что они не ожидают ответа на свои сообщения. А вот блокировать им свое выполнение или нет — решать только им. Как никак event loop и все такое это не такая уж и редкость.


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


акторы это принципиально более высокий уровень абстракции

Примерно тот же уровень абстракции что и "все есть объект". То есть по сути никакой конкретики.


А тема о SQL vs ORM.

Нам удобно представлять систему как объекты и процессы (их можно функциями делать), а SQL это лишь декларативный способ работы с данными. То есть вот этот вот vs надо заменить на with и все счастливы.

Тащить низкоуровневую реализацию в приложение? Не, спасибо)

Тогда нужны другие *RM, потому что в СУБД данные представляются в виде отношений и кортежей.

Вижу в вашем возражении проблему в том что вы все же думаете в рамках ORM.
SQL запрос возвращает вам именно коттедж или их список а не граф. Что собственно имеет нативную поддержку во многих не ООП языках — другими словами тот же список кортежей.
Если вы хотите автоматического сохранения и другой обработки связанного графа сущностей как вам предлагаю некоторые ORM тогда да вам что-то близкое к просто SQL не подойдет. На моей практике данные встроенные возможности либо не эффективно работали и подходили для ограниченного набора задач или требовали написание обвязок на SQL-схожем языке для конкретной ORM, что сложнее обычного SQL запроса, который к тому же элементарно при написании проверить, минуточку тем же запросом в БД и не нужно писать сложные интеграционные тесты с моками (учтите ещё что под каждую ORM нужно изучать особенности её работы и ещё языка запросов).
А можно просто написать ещё ещё один sql запрос или объединить несколько уже существующих в транзакцию.

Я думаю в разных рамках. При фразе о "не ООП языках" я думаю, прежде всего, о языках типа C. И писал (да и пишу) достаточно сложную логикe на голом SQL как раз из-за неэффективности ORM на больших массивах данных.

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


Вообще, одна из проблем SQL на мой взгляд в том, что оно хоть и относится к реляционным базам данным, но нормально работать с этими реляциями не позволяет. Только при создании таблицы можно указать, что у нас есть связь с вон той. А данные по этой связи в запросе достать не получится (строка из другой таблицы как значение поля), и даже join по foreign key сделать нельзя. ORM восполняет эту часть, задавая связи между объектами.

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

Хм, да, многозначное слово. Я имел в виду выражения вида "one-to-many relationship".

А реляционные СУБД от relation в смысле relational algebra, которая определяет такие операции как джойн над реляциями, в "простонародье" таблицами. One-to-many relationship к реляционным СУБД напрямую не относится, всякие явно и неявно заданные relationship — это семантика, которой мы описываем схему базы, но в самой СУБД её нет. ORM, кстати, один из инструментов такого наполнения, с помощью которого мы описываем, что таблица такая-то содержит данные объектов такого-то класса, вторая таблица — второго класса, а эти классы (а значит и таблицы) связаны один-ко-многим через такие-то поля. foreign key — это лишь инструмент обеспечения целостности, защита от кривых рук разработчика или пользователя, а не инструмент задания связей.

Ну насчет ORM я примерно о том же. Только foreign key это именно что связь между данными. Первичный ключ это ссылка, обозначающая объект. Целостность это значит, что мы не можем ссылаться на объект, которого нет в другой таблице. Зачем бы нам нужна была целостность, если нет связи.


Про relation согласен, я неточно сказал. Но связь между данными это следствие декомпозиции, а значит напрямую относится к базам данных. Скажем так, SQL позволяет нормально работать с relations, но не с relationships.

"Ссылка, обозначающая объект" — это уже вкладываемая нами семантика, для базы это значение, идентифицирующее строку в таблице. То же с внешними ключами — ограничение foreign key лишь указывает базе данных, что значение в одном столбце одной таблицы должно быть из множества значений другого столбца другой (иногда этой же) таблицы и ничего более, что-то вроде динамического enum без всякой семантики связей, связь у нас в голове или в файле с маппингом ORM. Связь описывается в терминах базы данных, но сама база о наличии связи "не подозревает", она просто не оперирует подобными терминами.


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

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


Назовите строку не объект, а кортеж, принцип от этого не поменяется — ссылка, обозначающая кортеж. Оно же так и называется — ссылочная целостность.

Вот было же уже сказано, что для сколько-нибудь нетривиальных ситуаций <a href=«http://blogs.tedneward.com/post/the-vietnam-of-computer-science/>ORM не годится, исходя из общий соображений. Для бесхитростного CRUD'а — вполне. Остальное — перемалывание воды в ступе. Несвежей воды…

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

$books = \App\Book::with('authors')->get(['id', 'name']);

И с этим уже, похоже, ничего не сделать, по крайней мере я ничего не нашел.

with(['authors' => function($query) { 
    $query->select('field1', 'field2'); 
}]) ...

Ощущение, что автор просто не использует ORM в реальной жизни, отсюда и претензии к читабельности с неуклюжими примерами с заменой жоржей. Тот же eloquent, например, лично меня выручает тем, что берет на себя преобразование дат и массивов из бд, плюс, из коробки кидает события при сохранении и создании объектов (понятно, что это пара строк кода и это уже не область ОРМ, но не мог не упомянуть). чистый sql использую только в случае сложных запросов.

@Tantacula


Ощущение, что автор просто не использует ORM в реальной жизни

вы вообще query builder обсуждаете, это не ORM. И Eloquent не особо то и ORM.

Sign up to leave a comment.