Nested Sets и Data Mapper

WMix

герр M:)ller
Партнер клуба
кто нибудь Nested Sets в cycle-orm использует?
я тут мучаюсь, создал
PHP:
return [
  'node' => [
    Schema::ENTITY      => Node::class,
    Schema::RELATIONS => [
      'tree' => [
        Relation::TYPE => Relation::HAS_MANY,
        Relation::TARGET => Node::class,
          Relation::SCHEMA => [
            Relation::INNER_KEY => 'tree_id',
            Relation::OUTER_KEY => 'tree_id',
          ],
        ]
      ]
    ]
  ]
]
а дальше в qb отсекаю на left/right чтоб path получить.
PHP:
$this->select->with('tree',[
  'where' => [
    'lft' => ['>=' => new Expression('node.lft')],
    'rgt' => ['<=' => new Expression('node.rgt')]
  ]
])
не могу сообразить как Relation::WHERE сразу описать, чтоб paht всегда иметь
 
Последнее редактирование:

fixxxer

К.О.
Партнер клуба
Я бы отдельно хранил сами сущности, а отдельно структуру дерева, с которой проще работать безо всяких ORM.
Агрегат в виде корня дерева, содержащий прям все дерево domain entities в памяти, я себе с трудом представляю.
Но это с cqrs, при смешении чтения и записи понимаю, что надо страдать.
 

WMix

герр M:)ller
Партнер клуба
это исключительно для выборки (только with и ни какого load), к каждой ветеке привязан список, задача получить все списки от ветки до корня,
у ветки нет property path, агрегатом для списка будет выбранная ветка. тк. добавляются списки действительно на одну ветку
или ты к тому, что нет смысла для чтения orm использовать?
 
Последнее редактирование:

WMix

герр M:)ller
Партнер клуба
зачем sql в ast загогнять? пиши сразу в qb
 

fixxxer

К.О.
Партнер клуба
или ты к тому, что нет смысла для чтения orm использовать?
Если нам надо выполнить команду, то нам надо получить обратно в память тот Aggregate Root, который мы когда-то "сериализовали" в базу, и вызвать его метод(ы). Тогда-таки весь смысл ORM в этом и есть. Но, опять же, я с трудом себе представляю разумный Aggregare Root вида "корень дерева".

Если нам надо выполнить запрос, то какие у нас варианты? Допустим, можно вытащить в память пачку Aggregate Root-ов, свойства которых нам нужны для выполнения запроса, достать нужные свойства через Reflection (мы ведь соблюдаем инкапсуляцию), обсчитать всякую группировку и агрегацию прямо в PHP (у нас ведь структура Aggregate Root-ов не подразумевает полей для всяких там count(foo) и sum(bar)), и замапить это все на некий DTO, который является результатом выполнения запроса. Можно, но ужасно неэффективно, и появляется вопрос - а зачем мы тогда вообще расписывали весь этот сложный маппинг объектов на реляции, если, используя этот подход, с таким же успехом мы могли бы хранить просто ID-шники и serialize()? Зачем мы вообще заморачивались с решением проблемы Object-relational impedance mismatch? Наверное, затем, чтобы положив Aggregate Root в базу, мы могли бы делать _разные_ запросы в базу. Не только восстановить Aggregate Root в памяти, но и делать произвольные запросы, с нужными срезами, со всякими там группировками и агрегациями, и даже пересекая границы разных Aggregate Roots - поджойнить там, скажем. Результатом такого запроса являются просто данные, которые удобно представить в виде обычной структуры. struct или record в PHP нет, ну пусть будут объекты с public [readonly] полями, и называются Read Model. Маппер тут, конечно, пригодится, чтобы не создавать вручную объекты из result rows, но другой, односторонний. Автомапперы подойдут.
 

WMix

герр M:)ller
Партнер клуба
Маппер тут, конечно, пригодится, чтобы не создавать вручную объекты из result rows, но другой, односторонний.
вероятнее всего мы все ищем подобный инструмент, достаточно чтоб был select но с group by, aggregate functions и union, c маппингом на ReadEntity, со всякими proxy для удобного хождения по результату типа $order->positions[42]->product->manufacturer->name, удобным QueryBuilder и Toos типа Pagination

можешь подсказать что нибудь
 

fixxxer

К.О.
Партнер клуба
Все это сразу из коробки не надо хотеть. :) Все сразу - это layering violation.
Query builder я использую тот, который в Spiral, просто потому что он уже есть вместе с CycleORM, можно и любой другой.
Read models - это просто POPO, не вижу смысла усложнять - lazy load обычно появляется при попытках использовать domain models как read model и натянуть сову на глобус. А POPO read models и так легкие, ну и при четком разделении на "команды" и "запросы" я в запросах и так точно знаю, что именно мне понадобится. Если кажется, что и тут нужен lazy load, скорее всего на самом деле надо разделить одну read model на несколько разных.
Pagination это вообще application/view level логика, на уровне запроса - получил лимит и оффсет снаружи да и все.
Автомапперов полно, посмотри, что больше подойдет, меня в целом устраивает automapper-plus.
 
Последнее редактирование:

WMix

герр M:)ller
Партнер клуба
погоди, если я возьму Query builder из spiral и automapper-plus, сколько строк мне понадобится чтоб получить модельку типа
$order->positions[42]->product->manufacturer->name
те. в CycleORM достаточно написать грубо говоря return $this->select()->load('positions')->load('product')->load('manufacturer')
остальное из коробки и pagination также на месте. есть проблемы с group by, aggregate functions и union но возможно я еще не в теме
 

fixxxer

К.О.
Партнер клуба
Это обманчивая достаточность.
Если мне надо count(product.id) group by manufacturer, как это у тебя будет выглядеть? На объекты каких классов это вообще мапить? Как может выглядеть маппинг какого-нибудь countOfProducts обратно в базу? (спойлер: никак).

