Nested Sets библиотека

Z6
На сайте с 12.10.2004
Offline
66
1347

Господа, кто нить пробовал http://php.russofile.ru/ru/authors/sql/nestedsets01 ?

Попробовал использовать в своей cms, вместо структуры базы id, pid,... , вывод-то наладил.

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

<select name="categories_parent_id">

<option value="0">Без категории</option>
<option selected value="1">Root</option>
<option value="3">&nbsp;&nbsp;&nbsp;Node 2</option>
<option value="7">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Subnode 1</option>
<option value="8">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Subnode 2</option>
<option value="9">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Subnode 3</option>
<option value="4">&nbsp;&nbsp;&nbsp;Node 3</option>
<option value="10">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Subnode 1</option>
<option value="12">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Subsubnode 1</option>
<option value="11">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Subnode 2</option>
<option value="5">&nbsp;&nbsp;&nbsp;Node 4</option>
<option value="6">&nbsp;&nbsp;&nbsp;Node 5</option>
</select>

ну и потом в методе так:


if ($msg_error == "")
{if($msg_error == "" and $parent_info[$table . "_id"] != post($table . '_parent_id'))
$this->_dbtree->MoveAll($id, post($table . '_parent_id'), '');

$this->_model->edit_structure_row($table, $insert, $id);
redirect("admin/structure/" . $table);
}
.

Собственно если родитель категории меняется то мы ее $this->_dbtree->MoveAll, потом остальные изменения полей (тайтл, контент, дескрипшн, атрибуты разные).

В общем вопрос такой: после $this->_dbtree->MoveAll как то нереально все переколбашивает, то есть бывает что рут вдруг станет чьим то сыном, однако над ним операций не проводилось (он скрыт от пользователя вообще). Как быть?

V
На сайте с 03.12.2007
Offline
94
#1

использую в многих проектах, правда переделанную и с другим драйвером базы данных. все работает отлично

vavenko добавил 14.09.2011 в 11:09

покажите свой MoveAll

Z6
На сайте с 12.10.2004
Offline
66
#2

То есть то что я показал должно работать? :)

Может там надо как то переструктурировать после МувОл() еще одним запросом?

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

Если глубина не больше 4, ну край, 5 уровней, а категорий в базе не больше 1000 (вообще не больше 100 по идее) есть ли смысл в nested sets?

ze6ra добавил 14.09.2011 в 11:14

Весь метод редактирования статьи (со всей его кривизной):

