Почему Doctrine ORM плохо подходит для PHP

Статья является переводом заметки Why Doctrine ORM is not suited for PHP от Lucas Corbeaux.

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

Doctrine вдохновлена Hibernate ORM


Вернёмся назад в 2000-е. Java очень популярен и одна из самых часто используемых Java-библиотек — это ORM Hibernate. Она поставлялась вместе с собственным языком запросов HQL и была горячо любима Java-сообществом. Hibernate помогала Java-объектам и реляционным таблицам сосуществовать вместе.

Doctrine был вдохновлён концептами Hibernate и хотел привнести их в мир PHP.

Различия между Java и PHP


Но PHP — это не Java, и из этого можно сделать один очень важный вывод. Java приложение живёт намного дольше, чем PHP запрос.

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

И именно поэтому ORM-паттерны в основном не нужны PHP. Поскольку HTTP протокол является протоколом «без сохранения состояния», вам не нужно поддерживать согласованность данных между между всеми вызовами.

Проблема сессий


Конечно же, вы можете сказать мне, что это неправда. Можно использовать сессии, чтобы хранить объекты между запросами, и тогда нужен способ, чтобы поддерживать их целостность. Это разумный аргумент. Вот только сериализация сущностей в Doctrine довольно каверзна и может привести с серьёзным проблемам.

Identity Map бесполезна в окружении «без сохранения состояния»


Identity Map — это часть Doctrine, которая поддерживает уникальность сущностей. Если вы, например, дважды запросите сущность с ID 4, то вы оба раза получите тот же самый объект.

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

  • Если ваш код хорошо структурирован, то вам и не понадобится дважды запрашивать одну и ту же сущность. Вместо этого вы воспользуетесь Dependency Injection;
  • Если вы измените данные, то это потому, что вы получили POST запрос. При получении POST запроса хорошей практикой считается сразу выполнить редирект. Нет никакой необходимости «обновлять» объекты.


Мне кажется, что доктриновская Identity Map полезна только в случае плохого дизайна. Или в очень редком и особенном случае.

UnitOfWork слишком переусложнён


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

Но с чем я не могу смириться, так это то, что большая часть сложностей возникла из-за «ленивой» загрузки и политик отслеживания изменений.

«Ленивая» загрузка бессмысленна


В окружении «без сохранения состояния», «ленивая» загрузка является плохой практикой. Мне нечего к этому добавить. Возможно, можно найти случаи, когда это немного увеличивало производительность, но это случается очень редко. Так почему же тогда это один из центральных концептов в Doctrine ORM?

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

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

Политики отслеживания изменений


Зачем нам нужна настолько сложная система, для обеспечивания целостности данных? Мы же в окружении «без сохранения состояния»! Если у нашего приложения хорошая архитектура, то данные не меняются в случайных местах. За исключением редких случаев (логирование, обновление времени последнего коннекта и проч.), нам просто нужно изменить данные по POST запросу. После чего мы незамедлительно редиректим пользователя на другую страницу.

Так зачем же нам нужна такая сложная система? Чтобы спрятать плохо спроектированное приложение?

В EntityManager слишком много магии


Глобальный EntityManager ведёт себя как паттерн Фасад, который работает с другими ORM-объектами. Это очень мощный инструмент, который может быть вызван в любом месте кода.

Однако, я свято верю в то, что в хорошо спроектированном приложении, EntityManager должен использоваться только в трёх случаях:

  • При загрузке для конфигурации
  • В ваших «фабриках», Service Manager или Dependency Injector, чтобы инициализировать необходимые объекты.
  • В некоторых репозиториях, если нужно создать SQL запрос


В других местах его использовать не нужно.

Всё же интересная и мощная библиотека


Для ясности, повторю во второй раз: я не говорю, что Doctrine ORM бесполезна или что вы не должны её использовать. Меня больше всего беспокоит то, что библиотека навязывает вредные привычки.

  • Entity Map позволяет разработчикам быть неаккуратными с инъекцией зависимостей и, соответственно, экземплярами сущностей.
  • «Ленивая» загрузка работает слишком магически и прячет проблемы производительности, пока не станет слишком поздно. Это особенно касается разработчиков с небольшим количеством опыта. Но опытные разработчики иногда тоже попадают в эту западню, ведь порой так заманчиво не утруждать себя использованием fetch-join.
  • EntityManager позволяет делать что угодно и где угодно. Удобно, но очень далеко от хороших практик.


Что дальше?