Опять же, если тебе такого достаточно, зачем мучаться и раскладывать все в базе по полочкам, почему бы не хранить все, кроме ID-шников, тупо в serialize?
 

WMix

герр M:)ller
Партнер клуба
Если мне надо count(product.id) group by manufacture
возвращаемся к доктрине?

Как может выглядеть маппинг какого-нибудь countOfProducts обратно в базу?
достаточно чтоб был select
 
Последнее редактирование:

WMix

герр M:)ller
Партнер клуба
так рассказываешь, как будто нет задачи отобразить список заказанных товаров и их производителей
а если взять если Query builder из spiral и automapper-plus, то модель чтения получается в разы сложнее, чем модель записи я уже не говорю о refactoring всего этого (имя поля изменили, разбили на 2 части и тд)
 

Вурдалак

Продвинутый новичок
я уже не говорю о refactoring всего этого (имя поля изменили, разбили на 2 части и тд)
Хаха, вот это ирония: рассуждать о сложности рефакторинга специфичных под конкретный контекст read models, защищая одну жирную модель, от которой зависит вся система и при изменениях которой потребуется править буквально весь код.
 

WMix

герр M:)ller
Партнер клуба
почему одну "жирную" модель? одно описание связей, да, одну библиотеку, да.

для записи (добавь позицию в заказ) мне названия продукта не нужно, не говоря о его производителе, в таких случаях обычно достаточно id продукта
но вот со срезами все несколько сложнее и в этом случае язык типа $this->select()->load('positions')->load('product')->load('manufacturer') очень кстати
 

fixxxer

К.О.
Партнер клуба
возвращаемся к доктрине?
Вот у меня 10 разных запросов, где-то есть группировки такие, где-то другие, где-то третьи. Что, предлагается засунуть в одну модель все варианты?
 

WMix

герр M:)ller
Партнер клуба
возвращаясь к моему вопросу
не могу сообразить как Relation::WHERE сразу описать, чтоб paht всегда иметь
получилось так
PHP:
return [
    'node' => [
        Schema::ENTITY      => Entities\Node::class,
        Schema::REPOSITORY  => Repositories\NodesRepository::class,
        Schema::RELATIONS   => [
            'subtree' => [
                Relation::TYPE   => Relation::HAS_MANY,
                Relation::TARGET => Entities\Node::class,
                Relation::SCHEMA => [
                    Relation::CASCADE   => true,
                    Relation::INNER_KEY => 'tree_id',
                    Relation::OUTER_KEY => 'tree_id',
                    Relation::WHERE => function(QueryBuilder $qb){
                        $qb
                            ->where('lft', '>=', new Expression('node.lft'))
                            ->where('rgt', '<=', new Expression('node.rgt'));
                    }
                ],
            ],
            'path' => [
                Relation::TYPE   => Relation::HAS_MANY,
                Relation::TARGET => Entities\Node::class,
                Relation::SCHEMA => [
                    Relation::CASCADE   => true,
                    Relation::INNER_KEY => 'tree_id',
                    Relation::OUTER_KEY => 'tree_id',
                    Relation::WHERE => function(QueryBuilder $qb){
                        $qb
                            ->where('lft', '<', new Expression('node.lft'))
                            ->where('rgt', '>', new Expression('node.rgt'));
                    }
                ],
            ]
        ]
    ]
];


class NodesRepository extends Repository{

    public function getParents(int $id){
        return $this
            ->select()
            ->with('subtree')
            ->where('subtree.id', $id);
    }

    public function getSubtree(int $id){
        return $this
            ->select()
            ->with('path')
            ->where('path.id', $id);
    }
}

$repo = $orm->getRepository(\Icos\Model\Entities\Node::class);

print_r($repo->getParents(7940)->fetchAll());
print_r($repo->getSubtree(7940)->fetchAll());

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

WMix

герр M:)ller
Партнер клуба
Контексты разные, а модель одна.
почему одна? нужна напиши еще одну, с учетом тонкостей
PHP:
'node2' => [
        Schema::ENTITY      => Entities\Node2::class,
]
у меня таблица клиенты это и клиенты и "приглашающие" (quellkunde называется, незнаю перевод), каталог это и сортимент для всех клиентов, и каталог для определенной "cost centre", и шаблон заказа, и "standing order" и все разные обьекты
 

MiksIr

miksir@home:~$
зачем sql в ast загогнять? пиши сразу в qb
qb никогда не сравниться с лаконичностью и читаемостью sql
а если даже sql сложен и плохо читаем - то qb будет гарантировано ужасен и нечитаем вообще
с другой стороны, если запрос конструируется динамически на основе каких-то условий - qb позволяет убрать ужасные склеивания строк
sql + qb позволят использовать обе сильные стороны - писать основу как sql и потом модифицировать через qb
 

WMix

герр M:)ller
Партнер клуба
то qb будет гарантировано ужасен и нечитаем вообще
вероятно ты qb не видел еще и рассуждаешь рамками https://spiral.dev/docs/database-query-builders

хочешь на sql
PHP:
return $this->select()->with('subtree')->where('subtree.id', $id);
этой цепочки из qb https://cycle-orm.dev/docs/query-builder-basic посмотреть
Код:
SELECT `node`.`id` AS `c0`, `node`.`pid` AS `c1`, `node`.`name` AS `c2`
FROM `node` AS `node`
INNER JOIN `node` AS `node_subtree`
    ON `node_subtree`.`tree_id` = `node`.`tree_id` AND (`node_subtree`.`lft` > `node`.`lft` AND `node_subtree`.`rgt` < `node`.`rgt`  )
WHERE `node_subtree`.`id` = ?
 
Сверху