function edit(){

if (!$this->_auth->access('edit_category')) $this->no_access();
$table = $this->param(0);
$id = $this->param(1);

if (($table != '' and !is_numeric($table)) and ($id != '' and is_numeric($id))) {
$row = $this->_model->get_row($table, $table . "_id=" . sql($id));

require_once(DIR_FW . 'libs' . DS . 'dbtree.php');
$this->_dbtree = new dbtree($table, $table, $this->_model);
$tree = $this->_dbtree->Full('');
$parent_info = $this->_dbtree->GetParentInfo($id);

#Array to edit
$insert = $row;
$insert[$table . '_url'] = post(urldecode($table . '_url'));
$insert[$table . '_title'] = post($table . '_title');
$insert[$table . '_seo_title'] = post($table . '_seo_title');
$insert[$table . '_content'] = post($table . '_content');
$insert[$table . '_description'] = post($table . '_description');
$insert[$table . '_seo_description'] = post($table . '_seo_description');

$msg_error = "";
if (post($table . '_title') != "" or post($table . '_url') != "")
{
if (trim(post($table . '_title')) == "")
$msg_error .= "Не введено название.<br />";
if (trim(post($table . '_url')) == "" or "" != $this->_model->get_result($table, $table . "_url", $table . "_url=" . sql(post('url')) . " AND " . $table . "_id!=" . sql($id, 'num')))
$msg_error .= "Не введен url статьи или этот url уже занят.<br />";

if ($msg_error == "")
{
if($msg_error == "" and $parent_info[$table . "_id"] != post($table . '_parent_id'))
//echo "Parent has changed!";
$this->_dbtree->MoveAll($id, post($table . '_parent_id'), '');

//echo($parent_info[$table . "_id"]);
//p($_POST);
$this->_model->edit_structure_row($table, $insert, $id);
redirect("admin/structure/" . $table);
}
}
$this->_view->load_page("edit/categories");
$this->_common();
$this->_view->setvar("msg_error", isset($msg_error) ? $msg_error : "");
$this->_view->setvar("item", $row);

#Categories tree generation
if (!empty($this->_dbtree->ERRORS_MES)) {
echo 'DB Tree Error!<pre>';
print_r($this->_dbtree->ERRORS_MES);
if (!empty($this->_dbtree->ERRORS)) {
print_r($this->_dbtree->ERRORS);
}
echo '</pre>';
exit;
} else {
while ($structure_tree = $this->_dbtree->NextRow()) {
$structure_tree['spacer'] = str_repeat('&nbsp;&nbsp;&nbsp;', 1 * $structure_tree[$table . '_level']);
$sections[] = $structure_tree;
$this->_view->clean("selected");
if ($structure_tree[$table . '_id'] == $parent_info[$table . '_id']) {$this->_view->parse("selected");}
if ($structure_tree[$table . '_id'] != $id) {
$this->_view->setvar("block", $structure_tree);
$this->_view->parse($table . "_structure");
}
}
}
$this->_view->setvar("pid", $parent_info[$table . '_id']);
$this->_view->render();
V
На сайте с 03.12.2007
Offline
94
#3

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

разные способы хороши для разных задач:

1. Списки смежности (Adjacency List)
+: выборка всех непосредственных детей для заданной вершины – очень быстро
-: выборка всех детей для заданной вершины (всего поддерева) – долго
-: выборка всех родителей для заданной вершины (определение пути) – долго
-: определить размер поддерева – долго
-: порядок элементов в уровне не может храниться в структуре дерева
-: проверка, является ли одна вершина дочерней по отношению к другой – долго, если вершины не являются непосредственными родственниками
+: добавление новых элементов в дерево – быстро
-: удаление ветки дерева – долго
+: перемещение веток дерева – быстро
+: количество узлов в дереве – не ограничено
?: устойчивость к ошибкам – средняя (если «исчезает» какой-то элемент, то исчезает только соответствущее поддерево)
+: понимание – легко

2. Вложенные множества (Nested Sets)
?: выборка всех непосредственных детей для заданной вершины – выполняется одним запросом, но долго (без использования индексов) в случае, если этой вершине соответствует поддерево значительных размеров.
+: выборка всех детей для заданной вершины (всего поддерева) – быстро
?: выборка всех родителей для заданной вершины (определение пути) – выполняется одним запросом, но часто без использования индексов (долго).
+: определить размер поддерева – очень быстро
+: структура дерева позволяет задавать порядок элементов в уровне
+: проверка, является ли одна вершина дочерней по отношению к другой – очень быстро
-: добавление новых элементов в дерево – долго
+: удаление ветки дерева – быстро
-: перемещение веток дерева – долго
+: количество узлов в дереве – не ограничено
-: устойчивость к ошибкам – низкая (если «исчезает» какой-то элемент, то все дерево становится неправильным)
?: понимание – разобраться в операциях выборки – просто, в операциях модификации – сложно

поэтому хранить структуру сайта, которую иногда редактирует админ в нестед сетс хорошо, а структуру форума где ветки создают пользователи - плохо.

пс. покажите метод MoveAll вместо редактирования статьи

Z6
На сайте с 12.10.2004
Offline
66
#4

function MoveAll($ID, $newParentId, $condition = '') {

$node_info = $this->GetNodeInfo($ID);
if (FALSE === $node_info) {
return FALSE;
}
list($leftId, $rightId, $level) = $node_info;
$node_info = $this->GetNodeInfo($newParentId);
if (FALSE === $node_info) {
return FALSE;
}
list($leftIdP, $rightIdP, $levelP) = $node_info;
if ($ID == $newParentId || $leftId == $leftIdP || ($leftIdP >= $leftId && $leftIdP <= $rightId) || ($level == $levelP+1 && $leftId > $leftIdP && $rightId < $rightIdP)) {
$this->ERRORS_MES[] = extension_loaded('gettext') ? _('cant_move_tree') : 'cant_move_tree';
return FALSE;
}
if (!empty($condition)) {
$condition = $this->_PrepareCondition($condition);
}
if ($leftIdP < $leftId && $rightIdP > $rightId && $levelP < $level - 1) {
$sql = 'UPDATE ' . $this->table . ' SET '
. $this->table_level . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_level.sprintf('%+d', -($level-1)+$levelP) . ' ELSE ' . $this->table_level . ' END, '
. $this->table_right . ' = CASE WHEN ' . $this->table_right . ' BETWEEN ' . ($rightId+1) . ' AND ' . ($rightIdP-1) . ' THEN ' . $this->table_right . '-' . ($rightId-$leftId+1) . ' '
. 'WHEN ' . $this->table_left . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_right . '+' . ((($rightIdP-$rightId-$level+$levelP)/2)*2+$level-$levelP-1) . ' ELSE ' . $this->table_right . ' END, '
. $this->table_left . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . ($rightId+1) . ' AND ' . ($rightIdP-1) . ' THEN ' . $this->table_left . '-' . ($rightId-$leftId+1) . ' '
. 'WHEN ' . $this->table_left . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_left . '+' . ((($rightIdP-$rightId-$level+$levelP)/2)*2+$level-$levelP-1) . ' ELSE ' . $this->table_left . ' END '
. 'WHERE ' . $this->table_left . ' BETWEEN ' . ($leftIdP+1) . ' AND ' . ($rightIdP-1);
} elseif ($leftIdP < $leftId) {
$sql = 'UPDATE ' . $this->table . ' SET '
. $this->table_level . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_level.sprintf('%+d', -($level-1)+$levelP) . ' ELSE ' . $this->table_level . ' END, '
. $this->table_left . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $rightIdP . ' AND ' . ($leftId-1) . ' THEN ' . $this->table_left . '+' . ($rightId-$leftId+1) . ' '
. 'WHEN ' . $this->table_left . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_left . '-' . ($leftId-$rightIdP) . ' ELSE ' . $this->table_left . ' END, '
. $this->table_right . ' = CASE WHEN ' . $this->table_right . ' BETWEEN ' . $rightIdP . ' AND ' . $leftId . ' THEN ' . $this->table_right . '+' . ($rightId-$leftId+1) . ' '
. 'WHEN ' . $this->table_right . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_right . '-' . ($leftId-$rightIdP) . ' ELSE ' . $this->table_right . ' END '
. 'WHERE (' . $this->table_left . ' BETWEEN ' . $leftIdP . ' AND ' . $rightId. ' '
. 'OR ' . $this->table_right . ' BETWEEN ' . $leftIdP . ' AND ' . $rightId . ')';
} else {
$sql = 'UPDATE ' . $this->table . ' SET '
. $this->table_level . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_level.sprintf('%+d', -($level-1)+$levelP) . ' ELSE ' . $this->table_level . ' END, '
. $this->table_left . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $rightId . ' AND ' . $rightIdP . ' THEN ' . $this->table_left . '-' . ($rightId-$leftId+1) . ' '
. 'WHEN ' . $this->table_left . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_left . '+' . ($rightIdP-1-$rightId) . ' ELSE ' . $this->table_left . ' END, '
. $this->table_right . ' = CASE WHEN ' . $this->table_right . ' BETWEEN ' . ($rightId+1) . ' AND ' . ($rightIdP-1) . ' THEN ' . $this->table_right . '-' . ($rightId-$leftId+1) . ' '
. 'WHEN ' . $this->table_right . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_right . '+' . ($rightIdP-1-$rightId) . ' ELSE ' . $this->table_right . ' END '
. 'WHERE (' . $this->table_left . ' BETWEEN ' . $leftId . ' AND ' . $rightIdP . ' '
. 'OR ' . $this->table_right . ' BETWEEN ' . $leftId . ' AND ' . $rightIdP . ')';
}
$sql .= $condition;
$this->db->StartTrans();
$res = $this->db->Execute($sql);
if (FALSE === $res) {
$this->ERRORS[] = array(2, 'SQL query error.', __FILE__ . '::' . __CLASS__ . '::' . __FUNCTION__ . '::' . __LINE__, 'SQL QUERY: ' . $sql, 'SQL ERROR: ' . $this->db->ErrorMsg());
$this->ERRORS_MES[] = extension_loaded('gettext') ? _('internal_error') : 'internal_error';
$this->db->FailTrans();
return FALSE;
}
$this->db->CompleteTrans();
return TRUE;
}
V
На сайте с 03.12.2007
Offline
94
#5

проверьте параметры, которые передаете в метод MoveAll и так же проверьте целостность вашего дерева:

http://webscript.ru/stories/04/09/01/8197045 (раздел УПРАВЛЕНИЕ ДЕРЕВОМ КАТАЛОГОВ)

Z6
На сайте с 12.10.2004
Offline
66
#6

Спасибо, покопаю в этом направлении. Потом по результату отпишусь в каком месте был балбесом :)

C
На сайте с 20.05.2011
Offline
14
#7

давно пришёл к личному заключению, что для NS/AL/MP в качестве библиотечного кода больше всего подходят хранимки с динамическим SQL-ем, сейчас только так и делаю. (производительность, не смотря на динамику, всё-равно больше, как мне показалось, и без этих лишних библиотек, а отдельное полноценное решение в своём слое доступа данных)

Z6
На сайте с 12.10.2004
Offline
66
#8

Вроде работать все правильно начало, балбесом был в этом месте именно, которое и показал вначале:

if ($msg_error == "")

{if($msg_error == "" and $parent_info[$table . "_id"] != post($table . '_parent_id'))
$this->_dbtree->MoveAll($id, post($table . '_parent_id'), '');

$this->_model->edit_structure_row($table, $insert, $id);
redirect("admin/structure/" . $table);
}

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

if($msg_error == "" and $parent_info[$table . "_id"] != post($table . '_parent_id')) 

{
//echo "Parent has changed! " . $parent_info[$table . "_id"] . "=>" . post($table . '_parent_id');
$this->_model->edit_structure_row($table, $insert, $id);
$this->_dbtree->MoveAll($id, post($table . '_parent_id'), '');
redirect("admin/structure/" . $table);
}
elseif($msg_error == "" and $parent_info[$table . "_id"] == post($table . '_parent_id'))
{
$this->_model->edit_structure_row($table, $insert, $id);
redirect("admin/structure/" . $table);
}
- вот так работает, еще почистить ифы осталось, но работает :)

Авторизуйтесь или зарегистрируйтесь, чтобы оставить комментарий