Модели в фреймворках

StalkerClasses

Новичок
Какой на ваш взгляд сейчас лучший класс в фреймворках для работы с базой данных?
Например изучая Laravel и Symfony (doctrine) я понял что это совершенно разные подходы к моделям. Что на ваш взгляд лучше Laravel ORM или Symfony Doctrine?
 

Фанат

oncle terrible
Команда форума
Давайте я, как неофит, отвечу.

Eloquent - это AR, все в одном флаконе - и данные, и средства работы с ними.
Doctrine - это DM, данные отдельно, средства работы с ними - отдельно.

Бери Цикл, фиксер плохого не посоветует.
Мне вот пока ума не хватает понять его объяснения, но если у тебя есть возможность выбирать, то ставь сейчас, а со временем поймешь как правильно использовать.
 

fixxxer

К.О.
Партнер клуба
Eloquent - это AR, все в одном флаконе - и данные, и средства работы с ними.
Doctrine - это DM, данные отдельно, средства работы с ними - отдельно.
Да, это простой и правильный ответ.

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

Вон, помню, был подкаст, где Тейлор и Феррара спорили, что лучше, и все это звучало довольно анекдотично, типа "DM лучше, чем AR!" - "Чем лучше?" - "Чем AR!". И, действительно, есть взять две анемичные модели, реализованные на Eloquent (с магией) и Doctrine (с геттерами-сеттерами), получается, что разница исключительно в том, "в одном флаконе" или "отдельно", а по сути-то ничего не меняется. А все это потому, что основная проблема с ORM, которая сформулирована еще в 90-х, а именно Object-relational impedance mismatch, вообще толком не обсуждалась!

Популярность анемичных моделей, полагаю, обусловлена именно проблемой impedance mismatch: дескать, если автоматически замапить объекты на реляции так сложно, пусть наши объекты будут вырожденными - один класс соответствует одной таблице, а его свойства соответствуют полям в таблицах - то есть, по сути, Persistence Models. Дальше в какой-то момент это уточнение "а какие именно у нас модели" затерялось, и "моделями" стали называть все подряд - вплоть до того, что "M" в "MVC" стало обозначать Persistence Models. Бизнес-логика сначала уехала в толстые контроллеры, потом (по мере осознавания глубины проблемы) в типичном Симфони появились сервисы... Но основная катастрофа так большинством осталась и незамеченной: все свалилось в процедурное программирование, когда вся бизнес-логика стала де-факто описываться процедурами, оперирующими структурами, представляющими собой копии строк в базе. В контроллерах это или в сервисах - не суть важно, сервисы - это просто реюзабельные процедуры (вынесли из контроллеров, чтобы не копипастить). А наличие формальных признаков ООП (ключевые слова class, стрелочки вот эти) создают у неокрепших умов впечатление, что так и надо ("все так делают!", "в документации так написано!").

Вернемся к impedance mismatch. Проблема там, конечно, не в том, что невозможно замапить объекты на реляции - нет, оба "языка" достаточно "полны", и это всегда возможно. Проблема в том, что в общем случае этот маппинг нетривиален, и нет таких правил, по которым в общем случае его можно построить автоматически: объектные зависимости и реляционные зависимости - это две большие разницы. Если начинать проектирование "от базы данных" (как оно часто принято в вебдеве по историческим причинам), и если начинать проектирование "от объектов" (как оно и должно быть в ООП), получатся довольно разные структуры, которые, тем не менее, изоморфны (то есть всегда можно написать функцию, однозначно преобразующую одно в другое и обратно).

Пытаясь создать универсальную функцию для такого взаимного преобразования, мы неизбежно столкнемся со случаями, когда эта абстракция "течет", и мы вынужденно нарушаем либо принципы проектирования ООП, либо принципы проектирования РСУБД (а то и оба два сразу!).

Давайте забудем на минутку про ORM и попробуем запрограммировать это дело вручную, как в старые добрые времена - ручным написанием SQL-запросов. Будем исходить из того, что мы хотим полноценного ООП, и наши модели - это полноценные Domain Models. Пусть нашей предметной областью будет... записная книжка. Ну вон типа как в телефоне. С точки зрения ООП у нас будет пачка классов типа PhoneNumber extends Contact, Address extends Contact, Email extends Contact, TelegramAccount extends Contact и так далее, и Collection<Contacts> у Person (полагаем, что у одного человека может быть сколько угодно контактов любого типа). А с точки зрения РСУБД тут должны быть отдельные таблицы для типов контактов и к каждой еще таблица-связка M:N, ну то есть ${foo}_contact : person. Вот, казалось бы, будет простая анемика и CRUD-ы, а уже все не так просто. Напоминаю - начали мы с ООП, то есть с представления Person в виде объектов в оперативной памяти. Положим, написали. Как теперь мы туда прикрутим персистенцию в базе?

