Comments 9
А можно просто использовать схему наподобие Nested Sets (те что с lkey rkey level) и не переизобретать ничего :)
Можно, конечно. Но я решал конкретную проблему на той структуре, что уже была на проекте. Менять её не планировалось, т.к. работало всё сносно. Если уж на то пошло, то тягать One-to-Many связь через сущность -- вообще такое себе. Лучше было бы вообще сырой запрос через Doctrine DBAL сделать и всё. Но опять же, подход зависит от ситуации.
Спасибо. Всегда приятно читать о таких практических кейсах. Вот прямо очень не хватает на Хабре именно такого рода статей.
решение проблемы в данной статье рассчитано именно на использование adjacency list
Мне кажется, вы здесь не совсем верно атрибутировали решение, поскольку оно полностью построено на работе с uid, то есть решение фактически рассчитано на использование materialized path, а с чистым списком смежности работать, разумеется, не будет.
У вас, кстати, баг в запросе. Вы не экранируете подчеркивание. То есть, запрос LIKE 222_%
найдет как детей категории 222, так и детей всех категорий от 2220 до 2229. Хотя если у вас фиксированная длина uid, то это, конечно, не страшно. Но в этом случае тогда его надо или совсем убрать, или лучше оставить, но экранировать. Причем не знаю, как в постгресе, а в мускуле надо экранировать дважды. То есть в запросе надо получить строку видаuid\\_%
Ну и совсем мелочь. Неужели sprintf('%s_%%', $uid))
читается проще, чем "{$uid}_%"
? Вот мне в первом случае физически приходится продираться через эти нолики, чтобы понять кто есть ху.
Спасибо. Всегда приятно читать о таких практических кейсах. Вот прямо очень не хватает на Хабре именно такого рода статей.
Благодарю за отзыв :)
Мне кажется, вы здесь не совсем верно атрибутировали решение, поскольку
оно полностью построено на работе с uid, то есть решение фактически
рассчитано на использование materialized path, а с чистым списком
смежности работать, разумеется, не будет.
Судя по всему, я зря усложнил сущность, добавив в неё UID. Это сбивает с толку. Там в тестах выбирается коллекция дочерних сущностей через Category::children()
, materialized path при этом не задействуется, только связь One-to-Many. А метод findDescendantsByUid
намеренно вытаскивает всех потомков подряд (со всех уровней), чтобы было проще потом Category::children()
тестировать.
У вас, кстати, баг в запросе. Вы не экранируете подчеркивание. То есть, запрос LIKE
222_%
найдет как детей категории 222, так и детей всех категорий от 2220 до
2229. Хотя если у вас фиксированная длина uid, то это, конечно, не
страшно.
Честно говоря, не понял, откуда возьмётся 2220-2229.
Ну и совсем мелочь. Неужели sprintf читается лучше, чем интерполяция строк? Вот мне в первом случае физически приходится продираться через эти нолики, чтобы понять кто есть ху.
Действительно. Что-то я к этому sprintf() так привык уже, что использую его везде :)
P.S. Извинияюсь за неточную цитату: Хабр некорректно обрабатывает код, заключенный в тег code
, внутри цитат. Я честно пытался.
Так, давайте разбираться.
Честно говоря, не понял, откуда возьмётся 2220-2229.
Здесь все просто. Как говорил старик Тровальдссон, 5 строк кода стоят тысячи слов, и я с ним полностью согласен! (И снова поблагодарим rozhnev за такой замечательный сервис!)
По поводу же механизма, я, честно говоря, в непонятках.
Судя по всему, я зря усложнил сущность, добавив в неё UID…
Вот тут я перестал вообще что-либо понимать.
Можете показать сам SQL запрос, который решает поставленную задачу — выбрать всех детишек одним запросом — нo при этом без использования UID?
Здесь все просто. Как говорил старик Тровальдссон, 5 строк кода стоят тысячи слов, и я с ним полностью согласен! (И снова поблагодарим @rozhnev за такой замечательный сервис!)
Теперь дошло! На проекте с такой проблемой не сталкивался.
Вот тут я перестал вообще что-либо понимать.
Можете показать сам SQL запрос, который решает поставленную задачу —
выбрать всех детишек одним запросом — нo при этом без использования UID?
Метод Category::findDescendantsByUid()
из статьи как раз вытягивает всех потомков за один запрос. Но проблема в том, что если мы у любого из них будем вызывать метод связи children()
, то Doctrine будет каждый раз делать запрос в БД:
$descendants = $repo->findDescendantsByUid('foo');
// без фикса тут будет +2 запроса в БД
$descendants[0]->children()->first()->children()->first();
Фикс заключается в ручной сборке вложенных коллекций таким образом, чтобы Doctrine не пыталась загружать их заново каждый раз.
На проекте с такой проблемой не сталкивался.
Вы, все-таки, посмотрите повнимательнее. Видимо, сейчас у вас все категории идут с одной разрядностью. Но если в какой-то момент добавится разряд, то проблемы начнутся в полный рост.
чтобы Doctrine не пыталась загружать их заново каждый раз.
Да, это логично. Меня просто смутили ваши слова о ненужности UID, хотя как раз на это поле-то и завязан весь механизм получения детей одним запросом.
Плюс, как я писал выше, из текста статьи складывается впечатление, что задача заключалась в той самой оптимизации. Но только в комментариях выясняется, что сама по себе эта оптимизация у вас уже была, но Доктрина её не видела. И задача заключалась не в том, чтобы сделать оптимизацию, а в том, чтобы подружить её с Доктриной.
В итоге сама оптимизация отодвигается на второй план, что, на мой взгляд, и незаслуженно, и делает статью менее информативной.
with recursive cat_tree as (
select id, name from categories where id = 1
union all
select categories.id, categories.name from categories
join cat_tree on cat_tree.id = categories.parent_id
) select * from cat_tree;
рекурсивный запрос который выберет всех детишек
https://sqlize.online/sql/psql15/bbf907629c612304c0a34b1526b6b525/
Вместо 1 подставить нужную цифру
Да, я подобными запросами вытаскивал полное название категории:
-- all ancestors
select string_agg(name, ' • ') from (
with recursive
ancestor (id, uid, name) as
(
select c.parent_category_id, c.uid, c.name
from category c
-- или какое-то другое условие
where c.uid = 'some_child_uid'
union all
select a.parent_category_id, a.uid, a.name
from ancestor, category c
where a.category_id = ancestor.id
)
select name from ancestor
order by uid
) as tbl;
-- all descendants
select string_agg(name, ' • ') from (
with recursive
descedant (id, uid, name) as
(
select c.category_id, c.uid, c.name
from category c
-- или какое-то другое условие
where c.uid = 'some_uid'
union all
select c.category_id, c.uid, c.name
from descedant, category c
where c.parent_category_id = descedant.id
)
select name from descedant
order by uid
) as tbl;
Оптимизация OneToMany коллекций Doctrine