Tree: дерево в таблице
DevBx\Core\Internals\Tree — абстрактный наследник Bitrix\Main\Entity\DataManager для
таблиц-деревьев по схеме вложенных множеств. Тот же приём, на котором стоит
Bitrix\Sale\Location\LocationTable в самом Битриксе: у каждого узла хранятся границы
поддерева, и поддерево читается одним запросом без рекурсии.
Пространство имён Internals означает «служебное»: это не публичная договорённость модуля, и
подпись методов может измениться без предупреждения.
Что должно быть в карте полей
| Поле | Тип | Зачем |
|---|---|---|
ID |
целое, первичный ключ, автозаполнение | |
PARENT_ID |
целое | родитель, 0 — корень |
LEFT_MARGIN, RIGHT_MARGIN |
целые | границы поддерева |
DEPTH_LEVEL |
целое | глубина, у корней — единица |
namespace My\Module;
use Bitrix\Main\ORM\Fields;
use DevBx\Core\Internals\Tree;
class CategoryTable extends Tree
{
public static function getTableName()
{
return 'my_module_category';
}
public static function getMap()
{
return [
(new Fields\IntegerField('ID'))->configurePrimary()->configureAutocomplete(),
new Fields\IntegerField('PARENT_ID'),
new Fields\IntegerField('LEFT_MARGIN'),
new Fields\IntegerField('RIGHT_MARGIN'),
new Fields\IntegerField('DEPTH_LEVEL'),
new Fields\StringField('NAME'),
];
}
}
Запись
CategoryTable::add(['NAME' => 'Техника']); // корень
CategoryTable::add(['NAME' => 'Чайники', 'PARENT_ID' => $id]); // ветка
CategoryTable::update($id, ['PARENT_ID' => $otherId]); // перенос поддерева
CategoryTable::delete($id); // узел вместе с поддеревом
Границы и глубину класс считает сам — передавать LEFT_MARGIN, RIGHT_MARGIN и DEPTH_LEVEL
не нужно, при обычной записи они всё равно вычёркиваются.
Узел без родителя встаёт в конец: это лес, а не одно дерево, корней может быть сколько угодно.
Смена PARENT_ID в update переносит весь узел с потомками. Класс не даст назначить узел
родителем самому себе и не даст перенести его внутрь собственного поддерева — обе попытки
вернутся ошибкой поля, а не исключением.
Массовая загрузка
Каждая вставка внутрь дерева раздвигает границы соседей — на тысяче узлов это тысяча перестроений. Для загрузки «с нуля» перестроение отключают и делают один раз в конце:
foreach ($rows as $row) {
CategoryTable::addExtended($row, ['REBALANCE' => false]);
}
CategoryTable::resort();
resort() обходит дерево в глубину и пересчитывает границы всем узлам. Вызывать его нужно в
транзакции: посреди пересчёта дерево несогласованно.
Проверить результат: CategoryTable::checkIntegrity() — true, если ни у одного узла границы
не пусты.
Удаление
CategoryTable::deleteExtended($id, ['DELETE_SUBTREE' => false]); // только сам узел
По умолчанию удаляется всё поддерево. Потомки удаляются прямым запросом, поэтому обработчики
событий DataManager на них не сработают: ни onDelete, ни onAfterDelete. Если на удалении
узла у вас висит очистка связанных данных, для поддерева её нужно делать самому — например,
собрав номера через getSubTree до удаления.