Начнем с ручного ActiveRecord (напоминаю, ручками написанный SQL внутрях модели - это тоже ActiveRecord, Фаулер на странице 160 PoEAA подтверждает). Тут у нас два варианта. Можно написать SQL в каждом классе контактов, и писать в базу при каждом изменении. Но тут сразу вылезает куча изменений: во-первых, мы наверняка вообще не имеем personId в Contact и его наследниках, во-вторых, вместо банального удаления из коллекции придется теперь добавить дергать какой-то там $contact->delete(), то есть делать этакий костыль. Фу. Нет, давайте лучше сделаем единственный метод save прямо в Person - там у нас есть все, что надо, можно даже оптимизировать все это дело и выполнить минимум запросов в одной транзакции. Заодно решается проблема отката частичных изменений ввиду их отсутствия - никаких внешних транзакций не потребуется. А для загрузки из базы сделаем статический метод loadById, где наджойним все, что надо, и воссоздадим из этого нашу структуру объектов. Ну и delete() до кучи (тут с on delete cascade получится совсем просто).

Посмотрим внимательно на то, что получилось. Теперь прочитаем определение Aggregate Root у Фаулера и еще раз посмотрим внимательно на то, что получилось. Получается, мы сделали DDD-шный Aggregate Root буквально из говна и палок, причем оставаясь в рамках Active Record?

Вроде бы да. Но откуда мы вообще взяли в методах save() и loadById() $db? Фаулер в своих примерах этот вопрос удобно игнорирует - "ну вот откуда-то взялся". Наверное, придется в конструктор и в статические методы его передавать в явном виде? И по цепочке везде таскать $db? Или, допустим, создавать все эти наши типа-aggregate-roots через абстрактную фабрику? Ммм, окей, а что делать со статикой? Создавать неконсистентный почти пустой (только с $db) объект рефлексией без конструктора и транслировать в нестатические вызовы, после чего самому создавать и возвращать из "неполноценного" инстанса себя другой "полноценный" (привет, Eloquent)?

Так или иначе, мы что-то такое сделали, оно неплохо работает, с точки зрения ООП все нормально (за исключением SRP и вопроса с $db), с точки зрения РСУБД все нормально.

Есть второй вариант: сделать отдельный класс PersonMapper и заюзать рефлексию. У мапперов в конструкторе $db, через абстрактную фабрику или DIC все пробрасывается легко, loadById($id): Person, save(Person $person) - обычные такие себе методы. Вроде теперь вообще все выглядит красиво (хотя внутри этих методов, конечно, будет адок).

Теперь посмотрим, что нам предлагают типичные "универсальные" ORM-ки, и ужаснемся:
1) Предлагают они в основном странное: либо испортить базу данных, сделав единственную табличку связей с полем type (до свидания, foreign keys), либо испортить ООП, раскидав общую коллекцию на пачку коллекций по типам: Collection<PhoneNumbers> $phoneNumbers, Collection<Email> $emails... А скорее всего и то, и другое. Вот он и impedance mismatch.
2) Плюс к тому, большинство ActiveRecord ORM-ок потребуют от нас ручками писать $phoneNumber->save(), $email->delete()... Одним методом $person->save() не обойдешься. (Нет, push() из Eloquent тут не поможет.)

Что же теперь, писать вот такие портянки SQL-я в save() и load() - как в нашем мысленном эксперименте - ручками на каждый Aggregate Root, присоединившись к лагерю противников ORM? Да в принципе-то нет. В условных 90% случаев стандартные правила из универсальных ORM прекрасно сработают. А для 10% надо лишь дать механизм, позволяющий определить собственные схемы маппинга там, где стандартных правил недостаточно.

Получается, что качество ORM в смысле решения проблемы impedance mismatch определяется двумя факторами:
1) поддержка концепции Aggregate Root,
2) возможность (и удобство) создания кастомных мапперов.

В CycleORM с этим почти все хорошо, с доктриной надо местами побороться и покостылить, а с типичными реализациями Active Record-ов это вообще не решаемо. Но это не значит, что Active Record принципиально не годится. Концептуально Active Record или Data Mapper - вообще не так уж и важно (единственное отличие - это проблема передачи $db в инстанс ActiveRecord Entity, ну и нарушение принципа SRP). Дело тут в том, что существующие реализации Active Record а-ля Rails по сути сводят все к Table Gateway со связями, даже не пытаясь дать решение проблемы impedance mismatch.
 
Последнее редактирование:

fixxxer

К.О.
Партнер клуба
Не очень структурированный поток мыслей получился, но, надеюсь, более-менее понятно, что я хотел сказать. :)
 

Yoskaldyr

"Спамер"
Партнер клуба
@fixxxer Хорошо расписал! :)
А что думаешь насчет read и write моделей в контексте современных ормов?
 

fixxxer

К.О.
Партнер клуба
А что думаешь насчет read и write моделей в контексте современных ормов?
Все вышенаписанное - это про write models, конечно. Чтобы ввести понятие read models, надо сначала вообще понять, зачем они нам нужны. Это ещё столько же писать, как-нибудь потом. :)
 

fixxxer

К.О.
Партнер клуба
Какой на ваш взгляд сейчас лучший класс в фреймворках для работы с базой данных?
Например изучая Laravel и Symfony (doctrine) я понял что это совершенно разные подходы к моделям.
Модели и база данных, за исключением частного случая ActiveRecord, не имеют прямого отношения друг к другу.

Модели в смысле Domain Models вообще никак не привязаны к фреймворку: они описывают предметную область, которая в каждом проекте своя. (Понятно, что может быть узкоспециализированный фреймворк - например, для интернет-магазинов, в котором модели предметной области реализованы в обобщённом виде, - но мы ведь сейчас об универсальных фреймворках говорим).

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

И, да, если ты перестанешь ходить вокруг да около, и вместо вопросов из разряда "хочу странного" опишешь изначальную постановку задачи с точки зрения бизнеса, будет намного продуктивнее. :)
 

Yoskaldyr

"Спамер"
Партнер клуба
Все вышенаписанное - это про write models, конечно.
Да это понятно что для write моделей :)
Только вот народ вот ооочень любит юзать совмещенные модели для read и write особенно когда ормы и примеры работы ормов этому способствуют (даже примеры кода рид моделей что выкладывали здесь на форуме были ну мягко говоря с душком, из-за тяжелого наследия орма, который не совсем для этого подходит). Да tutorial driven development, но именно так херово все начинают что-то делать, а потом остается как легаси и не переписывают ибо лень или привыкли. И это статистика, т.к. присутствует почти во всем коде что есть в паблике (гитхаб гитлаб и т.д.)
 

fixxxer

К.О.
Партнер клуба
народ вот ооочень любит юзать совмещенные модели для read и write
Это закономерное следствие подхода а-ля table gateway: вот инсерт, вот апдейт, вот селект. :)

Тут, опять же, виноват не сам ActiveRecord, а рельсоподобная архитектура. Там, где ее нет, обычно разделение так или иначе присутствует, потому что без ментального наследия Рельсов довольно затруднительно все это невпихуемое впихать в одну кучу. Вон даже в твоём любимом xenforo, в котором ООП не то чтобы ООП, в скорее полупроцедурщина, несмотря на терминологическую путаницу (то, что у них Model, им следовало бы назвать... ну, скажем, DataReader), разделение настолько очевидно, что непонятно, как иначе. При этом DataWriter-ы там ну почти что ActiveRecord (хоть бизнес-логика зачастую и снаружи), а Read Models - это на самом деле те ассоциативные массивы, которые возвращаются из наследников некорректно поименованного Xenforo_Model.
 

Yoskaldyr

"Спамер"
Партнер клуба
Вон даже в твоём любимом xenforo,
кавычки забыл, надо было так:
"любимом" xenforo

Вообще-то я имел ввиду именно tutorial driven development на базе популярных фреймворков/ормов которое кочует в продакшн (что и понятно в доках так же написано - значит так и будем делать).
 

fixxxer

К.О.
Партнер клуба
tutorial driven development
Это понятно, спинным мозгом проще работать, чем головным.

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

Yoskaldyr

"Спамер"
Партнер клуба
Впрочем, если головным мозгом задуматься, становится понятно, что не стоит искать хороших примеров на проектирование слоя domain в документации на инфраструктурные библиотеки.
Именно! Но вот в доках этих инфраструктурных библиотек как раз "доменные" примеры "из жизни"
 

fixxxer

К.О.
Партнер клуба
Прям интересно, как записать в базу collection<contact> не использовав полиморфную связь (не испортив базу)
А я ж там написал, как. :)

Но проще размышлять не так. Проще спроектировать структуру базы, не думая о том, что там где-то collection (подойти к проблеме со стороны объектов и со стороны РСУБД независимо), а потом посмотреть, как одно на другое мапится.
 

Yoskaldyr

"Спамер"
Партнер клуба
@fixxxer Почему меня наверно бомбит насчет того как сейчас работают с орм-ами, потому что сейчас такое гуано везде. Ормовские модели, не только для write моделей используют, но и для Read и еще в придачу туда кучу ViewModel логики запихнут. Т.е. одна модель которая умеет вообще все. Как результат - желаем веселого дебага с нежданчиками в шаблонах... А если взять в расчет неявное кеширование ентити во многих ормах, то вообще все очень весело. И самое печальное что это норма!
 

Yoskaldyr

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