У автора оригинальной статьи есть материал для ещё нескольких статей о Doctrine. Например, о ODM расширении или о генераторах. Кроме того он принимает заявки в комментариях.
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 53
  • +6
    Но PHP — это не Java

    Но PHP это не Ruby, так что, в нем нет места RoR подобным фреймворкам? В то же время PHP намного ближе к Java или c# нежели к Ruby или Python.

    Поскольку HTTP протокол является протоколом «без сохранения состояния»

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

    UnitOfWork слишком переусложнён

    Если разработчик использует MySQL есть не нулевая вероятность того что он еще и бесполезен. А вот с СУБД типа oracle или postgresql можно спокойно делать persistence ignorance и с этого получать нехилый профит.

    ленивая» загрузка является плохой практикой… Так почему же тогда это один из центральных концептов

    А кто сказал что это центральный концепт? Это не так, это просто плюшка.

    Зачем нам нужна настолько сложная система, для обеспечивания целостности данных?

    Потому что Unit-of-work за нас все разруливает

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

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

    Однако, я свято верю в то, что в хорошо спроектированном приложении, EntityManager должен использоваться только в трёх случаях:

    А я думаю что только в одном случае: реализация репозиториев. Ну и да, еще flush транзакции по завершению запроса.

    быть неаккуратными с инъекцией зависимостей

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

    Я думаю все проблемы доктрины среди разработчиков вызваны двумя вещами:
    — не понимание концепций, которые несет доктрина (ActiveRecord скажем в этом плане намного проще, просто объектное представление табличек в базе, тогда как в контексте доктрины мы вообще ничего о базе в нашем приложении не знаем)
    — mysql, который сильно усложняет работу со своими авто инкрементами, алтернатива которой использование UUID, что не всем нравится. А реализация отложенного формирования представления очень геморная штука.
    • +1
      Мне тоже статья показалась несколько надуманной. Надо только отметить, что все претензии относятся к оригинальной статье, а не к автору перевода.

      Меня поразили еще самые первые строки: «I’m not saying Doctrine <...> shouldn’t be used. I’m just saying it’s not suited for PHP And this can lead to critical problems if misused».
      • 0
        ну скажем так, я согласен с последней фразой, обычно так и происходит. Доктрину пытаются использовать как любую другую ORM и натыкаются на вещи, которые кажутся странными. Например большинство воспринимает конструкцию:

        $em->persist($entity);
        $em->flush();
        


        как прямой аналог save в контексте active record (причем используют persist даже для обновления сущности), плодят транзакции (несколько flush-ей в рамках одного запроса, либо кастыли с if-ами повсюду как например в FosUserBundle).

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

        Но вот вопрос касательно насколько эти подходы подходят для PHP — я лично считаю что эти подходы как нельзя тут подходят. Другая проблема что, если сравнивать с .NET комьюнити, средний PHP разработчик не способен их осознать. Но это беда не инструментов, а умов.
        • +1
          Доктрину пытаются использовать как любую другую ORM

          Я думаю, это проблема не языка («it’s not suited for PHP»), а непонимание идеологии библиотеки. Скажем, я могу пытаться использовать Hibernate в ActiveRecord стиле. И получится примерно та же самая ситуация.

          Своих проблем добавляет и сама доктрина, вводя такие понятия как сущность, репозиторий

          Я думаю, это не доктрина добавляет проблем. Понятия «сущность» и «репозитарий» — базовые понятия в разработке. Эти понятия в той или иной форме используются в любой DataMapper ORM библиотеке. Да и в ActiveRecord ORM эти понятия тоже встречаются. И их непонимание — это не проблема библиотеки, а проблема квалификации разработчика.

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

          Опять же, это проблема не Doctrine, а квалификации разработчика, не так ли? Разве ActiveRecord ORM сложно использовать так, чтобы «все смешалось»?

          Но это беда не инструментов, а умов.

          Полностью согласен.
      • 0
        Там же написано — не Java, потому что запрос живёт мало, один пользователь сделал один запрос, он отработал один раз и умер, почистив всё за собой. Следующий запрос таскает заново все объекты из стороджей. Да, есть демоны, но доктрина там не годится, так как не поддерживает асинхронную работу с базой.

        Identity Map действительно антипаттерн, т.к. является обычным Registry и вместо явной передачи объекта по цепочки мы таскаем его из реестра. (Опять же в рамках умирающего php). IM при нормальной архитектуре заменяется DI. Хотя даже DI в таком контексте неоднозначен и является переусложнением :).

        Ленивая загрузка бесмыссленно, за исключением подтягивания связей, а в доктрине она превратилось в портал для ошибок и магической логики из-за чего я отказался от Doctrine 2, сейчас, наверное, они почти исправлены, но они были даже спустя 1.5 года после стабильной «версии».

        Data mapping тоже отдельная история, он вроде есть, но в результате сводится к прямому биндингу как при AR одна ентити, один аттрибут — одна таблица, одно поле. Сложный биндинг на разные таблицы и разные стороджи нужно делать костылями.

        Ну и самое важно это AR которое используется в современных фреймворках это вовсе не классичессичейский фаулеровский AR, а нечто большее. И при нем ничто не мешает использовать IM, UoW, LL, DM и городить DDD.

        p.s. Спасибо, что напомнили в очередной раз заняться Pg, раз сейчас в отпуске, а то до сих пор использую его на уровне mysql. Раз уж про обёртки над стороджем, возможно вам будет интересно github.com/sad-spirit/pg-builder github.com/sad-spirit/pg-wrapper
        • 0
          один пользователь сделал один запрос, он отработал один раз и умер

          И что? С архитектурной точки зрения разницы особо нет. По сути правильно на каждый запрос создавать свой IM и свой UoW, который дропается сразу после завершения запроса. Скажем если два запроса работают с одним и тем же экземпляром энтити, это крайне не правильно держать их в одном IM и уж тем более в одном UoW, так как это совершенно разные транзакции. Так что с концептуальной точки зрения я не вижу разницы, демон это или короткоживущий процесс. То что не поддерживает асинхроннности — ну и черт с ним, можно всегда держать пул процессов-воркеров, которые занимаются обработкой запросов, а ускорение получать за счет prefork этих процессов с уже проинициализированными сервисами дожидающимися запроса (можно на php-pm так делать уже сейчас).

          Identity Map действительно антипаттерн

          Identity Map нужен в контексте UoW, что бы не-было возможность иметь в системе две точки изменения одной и той же сущности как двух разных объектов. Момент с оптимизацией тут так, по мелочи.

          IM при нормальной архитектуре заменяется DI.

          Мы говорим о сервисах или о бизнес-объектах? Причем тут вообще DI? И вы говорите о принципе инверсии зависимости, IoC или DiC? Энтити это энтити, это просто бинес-объекты, набор данных и чуть чуть бизнес правил.

          Data mapping тоже отдельная история

          С версии 2.5 с этим все получше, можно объекты-значения юзать например и тогда энтити + граф вложенных VO = таблица.

          Сложный биндинг на разные таблицы

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

          вовсе не классичессичейский фаулеровский AR

          Можно подробнее? Как-то не особо AR в том же Laravel выходит за рамки концепций описанных у Фаулера или реализованных например в RoR.

          Раз уж про обёртки над стороджем

          Меня как-то квери билдеры не особо возбуждают, хотя спасибо.
          • 0
            Вот к примеру то, как я использую доктрину (максимально упрощенно)

            class ApiController extends Controller
            {
                function getListOfItems() 
                {
            
                    return $this->view($this->get('items_repository')->getListOfItems());
                }
            
                function createNewItem(CreateItemRequest $request)
                {
                     $item = Item::fromDTO($request->getDTO());
                     // мы просто добавляем объект в репозиторий
                     // разве это не прекрасно?
                     $this->get('items_repository')->add($item);
             
                     return $this->view($item);
                }
            
                function updateNewItem(UpdateItemRequest $request)
                {
                     $item = $this->get('items_repository')->findItem($request->getItemID());
                     if (!$item) {
                           throw new ItemNotFoundException($request->getItemID());
                     }    
                     
                     $item->updateFromDTO($request->getDTO());
            
                     // все что вернул репозиторий уже крутится в UoW так что нам не надо делать persist
                     // это сохраняет смысл репозиториев, там не должно быть метода update
            
                     return $this->view($item);
                }
            
                function removeItem(Request $request)
                {
                     $itemRepository = $this->get('items_repository');
                     $item = $itemRepository->findItem($request->get('id'));
                     // DELETE запросы должны быть идемпотентными
                     // если такого айтема нет, то все хорошо, а если есть - удаляем из репозитория
                     if ($item) {
                         $itemRepository->remove($item);      
                     }
            
                     return new Response(null, 204);
                }
            
                // вообще этот метод в листенере во фронт контроллере, но мне лень
                // вызывается аккурат перед отправкой ответа
                // так что если транзакция зафэйлится, мы сможем сгенерить ошибку
                function onResponse() {
                     // flush вызывается на любой запрос, даже на get
                     $this->get('doctrine.orm.entity_manager')->flush();
                }
            }
            


            По сути тот код который мне важен (бизнес логика) вообще ничего не знает о доктрине и о том как контролировать транзакции и прочей чуши. А теперь скажите, чем доктрина плоха раз дает нам это организовать с минимальными усилиями?
            • 0
              А в Laravel это решается через DI, мы просто биндим модель к запросу и дальше получаем уже готовые объекты в методах
              Route::model('item', 'App\Item');
              Route::get('item/{item}', function(App\Item $item) {}); // Вуаля у нас нужный айтем, пустой если не передан id или нужный если он передан, а в случаей отсутствия мы сюда не попадаем, так как генерится ошибка.
              Для symfony есть похожий бандл.
              Вы понимаете каков машдаб плясок вокруг сраной модельки из-за того, что ей поручают заниматься не её делами, да ещё она и делает это криво.

              p.s. Кстати, для Doсtrine 1, были решения в пару строк кода, которые добавляли те же IM и UoW. Не велика беда добавить сей функционал туда где надо, а это малюсенький процент от общего количества проектов. Но во второй версии плясали от горячей печки в итоге и сами обожглись и еду недоготовили, выпустив сырой продукт.
              • 0
                А в Laravel это решается через DI

                Ну для начала не через DI а через IoC, во вторых в том же Symfony это называется ParamConverter. Подозреваю что в Laravel это тоже что-то подобное.

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

                Простите, а чем должна заниматься модель? Это ж самая важная часть приложения, сущности. Не ну если конечно вы загоняетесь по всяким там DDD.

                да ещё она и делает это криво.

                Тут как у кого руки.

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

                Где он сырой?
                • 0
                  Именно DI, потому что мы даже напрямую не пляшем не с EM как в случае doctrine, не с контейнером App в случае Laravel, а именно инджектится нужный объект в нужный метод function(App\Item $item), этакий чёрный ящик.

                  Про баги БАГИ К сожалению не могу нарыть твит, где авторы сами иронизировали, что приходится по три дня рыться, чтобы выяснить в чём баг.

                  Ну а что называется ParamConverter, какая разница как нызывается, если он подтверждает удобство инъекций. Какие-то попытки засунуть голову в песок и придраться к названиям.
                  • 0
                    Именно DI, потому что мы даже напрямую не пляшем не с EM как в случае doctrine, не с контейнером App в случае Laravel


                    А с Route::bind, который обращается к App\Item::findOrFail или что-то в этом духе, что скрыто за сахаром в виде Route::model. Никакого DI, просто сахар. Вот если бы мы инджектили энтитю в конструктор нашего контроллера — можно было бы говорить про DI. А так это просто аргумент, просто данные. Вы же не говорите что в случае:

                    function foo(string $bar) : string {
                         return 'foo' + $bar;
                    }
                    


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

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

                    А кто говорил что доктрина простая штука? Вот там был забавный баг связанный с поведением spl_object_hash, и его реально было тяжко обнаружить, но это баг не доктрины а PHP. Баги будут всегда, и чем сложнее решение — тем сложнее дебажить. Ребята работают, упрощают архитектуру, улучшают. По сравнению с 2.0 текущая 2.5 уже намного более интересна.
                    • –1
                      >> Вот если бы мы инджектили энтитю в конструктор нашего контроллера — можно было бы говорить про DI.
                      Laravel умеет инджектить в метод, не только в конструктор. Если хотите, инджектите в конструктор. Смысл в том, что нужную энтити вытаскивает другой инфраструктурный код и пробрасывает её по цепочки и в мидлеваре и в экшен контроллера. Инджект приятнее и читабельнее, удобно тестировать, нежели создание контейнера их которого потом внутри метода тянется $this->get('items_repository')->findItem();

                      IM в простом виде это и есть конструкция, проверяющая наличие объекта в карте. Это не дело ORM следить за Identity map.
                      if (!Registry::has('Item:'.$identity)) Registry::set('Item:'.$identity, Item::find($identity)) return !Registry::get('Item:'.$identity);
                      UoW примерно то же. сразу заботимся чтобы пользователь работал с IM объектами, а затем делаем методы save-delete отложенными. Никакой хайлевел магии нет, чтобы захломлять ей бизнеслогику и клепать жуткие контроллеры пляшущие вокруг EM.
                      • +1
                        Laravel умеет инджектить в метод

                        И? Причем тут eloquent? Вообще какой-то бред вы пишите уже. Помниться был экстеншен для Laravel добавляющий доктрину, и там так же была реализация чего-то типа Route::entity, реализующую ровно тоже самое что и Route::model для доктрины. Я просто не понимаю почему вы думаете что с доктриной чего-то нельзя или что при использовании eloquent у вас при запросе через статический метод не поднимается контейнер и прочая лабуда.

                        IM в простом виде это и есть конструкция

                        Вы так прицепились к этому IM как буд-то бы это что-то важное. Это просто пара строчек кода в реализации Unit-of-work. Это не кеширующая прослойка, это не регистри, к которой вообще кто-то имеет дотступ, это просто IM, доступный только в рамках реализации UoW. У вас почему-то ассоциации с IM как с IoC.

                        жуткие контроллеры пляшущие вокруг EM.

                        Ясно понятно, где оно пляшет вокруг EM? Вот скажите мне, где? У вас вот весь проект насквозь завязан на eloquent, и я считаю это годным только для маленьких проектов. У вас есть некий базовый класс вашей сущности который намного хуже entity manager.
                        • –1
                          >>Причем тут eloquent?
                          да блин, я и пишу что НИ ПРИ ЧЁМ, это не дело ORM заниматься контролем за количеством Entity. Я же это пишу уже который раз:
                          >>плясок вокруг сраной модельки из-за того, что ей поручают заниматься не её делами
                          >>IM при нормальной архитектуре заменяется DI.
                          >>Именно DI, потому что мы даже напрямую не пляшем не с EM как в случае doctrine
                          >>Смысл в том, что нужную энтити вытаскивает другой инфраструктурный код

                          извините, умываю руки, вы всё равно не читаете что я пишу.
                          • +1
                            Перечитайте еще раз те цитаты которые вы выделели и скажите, вы точно уверены что знаете о чем говорите? Вы точно понимаете что такое IM в контексте доктрины? Зачем он нужен? Вы понимаете что к этому IM имеет доступ ТОЛЬКО unit-of-work? Понимаете, что если это деталь реализации UoW то выделять IM в отдельный пункт как минимум глупо?

                            Теперь дальше.

                            вытаскивает другой инфраструктурный код

                            какой другой? Чем это отличается от Doctrine? Вы говорили про Route::model. Эта штука просто просит eloquent достать что-то, так же она может просить и доктрину через репозиторий. Разницы никакой.
            • 0
              >>По сути правильно на каждый запрос создавать свой IM и свой UoW
              А в чём тогда их смысл, если в паралельном запросе мы снесли вообще данную запись, которую потом считаем существуюей ибо на у нас извлекается из DI и даже есть цепочка действий с ней в UoW. И вообще это не дело ORM, это тупо зависимости.

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

              AR в Laravel — совсем не простой прямой мэппинг строки в таблице на объект. Во первых там есть работа с колекциями как ин мемори, так и при гидрации из базы, в любой момент мы моеж переопределить и получить тот же НЕ прямой мэпинг и мделать это в разы проще чем в Doctrine. Поддержка связей для вытаскивания связанных сущностей. Скоупы, которые являются аналогом DDD-шных критерий. Разные мутаторы и ассесоры, опять же выходящие за рамки прямого мэппинга данных. Обсервинг и т.д.
              • 0
                А в чём тогда их смысл, если в паралельном запросе мы снесли вообще данную запись, которую потом считаем существуюей ибо на у нас извлекается из DI


                Да забудьте вы про этот DI. Просто забудьте и не упоминайте в контексте модели предметной области. Что до сохранения целостности — на этот случай у нас (ну или у меня) есть постгрес, который это дело разрулит как последняя линяя обороны). Да и кейс который вы описали довольно редкий.

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

                в существующий проект не с самой продуманной структурой БД

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

                его хилость при наличии большого оверхеда на скорость разработки и производительность.

                Доктрина позволяет грамотно спроектировать систему, а далее можно оптимизировать хоть до посинения. Сначала оптимизируя работу доктрины (начиная от индексов в базе и заканчивая своими ChangeTrackingPolicies, гидраторами, проксями или вообще своей реализацией репозиториев на чистом DBAL). Есть довольно большое количество проектов где сделать быстрее бизнес логику намного важнее «сделать что бы работало быстро». По поводу оверхэда на время разработки — если честно, если ее использовать адекватно проблем не возникает особо.

                Во первых там есть работа с колекциями

                Где этого нет?

                НЕ прямой мэпинг и мделать это в разы проще чем в Doctrine.

                Весьма спорное утверждение.

                Поддержка связей для вытаскивания связанных сущностей.

                Это есть у всех ORM.

                Скоупы, которые являются аналогом DDD-шных критерий.

                Очень кривым аналогом, прошу заметить.

                Разные мутаторы и ассесоры


                Здравствуй анемичная модель.

                Обсервинг и т.д.

                Опять же, где этого нет и где это хорошо?
                • 0
                  Посмотрите на свой пример и на мой, у вас пляска вокруг EM, хотя это должен быть инфраструктурный слой и он не должен засирать бизнес логику, даже в случае с AR код получается чище. Потому что EM — это типичный God Object, который берёт на себя всё, при этом анемичная кодель представляет собой спагетти из анотаций, репозитории с своим подъязыком DQL, ограниченные IM и UoW, rкоторые всё равно не гарантируют атомарность и целостность данных.

                  Я был одним из первых поклонников Doctrine 1, нес её в массы, потому что она помогала решать проблемы, а вторая их только добавляет.
                  • 0
                    Посмотрите на свой пример и на мой, у вас пляска вокруг EM

                    Где у меня там пляска вокруг EM? то что я поленился в отдельный слой вынести flush? так я комментарием пометил что у меня это вынесено из контроллеров во фронтконтроллер (framework layer) и мое приложение вообще ничего о доктрине не знает.

                    даже в случае с AR код получается чище.

                    Где он чище? Вот покажите где? У меня есть отдельный объект-репозиторий, который знает как хранятся бизнес-сущности, а последние вообще ничего не знают о хранении данных. Принцип единой ответственности соблюден. А с AR я получаю кашу.

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

                    — Анемичная модель сама по себе бэд практис
                    — я не использую аннотации. Как и анемичные модели.

                    репозитории с своим подъязыком DQL

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

                    ограниченные IM и UoW

                    Какие? Вот реально, какие ограничения IM (вообще странно что его отдельно рассматривают, не смотря на то что это несколько строчек в реализации UoW) и UoW в реальных ситуациях не позволяют что-то сделать? Там есть нюансы, не спорю, но как по мне это довольно голословное утверждение.

                    rкоторые всё равно не гарантируют атомарность и целостность данных.

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

                    а вторая их только добавляет.

                    Только в самом начале, пока не приходит понимание как ее готовить. Альтернатив доктрине пока нет. Не могу сказать что это хорошо, конкуренция это всегда хорошо, но это факт. А с ActiveRecord очень легко все сделать очень плохо, что я обычно и вижу.
                    • +2
                      > Потому что EM — это типичный God Object
                      за счет чего? что он используется везде? драйвер подключения к бд это тоже God Object?

                      > собой спагетти из анотаций
                      пишите в конфиге, какие проблемы? аннотации удобны на самом деле

                      > ограниченные IM и UoW

                      IM кстати часть UoW (просто массив), uow содержит тот срез данных с которыми вы работаете, там лежат оригинальные данные выбранные из базы, на основе которых doctrine делает частичные обновления только изменений. IM используется в том числе для того чтобы правильно делать связи, не вы создаете объекты из SQL ответов, а ORM иначе она бы потеряла смысл.
                      и чем они ограничены?
                      > не гарантируют атомарность и целостность данных
                      если у вас есть параллельный запрос который удаляет данные дело тут не в ORM, вам нужно самому обеспокоится тем как разруливать это. а атомарность и целостность в конечном итоге гарантирует только субд
                      • –1
                        God Object — это не про доуступность, а про функциональность, доктрина класичейский его представитель, потому что берёт на себя слишком много функцинала, который может обеспечиваться другими инфраструктурными уровнями. Не зря е компоненты начали жить отдельной жизнью.

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

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

                          Решение проблем с concurency UoW решает как раз таки тем, что он точно знает что поменялось и будет менять только это. Так что если кто-то за мгновение до флаша трназакции поменя поле foo, а наша транзакция меняет поле bar — все будет хорошо.
                      • +1
                        у вас пляска вокруг EM, хотя это должен быть инфраструктурный слой и он не должен засирать бизнес логику, даже в случае с AR код получается чище.


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

                        Место для инфраструктурного кода — контроллер (в широком смысле слова, начиная с фронта, роутинга и т. д., возможно с вынесение части кода в сервисы и т. п.). Место для бизнес-логики — модель и только модель. Если у вас бизнес-логика в контроллере при не анемичной модели, то она у вас размазана, а у контроллера две обязанности. Если при анемичной, то просто две обязанности.
                        • 0
                          Согласен. В идеале да, например, в .net вполне себе удобно реализовано. А на практике в doctrine модельки такие, что ни одной засранной AR не снилось. Плюс всё разнесено по 100500 слоям из-за чего малейшее изменение влечёт кучу правок.

                          Довольно хорошо знаю и понимаю теорию, ещё лет пять назад молился на паттерны, увлекался ddd, а последние годы больше увлекаюсь гряным кодом с костылями. На моей личной практике такой подход оказывается более эффективным.
                          • 0
                            что ни одной засранной AR не снилось.

                            Можно конкретный пример? Вот я ни разу не видел подобного. До 2.5 бывало конечно (И в случае с AR в принципе не думаю что вышло бы лучше), но последние пол года ни разу.

                            Плюс всё разнесено по 100500 слоям из-за чего малейшее изменение влечёт кучу правок.

                            Можете описать подробнее? Любопытно же. И причем тут доктрина? Она по идее в одном из слоев должна лежать и не вылазить наружу.

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

                            Мне кажется вы привели пример одной крайности (молиться на паттерны) и другой (класть болт на паттерны, лишь бы работало). Ну и да, я бы не сказал что загоны по паттернам это признак хорошего тона.
            • +2
              ну статья бессмысленный крик души, собственно ни одного реального довода там не приведено, например:
              > Identity Map бесполезна в окружении «без сохранения состояния»
              без Identity Map вы не сможете привязать один и тотже объект автора к 2м его книгам, о чем речь вообще?

              > «Ленивая» загрузка бессмысленна
              Любая ORM создает объекты это её смысл, что бы предоставить абстракцию работы с базой через объекты. Если кто-то сам не озаботится тем как загрузить список references то это его проблема.

              > Политики отслеживания изменений
              > Если у нашего приложения хорошая архитектура, то данные не меняются в случайных местах
              так данные меняются то не архитектурой, вы оперируете Mutable объектами и естественно они могут быть случайно изменены в любом месте
              • +1
                А я, как человек, перешедший на query builder-ы c Doctrine ORM во многом согласен с доводами из статьи. Автор в целом прав, в том, что lazy-load данных из базы — ключевая концепция. Eager-load не проработан на столько же, возможно потому что это сложнее, или по другой причине, но нет возможности сгенерировать два запроса, вместо N вида:
                SELECT * FROM users u WHERE u.foo = bar;
                SELECT * FROM posts p JOIN users u ON u.id = p.user_id WHERE u.foo = bar; // или IN(id1, id2, ...)
                

                Ладно, у нас есть мощный query builder, попробуем сами такое сделать. Ан нет, во всяком случае в версии 2.4. Сущности не свяжутся и Identity Map становится бессмысленной, тк она не работает.
                Также с Identity Map был косяк, из-за которого пришлось форкать доктрину в одном из проектов — не работал DateTime в качестве primary key. Банально отсутствовал метод, который привел бы DateTime к строке и добавлять в ядро это не хотели, предложили заменить DateTime на свой, я честно старался сделать это, но через пару часов так ничего и не вышло.
                • +4
                  Мне сложно придумать случай, когда вот прямо надо делать DateTime первичным ключем. Очень любопытно было бы хотя бы немного узнать о такой задаче. Что до проблемы — она решаема. Просто нужно определить свой доктриновский тип и свой тип, что-то типа MyEntityID а внутри уже будет \DateTime, либо наследоваться от него. Словом варианты есть.
                  • 0
                    с запросами что-то непонятно, то что вы написали на DQL пишется так:
                    «SELECT u FROM Users u WHERE u.foo = :bar»
                    «SELECT p FROM Posts p WHERE p.user in (:users)»
                    если вы хотите выбрать посты, но что бы он потом выбрал пользователей этих постов есть query hint
                    у нас сделан хелпер который по списку proxy загружает их одним запросом (причем с учётом кеша)
                    • 0
                      Я понимаю, как это сделать на DQL. Тут смысл в том, что эти два запроса не свяжут посты и пользователей, во всяком случае в 2.4.
                      • 0
                        в смысле не свяжут?
                        это даже в 2.0 работало, если вы заранее выбираете список пользователей, то при вытягивании постов, они будут взяты из unit of work через identity map.
                        люди наоборот недопонимали, почему один и тот же объект подставляется (со старыми данными), хотя я его через sql update обновил
                        • 0
                          В том смысле, в котором я написал. Это не работало. Я сообщал о баге, было это около полугода назад. Не просто так же я пишу.
                          • +3
                            а можно ссылку на bug report?
                  • 0
                    Доктриновский EM — тот случай, когда паттерн «фасад» незаметно превращается в антипаттерн «God Object».

                    А что, с Eager Load все правда плохо? Давно не трогал доктрину, хотел недавно еще раз попробовать, теперь страшно.
                    • 0
                      Сам далеко не фанат доктрины, но со многим в статье не согласен. Например lazy load штука хорошая, но плохо контролируется в доктрине.

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

                      Примеры:
                      — найти все записи, у которых значения поля больше определенного значения. Все, прощайте простенькие методы репы, здравствуй QueryBuilder;

                      — от getScalarResult() я ожидал массив вида: [1,2,3], а получил [0=>[1], 1=>[2], 2=>[3]]. Результат совершенно не отличается от getArrayResult();

                      — простой, быстрый и элегантный запрос с подзапросом в джойне невозможно реализовать в DQL, а с подзапросом в where мне не подходил. Пришлось делать два запроса;

                      — еще несколько примеров, которые я не могу привести в силу сложности условий для возникновения;

                      Я уж не говорю про костыли при работе с плохо спроектированной БД — тут вообще ад и извращения.
                      • 0
                        А Doctrine научилась фильтровать по идентификатору внешнего ключа, не выгружая целиком весь связанный объект из БД? А также сетить идентификатор в relation вместо целого объекта?
                        • 0
                          А также сетить идентификатор в relation вместо целого объекта?

                          Это точно не умеет. Очень неудобно конечно, приходится извращаться, что портит и код, и производительность.

                          А Doctrine научилась фильтровать по идентификатору внешнего ключа, не выгружая целиком весь связанный объект из БД?

                          Немного не понял, о чем вы. Можете пример привести?
                          • +1
                            user.group = 5 работало всегда, делать select IDENTITY(user.group) научилась, getId у прокси объекта не вызывает подгрузку данных из бд
                            • 0
                              user.group = 5 работало всегда

                              Не знаю, как у вас все работает, но
                              $user->setGroupId(5);
                              

                              не работает, если у User есть ManyToOne user.group. Доктрина видит, что group у user не установлен, и затирает groupId
                              • +4
                                ну это часть была про фильтровать, ваш код так (если группа уже загружена вернет её, если нет то просто создаст прокси объект):
                                $user->setGroup($em->getReference(Group::class, 5))
                                • 0
                                  Как это решается я, конечно, знаю. Но не считаю это хорошим подходом. Во-первых, ухудшается читаемость, во-вторых это все-таки оверхед.
                                  • +2
                                    а откуда у вас при использовании ORM взялся id?
                                    если из вне то все равно идти в базу и проверять есть он там или нет.
                                    + мой вариант не ломает пользователя для других частей кода, то есть куда бы я его не передал это будет пользователь с объектом группы, с которым можно работать, а не так что у него группа админы, а вы ему ставите пользователя, но пока не сохраните в БД, все равно будет админ.
                                    В общем если используете ORM то используйте, нет, пишите
                                    $conn->executeQuery(«UPDATE users SET group_id = 5 where id = ?», [$user->getId()])
                                    так хоть будет видно в какой момент у вас пошел рассихрон объекта и базы и поведение будет более предсказуемо
                                    • 0
                                      Это работает в большинстве случаев, но не всегда. Я могу получить groupId из надежного источника, например если мне нужно скопировать запись в БД. При хорошем lazy load у меня будет groupId, но не будет самого group.

                                      Конечно, направлять разработчика на best practies — это хорошо. Но если программист неопытен, или если он просто идиот, то он будет ставить конкретные костыли, и будет еще хуже. Если же программист грамотный, то он хорошо знает, когда можно и нужно поступиться best practies, а система не позволит ему это сделать, и хороший программист будет грустить. И я говорю сейчас не только об этом конкретном примере, который мы обсуждаем, но обо всей симфони, и доктрине в частности.
                                      • +1
                                        Зачем пытаться всунуть id в orm когда вы можете просто сделать sql запрос?
                                        Что это за пример ухода от оверхеда за счет экономии на создании объекта, когда вы всеравно продолжаете вызывать весь код, который генерирует события (persist, update remove и т.п.), считает changeSet и прочее? да можно 100 объектов инстанцировать и вы всеравно не заметите разницы по сравнению с ним.
                                        getReference как раз про это, чтобы все сделать в терминах ORM не подгружая саму сущность из базы.
                                        и дело не в best practies, это просто логика. и это справедливо для всей симфони и доктрины.
                                        Доктрина к слову не частность симфони

                        • +2
                          [troll mode]ORM — для тех, кто не умеет и не хочет писать SQL[/troll mode]

                          На самом деле, любой ORM слишком тяжеловесен для любой платформы — PHP, Ruby или Java. Тот же Hibernate пугает, если посмотреть в его debug-лог. Конечно, при этом заметно упрощается непростая работа программиста по проектированию БД, выборкам со сложными связями и контролю целостности данных, но достигается это ценой времени рантайма.
                          В целом, можно смело пользоваться ORM, если результат нужен вчера, а нагрузка не планируется слишком большой — нынче наконец-то ценят трудозатраты программиста, экономия байтов памяти и тактов процессора, хвала сущему, ушла в прошлое. И да, код становится тоже проще и понятнее — ценой рантайма.

                          Компромисса можно достигать в разных точках — как в крайних (использовать орм или нет), так и в нескольких:
                          — реализовать сначала с помощью орм, а потом все переписать на нативные запросы
                          — переписать на скул только критичные места
                          — использовать query builder как недо-ормом
                          Для себя нашел вариант:
                          — проектируем с помощью объектов и аннотаций(подсказок орму ) для связей, смотрим на полученный результат и пишем запросы оглядываясь на него.
                          Вариант подходит как для пхп, так и для джавы, но также не забываем, что грамотно спроектированная база (типы, констрейнты и индексы) работает быстрее даже с с ORM в PHP, чем NoSQL в Java.
                          • +1
                            > любой ORM слишком тяжеловесен для любой платформы

                            Вовсе нет. ORM — это необязательно Hibernate-style или ROR ActiveRecords-style. Простейший DataMapper с ручным маппингом и составлением SQL-запросов через легкий QueryBuilder или вообще руками — тоже ORM.
                            • 0
                              Извините, но QueryBuilder без управления связей стыдно ORM'ом назвать.
                              • 0
                                Можно и связями вручную управлять. Формально это ORM :-)
                                • 0
                                  И насколько полезен такой формальный орм?
                                  • 0
                                    Всякие хайлоады именно так и пишутся
                                    • 0
                                      что и требовалось доказать (ц) = ) см. мой первый пост
                                      • +1
                                        Не совсем. Такой «ручной» DataMapper нужен для четкого разделения бизнес-логики и инфраструктурного слоя, отвечающего за персистенцию. Бизнес-логика и инкапсулированные данные находятся в модели, работа с хранилищами — на инфраструктурном уровне datamappers и repositories. Это и упрощает тестирование бизнес-логики, и снижает зависимость от конкретного хранилища или неудачной архитектуры базы. Да и не совсем ручной, на самом деле — легкий Query Builder, позволяющий манипулировать raw sql, и хелперы для упрощения управлением связями и в хайлоаде не навредят.

                                        Совсем «без ORM» получатся голые массивы или структуры (или псевдоструктуры в виде anemic models), которые будут гоняться по всему коду — что есть сведение к процедурному программированию. Спасибо, не хочу, кушайте сами.
                            • 0
                              В дополнение: часто использую такой способ:

                              — пишу на Доктрине с аннотациями с кастомным репозиторием

                              — на тормозящих местах делаю кастомные DQL запросы под ситуацию (где всё одним запросом выбрать, где N+1 лучше, и прочие логические оптимизации)

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

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