Дерево категорий Laravel. Как уменьшить количество запросов в БД?

В общем есть таблица категорий 81779a6023a44a3b8f080d2821dc3c23.png.
Код модели
class ProductCategory extends Model
{
    protected $table = 'product_categories';

    public function ProductCategory(){
        return $this->hasMany($this, 'parent_id');
    }

    public function rootCategories(){
        return $this->where('parent_id', 0)->with('ProductCategory')->get();
    }

}


Код контроллера
class ProductCategoryController extends Controller
{
    public function index(ProductCategory $productCategory){
        $rootCategories = $productCategory->rootCategories();
        return view('layouts.catalog', ['rootCategories' => $rootCategories,]);
    }
}


Код шаблона 'layouts.catalog'
<h1>Catalog</h1>
                <ul>
                    @foreach($rootCategories as $rootCategory)
                        <li>{{ $rootCategory->name }}</li>
                        @if($rootCategory->ProductCategory->count() > 0)
                            @include('layouts.partials.treeChildMenu', ['categories' => $rootCategory->ProductCategory])
                        @endif
                    @endforeach
                </ul>


И код шаблона 'layouts.partials.treeChildMenu'
<ul>
    @foreach($categories as $category)
        <li>{{ $category->name }}</li>
        @if($category->ProductCategory->count() > 0)
            @include('layouts.partials.treeChildMenu', ['categories' => $category->ProductCategory])
        @endif
    @endforeach
</ul>


Всё как-бы хорошо ab6b0be707fa4a70bc40839b972e05df.png

Но пугает количество запросов в базу данных. Может это можно как-то оптимизировать? Потому что при добавлении новых пунктов меню запросов становится всё больше и на много.
  • Вопрос задан
  • 12256 просмотров
Решения вопроса 1
ThunderCat
@ThunderCat Куратор тега MySQL
{PHP, MySql, HTML, JS, CSS} developer
Выбирать все одним запросом и строить дерево рекурсивной функцией, если еще пхп не забыли.
пример
Ответ написан
Пригласить эксперта
Ответы на вопрос 8
@jacob1237
Если используете PostgreSQL, посмотрите в сторону рекурсивных подзапросов.

Если это не PostgreSQL, то можете попробовать строить дерево из "сырых данных" непосредственно в PHP (как подсказывает Вам ThunderCat ).

Но вообще лично я бы для начала оценил частоту изменений категорий в каталоге и общее количество категорий (объем данных), и в соответствии с этой информацией выбрал более подходящую модель хранения деревьев в БД.

Таких основных моделей четыре: Adjacency list, Nested Sets, Materialized Path и Closure Tables (можно рассматривать как развитие Adjacency List) плюс их вариации.

Каждый тип модели может послужить для оптимизации выборки данных из древовидных структур в БД.

Присутствуют готовые библиотеки и для Laravel:
Nested Set
Closure Tables

Для Adjacency List (вашей текущей реализации), просто сортируйте данные самостоятельно, либо попробуйте этот пакет.
Ответ написан
@potkot
PHP программист
Я бы посоветовал вам вместо обычного дерева, использовать Nested Set дерево (вложенные множества). На чистом PHP как это делается хорошо написано тут www.getinfo.ru/article610.html. Для Laravel есть хороший пакет https://github.com/etrepat/baum (сам им пользуюсь).

Пример моей таблицы
db3d3f9781c442708531a6493d4ae153.png

Плюсы такого подхода - всю ветку можно получить одни запросом.
Минусы более сложная вставка и обновление.

Я у себя с некоторых пор каталоги и страницы храню именно в таком дереве - получить хлебные крошки на странице - 1 запрос. Получить дерево каталогов - 1 запрос.
Ответ написан
AmdY
@AmdY
PHP и прочие вебштучки
Добавьте в with подгрузку ещё пары уровней with('ProductCategory', 'ProductCategory.ProductCategory', 'ProductCategory.ProductCategory.ProductCategory'), а то у вас начиная со второго уровня идёт ленивая подгрузка с большим количеством запросов. Будет пара лишних запросов, но но это не критично для большинства проектов.

Если вы грузите всё дерево целиком, то можете выбирать его так же целиком, а потом уже на стороне php собирать это дерево.
Ответ написан
Комментировать
65536
@65536
Раньше тоже так делал (обычный AL и запросы к бд рекурсивно). На километровом дереве начались тормоза. Переделал на однозапросный вариант, как уже советовали. На практике оказалось, что тормоза были не из-за этого, а из-за рендрежки дерева. На сервере с настроенным mysql_cache и редко меняющейся таблице категорий разница между 1 и 200 запросами оказалась ерундовой, но она есть. + кому-то "повезёт" подождать дерево когда кэш сбросится.

Приложу свой вариант (как раз для элоквента), два класса, сложите куда-нибудь и переименуйте как вам надо.

class Tree
{
    private $builder;

    private $parentIdFieldName = 'parent_id';

    private $nodesById = [];
    private $nodesByParent = [];

    public function __construct($builder)
    {
        $this->builder = $builder;
    }

    public function parentIdField($name)
    {
        $this->parentIdFieldName = $name;

        return $this;
    }

