Я знаю, что заголовок этой статьи похож на троллинг. Но это не так, это просто констатация факта. Я не пытаюсь сказать, что 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 расширении или о генераторах. Кроме того он принимает заявки в комментариях.
Комментарии (53)
neolink
04.06.2015 13:03+2ну статья бессмысленный крик души, собственно ни одного реального довода там не приведено, например:
> Identity Map бесполезна в окружении «без сохранения состояния»
без Identity Map вы не сможете привязать один и тотже объект автора к 2м его книгам, о чем речь вообще?
> «Ленивая» загрузка бессмысленна
Любая ORM создает объекты это её смысл, что бы предоставить абстракцию работы с базой через объекты. Если кто-то сам не озаботится тем как загрузить список references то это его проблема.
> Политики отслеживания изменений
> Если у нашего приложения хорошая архитектура, то данные не меняются в случайных местах
так данные меняются то не архитектурой, вы оперируете Mutable объектами и естественно они могут быть случайно изменены в любом месте
hell0w0rd
04.06.2015 14:40+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 на свой, я честно старался сделать это, но через пару часов так ничего и не вышло.Fesor
04.06.2015 14:59+4Мне сложно придумать случай, когда вот прямо надо делать DateTime первичным ключем. Очень любопытно было бы хотя бы немного узнать о такой задаче. Что до проблемы — она решаема. Просто нужно определить свой доктриновский тип и свой тип, что-то типа MyEntityID а внутри уже будет \DateTime, либо наследоваться от него. Словом варианты есть.
neolink
04.06.2015 15:51с запросами что-то непонятно, то что вы написали на DQL пишется так:
«SELECT u FROM Users u WHERE u.foo = :bar»
«SELECT p FROM Posts p WHERE p.user in (:users)»
если вы хотите выбрать посты, но что бы он потом выбрал пользователей этих постов есть query hint
у нас сделан хелпер который по списку proxy загружает их одним запросом (причем с учётом кеша)hell0w0rd
04.06.2015 18:12Я понимаю, как это сделать на DQL. Тут смысл в том, что эти два запроса не свяжут посты и пользователей, во всяком случае в 2.4.
neolink
04.06.2015 19:11в смысле не свяжут?
это даже в 2.0 работало, если вы заранее выбираете список пользователей, то при вытягивании постов, они будут взяты из unit of work через identity map.
люди наоборот недопонимали, почему один и тот же объект подставляется (со старыми данными), хотя я его через sql update обновил
symbix
04.06.2015 15:43Доктриновский EM — тот случай, когда паттерн «фасад» незаметно превращается в антипаттерн «God Object».
А что, с Eager Load все правда плохо? Давно не трогал доктрину, хотел недавно еще раз попробовать, теперь страшно.
Reposlav
04.06.2015 17:43Сам далеко не фанат доктрины, но со многим в статье не согласен. Например lazy load штука хорошая, но плохо контролируется в доктрине.
Главными недостатками доктрины вижу чрезмерную монструозность и тормознутость, сложность реализации простых вещей, и неочевидность поведения в сложных случаях.
Примеры:
— найти все записи, у которых значения поля больше определенного значения. Все, прощайте простенькие методы репы, здравствуй QueryBuilder;
— от getScalarResult() я ожидал массив вида: [1,2,3], а получил [0=>[1], 1=>[2], 2=>[3]]. Результат совершенно не отличается от getArrayResult();
— простой, быстрый и элегантный запрос с подзапросом в джойне невозможно реализовать в DQL, а с подзапросом в where мне не подходил. Пришлось делать два запроса;
— еще несколько примеров, которые я не могу привести в силу сложности условий для возникновения;
Я уж не говорю про костыли при работе с плохо спроектированной БД — тут вообще ад и извращения.Blumfontein
04.06.2015 19:26А Doctrine научилась фильтровать по идентификатору внешнего ключа, не выгружая целиком весь связанный объект из БД? А также сетить идентификатор в relation вместо целого объекта?
Reposlav
04.06.2015 19:39А также сетить идентификатор в relation вместо целого объекта?
Это точно не умеет. Очень неудобно конечно, приходится извращаться, что портит и код, и производительность.
А Doctrine научилась фильтровать по идентификатору внешнего ключа, не выгружая целиком весь связанный объект из БД?
Немного не понял, о чем вы. Можете пример привести?
neolink
04.06.2015 19:56+1user.group = 5 работало всегда, делать select IDENTITY(user.group) научилась, getId у прокси объекта не вызывает подгрузку данных из бд
Reposlav
04.06.2015 20:02user.group = 5 работало всегда
Не знаю, как у вас все работает, но
$user->setGroupId(5);
не работает, если у User есть ManyToOne user.group. Доктрина видит, что group у user не установлен, и затирает groupIdneolink
04.06.2015 20:07+4ну это часть была про фильтровать, ваш код так (если группа уже загружена вернет её, если нет то просто создаст прокси объект):
$user->setGroup($em->getReference(Group::class, 5))Reposlav
05.06.2015 12:13Как это решается я, конечно, знаю. Но не считаю это хорошим подходом. Во-первых, ухудшается читаемость, во-вторых это все-таки оверхед.
neolink
05.06.2015 12:45+2а откуда у вас при использовании ORM взялся id?
если из вне то все равно идти в базу и проверять есть он там или нет.
+ мой вариант не ломает пользователя для других частей кода, то есть куда бы я его не передал это будет пользователь с объектом группы, с которым можно работать, а не так что у него группа админы, а вы ему ставите пользователя, но пока не сохраните в БД, все равно будет админ.
В общем если используете ORM то используйте, нет, пишите
$conn->executeQuery(«UPDATE users SET group_id = 5 where id = ?», [$user->getId()])
так хоть будет видно в какой момент у вас пошел рассихрон объекта и базы и поведение будет более предсказуемоReposlav
05.06.2015 15:25Это работает в большинстве случаев, но не всегда. Я могу получить groupId из надежного источника, например если мне нужно скопировать запись в БД. При хорошем lazy load у меня будет groupId, но не будет самого group.
Конечно, направлять разработчика на best practies — это хорошо. Но если программист неопытен, или если он просто идиот, то он будет ставить конкретные костыли, и будет еще хуже. Если же программист грамотный, то он хорошо знает, когда можно и нужно поступиться best practies, а система не позволит ему это сделать, и хороший программист будет грустить. И я говорю сейчас не только об этом конкретном примере, который мы обсуждаем, но обо всей симфони, и доктрине в частности.neolink
05.06.2015 16:04+1Зачем пытаться всунуть id в orm когда вы можете просто сделать sql запрос?
Что это за пример ухода от оверхеда за счет экономии на создании объекта, когда вы всеравно продолжаете вызывать весь код, который генерирует события (persist, update remove и т.п.), считает changeSet и прочее? да можно 100 объектов инстанцировать и вы всеравно не заметите разницы по сравнению с ним.
getReference как раз про это, чтобы все сделать в терминах ORM не подгружая саму сущность из базы.
и дело не в best practies, это просто логика. и это справедливо для всей симфони и доктрины.
Доктрина к слову не частность симфони
Lure_of_Chaos
05.06.2015 10:32+2[troll mode]ORM — для тех, кто не умеет и не хочет писать SQL[/troll mode]
На самом деле, любой ORM слишком тяжеловесен для любой платформы — PHP, Ruby или Java. Тот же Hibernate пугает, если посмотреть в его debug-лог. Конечно, при этом заметно упрощается непростая работа программиста по проектированию БД, выборкам со сложными связями и контролю целостности данных, но достигается это ценой времени рантайма.
В целом, можно смело пользоваться ORM, если результат нужен вчера, а нагрузка не планируется слишком большой — нынче наконец-то ценят трудозатраты программиста, экономия байтов памяти и тактов процессора, хвала сущему, ушла в прошлое. И да, код становится тоже проще и понятнее — ценой рантайма.
Компромисса можно достигать в разных точках — как в крайних (использовать орм или нет), так и в нескольких:
— реализовать сначала с помощью орм, а потом все переписать на нативные запросы
— переписать на скул только критичные места
— использовать query builder как недо-ормом
Для себя нашел вариант:
— проектируем с помощью объектов и аннотаций(подсказок орму ) для связей, смотрим на полученный результат и пишем запросы оглядываясь на него.
Вариант подходит как для пхп, так и для джавы, но также не забываем, что грамотно спроектированная база (типы, констрейнты и индексы) работает быстрее даже с с ORM в PHP, чем NoSQL в Java.symbix
05.06.2015 10:37+1> любой ORM слишком тяжеловесен для любой платформы
Вовсе нет. ORM — это необязательно Hibernate-style или ROR ActiveRecords-style. Простейший DataMapper с ручным маппингом и составлением SQL-запросов через легкий QueryBuilder или вообще руками — тоже ORM.Lure_of_Chaos
05.06.2015 11:03Извините, но QueryBuilder без управления связей стыдно ORM'ом назвать.
symbix
05.06.2015 13:20Можно и связями вручную управлять. Формально это ORM :-)
Lure_of_Chaos
05.06.2015 13:40И насколько полезен такой формальный орм?
symbix
05.06.2015 15:27Всякие хайлоады именно так и пишутся
Lure_of_Chaos
05.06.2015 15:34что и требовалось доказать (ц) = ) см. мой первый пост
symbix
05.06.2015 16:38+1Не совсем. Такой «ручной» DataMapper нужен для четкого разделения бизнес-логики и инфраструктурного слоя, отвечающего за персистенцию. Бизнес-логика и инкапсулированные данные находятся в модели, работа с хранилищами — на инфраструктурном уровне datamappers и repositories. Это и упрощает тестирование бизнес-логики, и снижает зависимость от конкретного хранилища или неудачной архитектуры базы. Да и не совсем ручной, на самом деле — легкий Query Builder, позволяющий манипулировать raw sql, и хелперы для упрощения управлением связями и в хайлоаде не навредят.
Совсем «без ORM» получатся голые массивы или структуры (или псевдоструктуры в виде anemic models), которые будут гоняться по всему коду — что есть сведение к процедурному программированию. Спасибо, не хочу, кушайте сами.
VolCh
05.06.2015 18:57В дополнение: часто использую такой способ:
— пишу на Доктрине с аннотациями с кастомным репозиторием
— на тормозящих местах делаю кастомные DQL запросы под ситуацию (где всё одним запросом выбрать, где N+1 лучше, и прочие логические оптимизации)
— на всё ещё тормозящих местах переписываю их на нативный скуль и реализую примитивный датамаппинг, что позволяет не менять логику
Fesor
Но PHP это не Ruby, так что, в нем нет места RoR подобным фреймворкам? В то же время PHP намного ближе к Java или c# нежели к Ruby или Python.
Последнее время идея демонизировать приложения на PHP все чаще мелькает, а учитывая то что с версии 5.3 сделать долгоиграющее приложение не особо проблема, думаю этот пункт можно опустить.
Если разработчик использует MySQL есть не нулевая вероятность того что он еще и бесполезен. А вот с СУБД типа oracle или postgresql можно спокойно делать persistence ignorance и с этого получать нехилый профит.
А кто сказал что это центральный концепт? Это не так, это просто плюшка.
Потому что Unit-of-work за нас все разруливает
и причем тут POST запрос и изменение данных? Соль то как раз в четком разделении ответственности, что и дает нам профит в плане архитектуры.
А я думаю что только в одном случае: реализация репозиториев. Ну и да, еще flush транзакции по завершению запроса.
Откуда вообще взялась инъекция зависимостей? Оно тут вообще не причем.
Я думаю все проблемы доктрины среди разработчиков вызваны двумя вещами:
— не понимание концепций, которые несет доктрина (ActiveRecord скажем в этом плане намного проще, просто объектное представление табличек в базе, тогда как в контексте доктрины мы вообще ничего о базе в нашем приложении не знаем)
— mysql, который сильно усложняет работу со своими авто инкрементами, алтернатива которой использование UUID, что не всем нравится. А реализация отложенного формирования представления очень геморная штука.
FractalizeR
Мне тоже статья показалась несколько надуманной. Надо только отметить, что все претензии относятся к оригинальной статье, а не к автору перевода.
Меня поразили еще самые первые строки: «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».
Fesor
ну скажем так, я согласен с последней фразой, обычно так и происходит. Доктрину пытаются использовать как любую другую ORM и натыкаются на вещи, которые кажутся странными. Например большинство воспринимает конструкцию:
как прямой аналог save в контексте active record (причем используют persist даже для обновления сущности), плодят транзакции (несколько flush-ей в рамках одного запроса, либо кастыли с if-ами повсюду как например в FosUserBundle).
Своих проблем добавляет и сама доктрина, вводя такие понятия как сущность, репозиторий… при этом у людей слегка смешиваются понятия в голове и теряется суть, слой инфраструктуры сильно смешивается со слоем предметной области… Это обычно ведет к увеличению связанности системы, повышению сложности поддержки и т.д.
Но вот вопрос касательно насколько эти подходы подходят для PHP — я лично считаю что эти подходы как нельзя тут подходят. Другая проблема что, если сравнивать с .NET комьюнити, средний PHP разработчик не способен их осознать. Но это беда не инструментов, а умов.
FractalizeR
Я думаю, это проблема не языка («it’s not suited for PHP»), а непонимание идеологии библиотеки. Скажем, я могу пытаться использовать Hibernate в ActiveRecord стиле. И получится примерно та же самая ситуация.
Я думаю, это не доктрина добавляет проблем. Понятия «сущность» и «репозитарий» — базовые понятия в разработке. Эти понятия в той или иной форме используются в любой DataMapper ORM библиотеке. Да и в ActiveRecord ORM эти понятия тоже встречаются. И их непонимание — это не проблема библиотеки, а проблема квалификации разработчика.
Опять же, это проблема не Doctrine, а квалификации разработчика, не так ли? Разве ActiveRecord ORM сложно использовать так, чтобы «все смешалось»?
Полностью согласен.
AmdY
Там же написано — не 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
Fesor
И что? С архитектурной точки зрения разницы особо нет. По сути правильно на каждый запрос создавать свой IM и свой UoW, который дропается сразу после завершения запроса. Скажем если два запроса работают с одним и тем же экземпляром энтити, это крайне не правильно держать их в одном IM и уж тем более в одном UoW, так как это совершенно разные транзакции. Так что с концептуальной точки зрения я не вижу разницы, демон это или короткоживущий процесс. То что не поддерживает асинхроннности — ну и черт с ним, можно всегда держать пул процессов-воркеров, которые занимаются обработкой запросов, а ускорение получать за счет prefork этих процессов с уже проинициализированными сервисами дожидающимися запроса (можно на php-pm так делать уже сейчас).
Identity Map нужен в контексте UoW, что бы не-было возможность иметь в системе две точки изменения одной и той же сущности как двух разных объектов. Момент с оптимизацией тут так, по мелочи.
Мы говорим о сервисах или о бизнес-объектах? Причем тут вообще DI? И вы говорите о принципе инверсии зависимости, IoC или DiC? Энтити это энтити, это просто бинес-объекты, набор данных и чуть чуть бизнес правил.
С версии 2.5 с этим все получше, можно объекты-значения юзать например и тогда энтити + граф вложенных VO = таблица.
Ммм? Я представляю себе только один случай когда это нужно, когда речь идет не о энтитях, или же когда мы пытаемся замэпить уже существующую базу данных на объекты. Тут опять же NativeQueries могут помочь, хотя я считаю что для таких случаев в принципе доктрина плохо подходит.
Можно подробнее? Как-то не особо AR в том же Laravel выходит за рамки концепций описанных у Фаулера или реализованных например в RoR.
Меня как-то квери билдеры не особо возбуждают, хотя спасибо.
Fesor
Вот к примеру то, как я использую доктрину (максимально упрощенно)
По сути тот код который мне важен (бизнес логика) вообще ничего не знает о доктрине и о том как контролировать транзакции и прочей чуши. А теперь скажите, чем доктрина плоха раз дает нам это организовать с минимальными усилиями?
AmdY
А в Laravel это решается через DI, мы просто биндим модель к запросу и дальше получаем уже готовые объекты в методах
Route::model('item', 'App\Item');
Route::get('item/{item}', function(App\Item $item) {}); // Вуаля у нас нужный айтем, пустой если не передан id или нужный если он передан, а в случаей отсутствия мы сюда не попадаем, так как генерится ошибка.
Для symfony есть похожий бандл.
Вы понимаете каков машдаб плясок вокруг сраной модельки из-за того, что ей поручают заниматься не её делами, да ещё она и делает это криво.
p.s. Кстати, для Doсtrine 1, были решения в пару строк кода, которые добавляли те же IM и UoW. Не велика беда добавить сей функционал туда где надо, а это малюсенький процент от общего количества проектов. Но во второй версии плясали от горячей печки в итоге и сами обожглись и еду недоготовили, выпустив сырой продукт.
Fesor
Ну для начала не через DI а через IoC, во вторых в том же Symfony это называется ParamConverter. Подозреваю что в Laravel это тоже что-то подобное.
Простите, а чем должна заниматься модель? Это ж самая важная часть приложения, сущности. Не ну если конечно вы загоняетесь по всяким там DDD.
Тут как у кого руки.
Где он сырой?
AmdY
Именно DI, потому что мы даже напрямую не пляшем не с EM как в случае doctrine, не с контейнером App в случае Laravel, а именно инджектится нужный объект в нужный метод function(App\Item $item), этакий чёрный ящик.
Про баги БАГИ К сожалению не могу нарыть твит, где авторы сами иронизировали, что приходится по три дня рыться, чтобы выяснить в чём баг.
Ну а что называется ParamConverter, какая разница как нызывается, если он подтверждает удобство инъекций. Какие-то попытки засунуть голову в песок и придраться к названиям.
Fesor
А с Route::bind, который обращается к App\Item::findOrFail или что-то в этом духе, что скрыто за сахаром в виде Route::model. Никакого DI, просто сахар. Вот если бы мы инджектили энтитю в конструктор нашего контроллера — можно было бы говорить про DI. А так это просто аргумент, просто данные. Вы же не говорите что в случае:
принцип инверсии зависимости имеет хоть какое-то отношение? Вот вы швыряетесь этим DI а что подразумеваете я без понятия. Просто магия на рефлексии позволяющая что-то инджектить? это заслуга не ORM а других компонентов, в частности раутинга в Laravel (в виде каких-то мидлвэров). Так что эту часть диалога можно вообще забыть как несущественную.
А кто говорил что доктрина простая штука? Вот там был забавный баг связанный с поведением spl_object_hash, и его реально было тяжко обнаружить, но это баг не доктрины а PHP. Баги будут всегда, и чем сложнее решение — тем сложнее дебажить. Ребята работают, упрощают архитектуру, улучшают. По сравнению с 2.0 текущая 2.5 уже намного более интересна.
AmdY
>> Вот если бы мы инджектили энтитю в конструктор нашего контроллера — можно было бы говорить про 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.
Fesor
И? Причем тут eloquent? Вообще какой-то бред вы пишите уже. Помниться был экстеншен для Laravel добавляющий доктрину, и там так же была реализация чего-то типа Route::entity, реализующую ровно тоже самое что и Route::model для доктрины. Я просто не понимаю почему вы думаете что с доктриной чего-то нельзя или что при использовании eloquent у вас при запросе через статический метод не поднимается контейнер и прочая лабуда.
Вы так прицепились к этому IM как буд-то бы это что-то важное. Это просто пара строчек кода в реализации Unit-of-work. Это не кеширующая прослойка, это не регистри, к которой вообще кто-то имеет дотступ, это просто IM, доступный только в рамках реализации UoW. У вас почему-то ассоциации с IM как с IoC.
Ясно понятно, где оно пляшет вокруг EM? Вот скажите мне, где? У вас вот весь проект насквозь завязан на eloquent, и я считаю это годным только для маленьких проектов. У вас есть некий базовый класс вашей сущности который намного хуже entity manager.
AmdY
>>Причем тут eloquent?
да блин, я и пишу что НИ ПРИ ЧЁМ, это не дело ORM заниматься контролем за количеством Entity. Я же это пишу уже который раз:
>>плясок вокруг сраной модельки из-за того, что ей поручают заниматься не её делами
>>IM при нормальной архитектуре заменяется DI.
>>Именно DI, потому что мы даже напрямую не пляшем не с EM как в случае doctrine
>>Смысл в том, что нужную энтити вытаскивает другой инфраструктурный код
извините, умываю руки, вы всё равно не читаете что я пишу.
Fesor
Перечитайте еще раз те цитаты которые вы выделели и скажите, вы точно уверены что знаете о чем говорите? Вы точно понимаете что такое IM в контексте доктрины? Зачем он нужен? Вы понимаете что к этому IM имеет доступ ТОЛЬКО unit-of-work? Понимаете, что если это деталь реализации UoW то выделять IM в отдельный пункт как минимум глупо?
Теперь дальше.
какой другой? Чем это отличается от Doctrine? Вы говорили про Route::model. Эта штука просто просит eloquent достать что-то, так же она может просить и доктрину через репозиторий. Разницы никакой.
AmdY
>>По сути правильно на каждый запрос создавать свой IM и свой UoW
А в чём тогда их смысл, если в паралельном запросе мы снесли вообще данную запись, которую потом считаем существуюей ибо на у нас извлекается из DI и даже есть цепочка действий с ней в UoW. И вообще это не дело ORM, это тупо зависимости.
Да, с проблемой мэпинга я столкнулся когда пытались втиснуть в существующий проект не с самой продуманной структурой БД. Но это в любом случае не оправдывает его хилость при наличии большого оверхеда на скорость разработки и производительность.
AR в Laravel — совсем не простой прямой мэппинг строки в таблице на объект. Во первых там есть работа с колекциями как ин мемори, так и при гидрации из базы, в любой момент мы моеж переопределить и получить тот же НЕ прямой мэпинг и мделать это в разы проще чем в Doctrine. Поддержка связей для вытаскивания связанных сущностей. Скоупы, которые являются аналогом DDD-шных критерий. Разные мутаторы и ассесоры, опять же выходящие за рамки прямого мэппинга данных. Обсервинг и т.д.
Fesor
Да забудьте вы про этот DI. Просто забудьте и не упоминайте в контексте модели предметной области. Что до сохранения целостности — на этот случай у нас (ну или у меня) есть постгрес, который это дело разрулит как последняя линяя обороны). Да и кейс который вы описали довольно редкий.
UoW содержит те энтити, которые участвуют в одной транзакции. То есть там не могут быть все энтити из всех текущих транзакций и т.д. На каждого клиента свой UnitOfWork. Нужен он в первую очередь для того, что бы перенести изменения, совершенные в рамках нашей бизнес логики с бизнес-сущностями на хранилище. Это позволяет очень жестко провести грань между слоем предметной области и слоем хранения данных/инфраструктурой. И именно это круто. Вы можете писать код бизнес логики не привязываясь к структуре базы данных, вообще не думая о ней. Сначала описываете бизнес логику, потом думаете о том как это дело будете хранить.
Тут использовать доктрину уже не удобно. Во всяком случае без рефакторинга структуры базы.
Доктрина позволяет грамотно спроектировать систему, а далее можно оптимизировать хоть до посинения. Сначала оптимизируя работу доктрины (начиная от индексов в базе и заканчивая своими ChangeTrackingPolicies, гидраторами, проксями или вообще своей реализацией репозиториев на чистом DBAL). Есть довольно большое количество проектов где сделать быстрее бизнес логику намного важнее «сделать что бы работало быстро». По поводу оверхэда на время разработки — если честно, если ее использовать адекватно проблем не возникает особо.
Где этого нет?
Весьма спорное утверждение.
Это есть у всех ORM.
Очень кривым аналогом, прошу заметить.
Здравствуй анемичная модель.
Опять же, где этого нет и где это хорошо?
AmdY
Посмотрите на свой пример и на мой, у вас пляска вокруг EM, хотя это должен быть инфраструктурный слой и он не должен засирать бизнес логику, даже в случае с AR код получается чище. Потому что EM — это типичный God Object, который берёт на себя всё, при этом анемичная кодель представляет собой спагетти из анотаций, репозитории с своим подъязыком DQL, ограниченные IM и UoW, rкоторые всё равно не гарантируют атомарность и целостность данных.
Я был одним из первых поклонников Doctrine 1, нес её в массы, потому что она помогала решать проблемы, а вторая их только добавляет.
Fesor
Где у меня там пляска вокруг EM? то что я поленился в отдельный слой вынести flush? так я комментарием пометил что у меня это вынесено из контроллеров во фронтконтроллер (framework layer) и мое приложение вообще ничего о доктрине не знает.
Где он чище? Вот покажите где? У меня есть отдельный объект-репозиторий, который знает как хранятся бизнес-сущности, а последние вообще ничего не знают о хранении данных. Принцип единой ответственности соблюден. А с AR я получаю кашу.
— Анемичная модель сама по себе бэд практис
— я не использую аннотации. Как и анемичные модели.
который предоставляет замечательный язык для работы с энтитями а не с базой данных, позволяет нам абстрагироваться, да и DQL этот есть только внутри репозиториев, где ему и место.
Какие? Вот реально, какие ограничения IM (вообще странно что его отдельно рассматривают, не смотря на то что это несколько строчек в реализации UoW) и UoW в реальных ситуациях не позволяют что-то сделать? Там есть нюансы, не спорю, но как по мне это довольно голословное утверждение.
А оно должно? UoW гарантирует что то что вы сделали с сущностями оно сделает с базой. Не больше. Целостность данных это ваша забота. Ваша и СУБД. UoW гарантирует вам что все изменения будут выполнены в рамках одной транзакции, чего более чем достаточно.
Только в самом начале, пока не приходит понимание как ее готовить. Альтернатив доктрине пока нет. Не могу сказать что это хорошо, конкуренция это всегда хорошо, но это факт. А с ActiveRecord очень легко все сделать очень плохо, что я обычно и вижу.
neolink
> Потому что EM — это типичный God Object
за счет чего? что он используется везде? драйвер подключения к бд это тоже God Object?
> собой спагетти из анотаций
пишите в конфиге, какие проблемы? аннотации удобны на самом деле
> ограниченные IM и UoW
IM кстати часть UoW (просто массив), uow содержит тот срез данных с которыми вы работаете, там лежат оригинальные данные выбранные из базы, на основе которых doctrine делает частичные обновления только изменений. IM используется в том числе для того чтобы правильно делать связи, не вы создаете объекты из SQL ответов, а ORM иначе она бы потеряла смысл.
и чем они ограничены?
> не гарантируют атомарность и целостность данных
если у вас есть параллельный запрос который удаляет данные дело тут не в ORM, вам нужно самому обеспокоится тем как разруливать это. а атомарность и целостность в конечном итоге гарантирует только субд
AmdY
God Object — это не про доуступность, а про функциональность, доктрина класичейский его представитель, потому что берёт на себя слишком много функцинала, который может обеспечиваться другими инфраструктурными уровнями. Не зря е компоненты начали жить отдельной жизнью.
UoW прекрасно работает, когда у нас долгоживущее приложение, мы можем хоть год однажды вытащив объект работать с ним не синкая с базой. А когда у нас долисекундный цикл, то оверхед от реализации UoW съедают прибыль от ленивости операций. А хуже всего, что при реализации этой ленивости в доктрине была куча багов, я имел анальное удовольствие пару раз пробираться через слои абстракции в поисках проблемы с ленивой подгрузкой.
Fesor
Приведите пруф что хоть где-то это работает именно так как вы думаете. Вот я серьезно. В исходниках или еще где, где прямо говорится что «иногда какой-нибудь Hibernate не лезет в базу а сам становится базой данных». Я с таким подходом вижу целую гору потенциальных проблем. Например ок, можно использовать кэш что бы не забирать состояние сущности. Но у нас же могут быть нескольо инстансов приложений, например на разных серваках, которые работают с одной базой. И в итоге у каддого инстанса будет свой кэш, который может очень легко и просто стать неактуальным.
Решение проблем с concurency UoW решает как раз таки тем, что он точно знает что поменялось и будет менять только это. Так что если кто-то за мгновение до флаша трназакции поменя поле foo, а наша транзакция меняет поле bar — все будет хорошо.
VolCh
В том-то и дело, что это при использовании AR инфраструктурный код засирает логику — в одном объекте и бизнес-код, и инфраструктурный. Дата-маппер же их разделяет — в модели исключительно бизнес-код, который ничего об инфраструктуре не знает, от неё не зависит. Всякие префетчи, лэйзи-лоадинги, персисты и флаши, реализованные в инфраструктурном слое создают у модели впечатление, что она живёт вечно в оперативке.
Место для инфраструктурного кода — контроллер (в широком смысле слова, начиная с фронта, роутинга и т. д., возможно с вынесение части кода в сервисы и т. п.). Место для бизнес-логики — модель и только модель. Если у вас бизнес-логика в контроллере при не анемичной модели, то она у вас размазана, а у контроллера две обязанности. Если при анемичной, то просто две обязанности.
AmdY
Согласен. В идеале да, например, в .net вполне себе удобно реализовано. А на практике в doctrine модельки такие, что ни одной засранной AR не снилось. Плюс всё разнесено по 100500 слоям из-за чего малейшее изменение влечёт кучу правок.
Довольно хорошо знаю и понимаю теорию, ещё лет пять назад молился на паттерны, увлекался ddd, а последние годы больше увлекаюсь гряным кодом с костылями. На моей личной практике такой подход оказывается более эффективным.
Fesor
Можно конкретный пример? Вот я ни разу не видел подобного. До 2.5 бывало конечно (И в случае с AR в принципе не думаю что вышло бы лучше), но последние пол года ни разу.
Можете описать подробнее? Любопытно же. И причем тут доктрина? Она по идее в одном из слоев должна лежать и не вылазить наружу.
Мне кажется вы привели пример одной крайности (молиться на паттерны) и другой (класть болт на паттерны, лишь бы работало). Ну и да, я бы не сказал что загоны по паттернам это признак хорошего тона.