это понятно если мы говорим о презентационном слое. А в доменном слое у нас может быть такое $post->addTag(Tag $tag) и мы не сможем корректно обработать эту ситуацию без остальных загруженных в $post десяти тегов.ElisDN писал(а):Потребность в связях и lazy load обычно возникает при первой же попытке отображать на страницах сами сущности домена. Проблема полностью исчезает, если для листингов и вывода использовать не сами сущности, а отдельные DTO с нужными для каждого листинга наборами полей. В итоге в Repository можно оставить только методы find(), add(), save() и remove(), нужные для непосредственной работы с сущностями, а все остальные для выборок с критериями, паджинациями и сортировками переместить в отдельный ReadRepository.Melodic писал(а):Ну и как тогда быть?
Сервисный слой, как правильно?
Re: Сервисный слой, как правильно?
Re: Сервисный слой, как правильно?
Для сущностей Post и Tag со своими репозиториями в Post::$tags будет всего лишь коллекция идентификаторов с добавлением по $post->addTag($tag->getId()). Сами теги для этого в посте не нужны. Так что загружать всю сущность с VO и айдишниками тегов жадно - не проблема.zelenin писал(а):А в доменном слое у нас может быть такое $post->addTag(Tag $tag) и мы не сможем корректно обработать эту ситуацию без остальных загруженных в $post десяти тегов.
А если VO слишком много для жадной загрузки, то у некоторых встречал в репозитории помимо find($id) ещё и методы вроде findWithTags($id) и т.п. для выборочной прогрузки только нужных для каждого юзкейса фрагментов.
Re: Сервисный слой, как правильно?
Post не есть агрегат без тегов. Хранение id тегов - это протечка слоя хранения в домен. Ты на книжной полке хранишь книги, а не корешки от них. Агрегат - это то, из чего можно получить коллекцию тегов $post->getTags().ElisDN писал(а):Для агрегатов Post и Tag со своими репозиториями в Post::$tags будет всего лишь коллекция идентификаторов с добавлением по $post->addTag($tag->getId()). Сами теги для этого в посте не нужны. Так что загружать всю сущность с VO и айдишниками тегов жадно - не проблема.
ну собственно варианты lazy load я разбирал выше. Это не lazy load. Ищем самый лучший вариант, чтобы по $post->getTags() получить коллекцию тегов.ElisDN писал(а):А если VO слишком много для жадной загрузки, то у некоторых встречал в репозитории помимо find($id) ещё и методы вроде findWithTags($id) и т.п. для выборочной прогрузки только нужных для каждого кейса фрагментов.
Re: Сервисный слой, как правильно?
Если Tag имеется только в посте, то его место в агрегате Post с записью по $post->addTag($tagName) и получением через $post->getTags().zelenin писал(а):Post не есть агрегат без тегов.
Если Tag - это отдельный агрегат (как Category) со своим репозиторием, то он полноценный объект с getId() и получением через $tagRepository->getAllByPostId($post->getId());
Иногда мы не грузим всё в Category и не делаем $category->addProduct($product) и $category->getProducts(), когда у нас миллиарды товаров. Логичнее делать $product->assignToCaregory($category->getId()) и получать $productRepository->getAllByCategory($category->getId(), $pager).
При таком подходе жадно подгружаем только айдишники. Ни ленивая, ни жадная загрузка самих связанных сущностей в сам агрегат не пригождается.
Последний раз редактировалось ElisDN 2016.08.04, 00:06, всего редактировалось 2 раза.
Re: Сервисный слой, как правильно?
Что происходит в методе $product->assignToCategory($category->getId())? Там все id товаров, которые принадлежат категории?ElisDN писал(а):Если Tag - это VO, то его место в агрегате Post с записью по $post->addTag($tagName) и получением через $post->getTags().zelenin писал(а):Post не есть агрегат без тегов.
Если Tag - это сущность (как Category), то он полноценный объект с getId() и получением через $tagRepository->getAllByPostId($post->getId());
Мы же не грузим всё в Category и не делаем $category->addProduct($product) и $category->getProducts(), когда у нас миллиарды товаров. Логичнее делать $product->assignToCaregory($category->getId()) и получать $productRepository->getAllByCategory($category->getId(), $pager).
Re: Сервисный слой, как правильно?
Наоборот. Один или несколько id категорий, в которых этот товар.Melodic писал(а):Что происходит в методе $product->assignToCategory($category->getId())? Там все id товаров, которые принадлежат категории?
Re: Сервисный слой, как правильно?
Ой, не внимательно прочитал название метода.ElisDN писал(а):Наоборот. Один или несколько id категорий, в которых этот товар.Melodic писал(а):Что происходит в методе $product->assignToCategory($category->getId())? Там все id товаров, которые принадлежат категории?
Re: Сервисный слой, как правильно?
репозитории создаются для агрегатов. В агрегат Post входит сущность Tag. Агрегат Post выбирается из PostRepository. На выходе я должен получить объект, который с помощью метода getTags() вернет коллекцию сущностей Tag.ElisDN писал(а):Если Tag - это сущность (как Category) со своим репозиторием, то он полноценный объект с getId() и получением через $tagRepository->getAllByPostId($post->getId());
Все верно, только ты перевернул ситуацию. Мы не в категорию добавляем товар, а товару присваиваем ОДНУ категорию. В случае с постом все ровно наоборот - мы посту присваиваем много тегов. То есть не тегу присваиваем много постов, а посту много тегов.ElisDN писал(а):Мы же не грузим всё в Category и не делаем $category->addProduct($product) и $category->getProducts(), когда у нас миллиарды товаров. Логичнее делать $product->assignToCaregory($category->getId()) и получать $productRepository->getAllByCategory($category->getId(), $pager).
Агрегат - это логическая или транзакционная единица. Пост - это сущность с тегами и автором. Она существует как единица. Единицей вытаскивается из репозитория, единицей сохраняется в репозитории. Собственно это базисное понятие DDD. Классический пример Order+OrderLines
UPD http://stackoverflow.com/a/1958765/1320921
Re: Сервисный слой, как правильно?
Виджету последних статей нужны только title, date, photo, description и вычисляемое commentsCount. Гриду в админке - title, date, categoryName, authorName, status. Странице самого поста - title, text, date, photo и все связи category, tags, author, comments, relatedPosts.Melodic писал(а):Т.е. для в каждом экшене контроллера формировать свой DTO, который и будет отображаться во вьюхе?
Возвращать массивы оригинальных сущностей Post с полной грудой данных - слишком прожорливо. Логично для каждой выборки сделать отдельные DTO только с нужными данными и возвращать их из getLatestPosts($limit) и т.п.
Re: Сервисный слой, как правильно?
Да. Всё верно. У Эванса:zelenin писал(а):Агрегат - это логическая или транзакционная единица. Пост - это сущность с тегами и автором. Она существует как единица. Единицей вытаскивается из репозитория, единицей сохраняется в репозитории. Собственно это базисное понятие DDD. Классический пример Order+OrderLines.
и в ответе:The root is the only member of the AGGREGATE that outside objects are allowed to hold references to.
Сущностью (ну и агрегатом) является объект, обладающий идентификатором. По нему и извлекается и сохраниется в репозиторий. Только на сущности и агрегаты можно ссылаться извне из других объектов и создавать репозитории (так как есть идентификатор).This means that aggregate roots are the only objects that can be loaded from a repository.
An example is a model containing a Customer entity and an Address entity. We would never access an Address entity directly from the model as it does not make sense without the context of an associated Customer. So we could say that Customer and Address together form an aggregate and that Customer is an aggregate root.
А агрегат состоит из вложенных сущностей с идентификатором и ValueObjects без глобального идентификатора, существующих только внутри агрегата. Методами самого агрегата они создаются, редактируется и удаляются. Ни одна другая сущность не может получить доступ к чужому VO в обход самого агрегата.
Как Address - это просто часть в Customer, так и массив из OrderLine у нас имеется только в Order. Мы их создаём через методы агрегата $customer->changeAddress($country, $city) и $order->addLine($product->getId(), $price, $count). И просматриваем их от $customer->getAddress() и $order->getLines(). Мы никогда не выводим в админке напрямую грид из Address без Customer или OrderLines без Order. Это не имеет смысла. У OrderLines будет локальный идентификатор, но он снаружи никому не интересен. И, соответственно, для них у нас нет репозитория.
Если Tag и Author - просто обёртки над string, то да, их сохраняем прямо в Post в $postRepository.
А если Tag, Author (User) и Category являются полноценными агрегатами со своими первичными getId(), то это уже сложно. Мы выводим их в отдельных CRUDах, добавляем, переименовываем, удаляем. Получаем их список для вывода облака или списка в виджете и ссылаемся на них из разных постов, следим за уникальностью. Это отдельные полноценные сущности или агрегаты, имеющие свои репозитории.
Так что свои запчасти VO мы храним внутри агрегата, а на другие агрегаты для экономии связей удобнее ссылаться по id. Если вдруг из двух агрегатов потребуется сослаться на одну запчасть или выводить и редактировать её отдельно, то это повод вынести её в отдельный агрегат с репозиторием. А в оригинальном DDD действительно можно всё друг в друга вкладывать.
Последний раз редактировалось ElisDN 2016.08.04, 00:13, всего редактировалось 3 раза.
Re: Сервисный слой, как правильно?
все верно: Post может быть агрегатом, состоящим из сущностей Post и Tag, извлекаемым из PostRepository. Tag может быть агрегатом из одной сущности Tag, извлекаемым из TagRepository.ElisDN писал(а):Сущностью (ну и агрегатом) является объект, обладающий идентификатором. По нему и извлекается и сохраниется в репозиторий. Только на сущности и агрегаты можно ссылаться извне из других объектов и создавать репозитории (так как есть идентификатор).
Агрегат состоит и из VO, не имеющих id, и из Entity, имеющих id. Агрегат состоит из объектов. Агрегат не содержит Id вместо сущностей.ElisDN писал(а):А агрегат состоит из вложенных ValueObjects - объектов без глобального идентификатора, существующих только внутри агрегата.
http://i.imgur.com/SEq8DP2.png
https://github.com/idr0id/ddd-blog/blob ... y/User.php
у OrderLine есть не локальный идентификатор, а локальная идентифицируемость с помощью id, в отличии от VO, которые локально не идентфиицируемы. То есть локальный иднетификатор - это не какой id, который отличается от глобального, а тот же самый, по которому мы можем orderline изменить напрямую в составе собственного агрегата. поэтому это сущность, она однозначна идентифицируема, и конкретная одна из десяти может быть мною изменена. Я могу увеличить count OrderLine(id=15) у Order(id=2) с 3 на 4, однозначно выбрав определенный OrderLine из имеющихся. С VO я так сделать не могу.ElisDN писал(а):Как Address - это просто часть в Customer, так и массив из OrderLine у нас имеется только в Order. Мы их создаём через методы агрегата $customer->changeAddress($country, $city) и $order->addLine($product->getId(), $price, $count). И просматриваем их от $customer->getAddress() и $order->getLines(). Мы никогда не выводим в админке напрямую грид из Address без Customer или OrderLines без Order. Это не имеет смысла. У OrderLines будет локальный идентификатор, но он снаружи никому не интересен. И, соответственно, для них у нас нет репозитория.
а оно и не должно быть VO. Tag может быть частью агрегата Post, когда мы редактируем пост (/post/16/edit), а может быть частью агрегата Tag, когда мы добавляем теги в гриде (/tag/index), как ты и сказал.ElisDN писал(а): А если Tag, Author (User) и Category являются полноценными сущностями со своими первичными getId(), то это уже не VO.
но вот это все измышления. В DDD мы оперируем сущностями, а не ID.ElisDN писал(а):Так что свои запчасти VO мы храним внутри агрегата, а на другие сущности или агрегаты ссылаемся по id. Если вдруг из двух агрегатов потребуется сослаться на одну запчасть или выводить и редактировать её отдельно, то это повод вынести её в отдельную сущность с getId() и репозиторием.
ну и Вон Вернон, автор второй популярной книги по ddd, разжевавший все вводные обтекаемые концепты Эванса - https://github.com/VaughnVernon/IDDD_Sa ... /Team.java
Есть агрегат Team, есть связанные сущности - однозначно идентифицируемые сущности, без намека на VO-шность - члены команды и владелец продукта. Связанные сущности добавляются в корень агрегата объектами. В этом весь ddd. Никаких id.
http://blog.byndyu.ru/2010/05/domain-driven-design.html решение 3 - аналогично добавляем сущность Заказ к юзеру.
Re: Сервисный слой, как правильно?
Да, в идеальном DDD в каждом контексте всё нужно делать вложенными сущностями и коллекциями по решению 3. Но на практике с точки зрения производительности часто приходится нарушать инкапсуляцию и уступать в сторону варианта 2 c Service::addOrder($account, $order). Иначе, как уже затронули в теме, нужно либо реализовывать ленивые загрузки через прокси, либо загружать все связи сразу жадно, либо указывать дополнительным параметром в find(), какие именно связи сейчас нужны; либо для проверок в addOrder() инъектить сервис с репозиторием в сущность вроде addOrder(Order $order, OrderChecker $checker), который бы вместо foreach ($this->orders) { if (...) } дёргал $repository->hasOrders($criteria) или в $this->orders хранить Query-объект, либо для каждого контекста делать отдельную сущность только со своими связями... Ведь непонятно, как с этим жить, когда связанных данных тысячи или миллионы и ещё есть связи связей. Здесь я, увы, ещё не познал дзенzelenin писал(а):В DDD мы оперируем сущностями, а не ID.
Последний раз редактировалось ElisDN 2016.05.26, 09:55, всего редактировалось 2 раза.
Re: Сервисный слой, как правильно?
собственно об этом и рассуждали - как познать дзен без боли. Имхо самый приемлимый способ - инджектить ресолвером перед обращением к связаным сущностям. Либо проксировать, но отказываемся от final в угоду реализации другого слоя, что не оч. корректно.ElisDN писал(а):Да, в идеальном DDD в каждом контексте всё нужно делать вложенными сущностями и коллекциями по решению 3. Но на практике с точки зрения производительности часто приходится нарушать инкапсуляцию и уступать в сторону варианта 2 c Service::addOrder($account, $order). Иначе, как уже затронули в теме, нужно либо реализовывать ленивые загрузки через прокси, либо загружать все связи сразу жадно, либо указывать дополнительным параметром в find(), какие именно связи сейчас нужны; либо для проверок в addOrder() инъектить сервис с репозиторием в сущность вроде addOrder(Order $order, OrderChecker $checker), который бы вместо foreach ($this->orders) { if (...) } дёргал $repository->hasOrders($criteria) или в $this->orders хранить Query-объект, либо для каждого контекста делать отдельную сущность только со своими связями... Ведь непонятно, как с этим жить, когда связанных данных тысячи или миллионы и ещё есть связи связей. Здесь я, увы, ещё не познал дзенzelenin писал(а):В DDD мы оперируем сущностями, а не ID.
Re: Сервисный слой, как правильно?
Есть Order, в нём есть Product[] $products. Есть метод Order::removeProduct(Product $product), который просто удаляет определёный продукт из массива $products. Как репозиторий узнает, что нужно отвязать удалёный продукт в БД?
Re: Сервисный слой, как правильно?
array_diff типа. Доктрина из коробки это делает.Melodic писал(а):Есть Order, в нём есть Product[] $products. Есть метод Order::removeProduct(Product $product), который просто удаляет определёный продукт из массива $products. Как репозиторий узнает, что нужно отвязать удалёный продукт в БД?
Re: Сервисный слой, как правильно?
Что бы не городить свой велосипед, стоит использовать Доктрину?zelenin писал(а):array_diff типа. Доктрина из коробки это делает.Melodic писал(а):Есть Order, в нём есть Product[] $products. Есть метод Order::removeProduct(Product $product), который просто удаляет определёный продукт из массива $products. Как репозиторий узнает, что нужно отвязать удалёный продукт в БД?
Re: Сервисный слой, как правильно?
я бы не стал - слишком тяжела.Melodic писал(а):Что бы не городить свой велосипед, стоит использовать Доктрину?zelenin писал(а):array_diff типа. Доктрина из коробки это делает.Melodic писал(а):Есть Order, в нём есть Product[] $products. Есть метод Order::removeProduct(Product $product), который просто удаляет определёный продукт из массива $products. Как репозиторий узнает, что нужно отвязать удалёный продукт в БД?
Re: Сервисный слой, как правильно?
Т.е. альтернатив никаких нет? Придётся свой велосипед делать?)zelenin писал(а):я бы не стал - слишком тяжела.Melodic писал(а):Что бы не городить свой велосипед, стоит использовать Доктрину?zelenin писал(а): array_diff типа. Доктрина из коробки это делает.
И что бы сделать array_diff, нужно будет ещё раз загрузить Order?
Re: Сервисный слой, как правильно?
ну а что там делать? в методе save делаете проверку. Как? Либо напрямую запросами, либо с помощью какого-то реестра, в котором будут храниться начальные сущности. Опять же вы не сможете создать сущность, не сгенерировав id в приложении (мы общались об этом в личке).Melodic писал(а):Т.е. альтернатив никаких нет? Придётся свой велосипед делать?)zelenin писал(а):я бы не стал - слишком тяжела.Melodic писал(а):
Что бы не городить свой велосипед, стоит использовать Доктрину?
Мое мнение насчет доктрины: лучше сделать в пять раз менее функциональное решение, но в пять раз более быстрое.
Re: Сервисный слой, как правильно?
existById/existByIds - собственно можно что-то и другое придумать. Реализация ваша.Melodic писал(а):И что бы сделать array_diff, нужно будет ещё раз загрузить Order?