    /**
     * @return Output
     */
    public function get()
    {
        $nodes = $this->builder->get();

        foreach ($nodes as $node) {
            $this->nodesById[$node->id] = $node;
            $this->nodesByParent[$node->{$this->parentIdFieldName}][] = $node;
        }

        $output = new Output;

        $output->parentIdFieldName = $this->parentIdFieldName;
        $output->nodesById = $this->nodesById;
        $output->nodesByParent = $this->nodesByParent;

        return $output;
    }
}

class Output
{
    public $parentIdFieldName = 'parent_id';

    public $nodesById = [];
    public $nodesByParent = [];

    public function getNode($id)
    {
        if (!isset($this->nodesById[$id])) {
            $this->nodesById[$id] = [];
        }

        return $this->nodesById[$id];
    }

    public function getSubnodes($id)
    {
        if (!isset($this->nodesByParent[$id])) {
            $this->nodesByParent[$id] = [];
        }

        return $this->nodesByParent[$id];
    }

    private $branch;

    public function getBranch($id))
    {
        if (isset($this->nodesById[$id])) {
            $this->branch = [];
            $this->branchRecursion($id);

            return array_reverse($this->branch);
        }
    }

    private function branchRecursion($id))
    {
        $this->branch[] = $this->nodesById[$id];

        if ($this->nodesById[$id][$this->parentIdFieldName] > 0) {
            $this->branchRecursion($this->nodesById[$id][$this->parentIdFieldName]);
        }
    }
}


На вход надо подать билдер запроса, можно отфильтровать по каким-то критериям (видимость, например), отсортировать, приделать жадные загрузки и тд. Нельзя ограничивать по родительской категории, то есть никак нельзя таким способом взять поддерево определенного узла, это минус такого способа.

На выходе можно получить модель по ее ид ($tree->getNode($id)) и массив вложенных моделей модели с таким-то ид ($tree->getSubnodes($id)). Можно еще ветку получить - $tree->getBranch($id) выдаст массив моделей от корневой до той у которой ид = $id. Хлебные крошки рисовать чтобы, например

Вакуумный пример с вашей таблицей. Блейдом не пользовался, но думаю можно ему массив какой то дать или как там делается
class Test
{
    private $tree;

    private $output;

    public function treeView()
    {
        $builder = new ProductCategory; // тут можно дописать что-нибудь типа orderBy('position') или where('enabled', true); главное так чтобы любые ограничения в первую очередь исключали вышестоящие узлы (ни в коем случае не исключали нижестоящие не исключая нижестоящие), иначе дерево получится кривое - недостаток способа

        $this->tree = (new Tree($builder))->get();

        $this->treeRecursion(0); // здесь уже можно указать ид корневого узла для дерева на выходе

        return implode('<br>', $this->output);
    }

    private $level = 0;

    private function treeViewRecursion($id)
    {
        $node = $this->tree->getNode($id);
        $subnodes = $this->tree->getSubnodes($id);

        $this->output[] = str_repeat('--', $this->level) . ' ' . $node->name;

        if ($subnodes) {
            $this->level++;

            foreach ($subnodes as $subnode) {
                $this->treeViewRecursion($subnode->id);
            }

            $this->level--;
        }
    }
}

print (new Test)->tree();


По-хорошему лучше вообще хранить хтмл кэш готового дерева и заменять его после каждой операции над ним
Ответ написан
Комментировать
@deliro
Использовать MPTT. Позволяет делать почти все нужные операции за 1 запрос: выборка семейства, поддерева, сыновей, листьев, потомков, предков.
Ответ написан
Комментировать
@Miron11
Пишу sql 20 лет. Срок :)
Для MySQL было бы хорошо подтвердить что таблица категорий создана с поддержкой InnoDB. Если так, то проверьте если есть, и если нет, то добавьте индекс
CREATE INDEX IX_product_categories_NC1 ON product_categories ( parent_id, name, id );
Ответ написан
@vintkor Автор вопроса
Всем спасибо. Сделал как подсказал ThunderCat
Ответ написан
Комментировать
@senslab
Models/Catalog.php
public static function getCategories() {
        // Получаем одним запросом все разделы
        $arr = self::orderBy('name')->get();

        // Запускаем рекурсивную постройку дерева и отдаем на выдачу
        return self::buildTree($arr, 0);
}


// Сама функция рекурсии
public static function buildTree($arr, $pid = 0) {
        // Находим всех детей раздела
        $found = $arr->filter(function($item) use ($pid){return $item->parent_id == $pid; });

        // Каждому детю запускаем поиск его детей
        foreach ($found as $key => $cat) {
            $sub = self::buildTree($arr, $cat->id);
            $cat->sub = $sub;
            }

        return $found;
    }


Все подкатегории улетают в свойство sub категории.
Запрашиваем в контроллере категории:
public function index() {
        $categories = Catalog::getCategories();

        return view('Catalog.index')
            ->withCategories($categories);
    }


И в виде выводим категории рекурсивно.
catalog.partial.show:
<ul>
    @foreach($categories as $category)
        <li>
            {{$category->name}}
            @if($category->sub->count())
                    @include('catalog.partial.show', ['categories' => $category->sub])
            @endif
        </li>
    @endforeach
</ul>
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы