Организация многоуровневой структуры сайта (tree, mod_rewrite)

Silex

unitecsys
Организация многоуровневой структуры сайта (tree, mod_rewrite)

Возникла необходимость организовать многоуровневую структуру сайта с неограниченным числом уровней вложенности. С хранением проблем не возникло - phpDBTree (http://dev.e-taller.net/dbtree/).

Основной вопрос - как организовать формирование и обработку ссылок? Пока что пришел к тому, чтобы в записи в отдельном поле хранить алиасы - имена виртуальных папок. Алиасы уникальны только в пределах детей одного родителя. При помощи mod_rewrite управление передается на скрипт, который explode'ом по '/' загоняет переданный URL вида www.example.org/level1/level2/levelN/ в массив (или каким-либо другим образом), и затем уже с конца массива начинает делать выборку по алиасу из базы. Поскольку алиасы не уникальны, могут быть коллизии. В этом случае проверяется родитель, и так пока не будет получена однозначность. Ну, дальше уже с записью что угодно можно делать, зная ее уникальный id.

Естественно, предложенный алгоритм, мягко говоря, нехороший, и сводит на нет все преимущество хранения деревьев по алгоритму Nested Sets. Запросы, запросы...

Основная загвоздка - в неуникальности алиасов. Как вариант рассматривалось динамическое формирование Rewrite Rules со связкой "полный УРЛ" <=> "передача однозначного id в скрипт" при добавлении нового раздела, но это ж тоже ужас.

Есть соображения по этому поводу?
 

Макс

Старожил PHPClub
сделать поле
long_alias varchar(128) not null,
unique key long_alias (long_alias)
и хранить в нем строку /level1/level2/level3/
 

Макс

Старожил PHPClub
или можно
hash_alias char(32) not null,
unique key hash_alias (hash_alias)
и хранить в нем строку md5("/level1/level2/level3/")
 

Silex

unitecsys
Насчет хранения хэша - классная идея! В принципе, решает все проблемы, нужно только следить за целостностью в случае изменения алиаса у какого-либо родителя и перегенерировать hash_alias у детей. Попутно можно не генерить hash_alias для разделов-контейнеров, которые не содержат информации, а служат лишь логическими уровнями в структуре (например, раздел "О компании" содержит разделы "История", "Люди" и т.п., но непосредственно текста в нем нет). Соответственно not null убрать.

Спасибо, вывел из тупика ,)

Хотелось бы услышать еще, как у других разработчиков реализована подобная структура.
 

Yurik

/dev/null
и хранить в нем строку /level1/level2/level3/
md5("/level1/level2/level3/")
не нравится мне такое, зачем в дочернем елементе хранить атрибуты родителя? А если я переименую родительский елемент, его дети станут сиротами?
Основная загвоздка - в неуникальности алиасов. Как вариант рассматривалось динамическое формирование Rewrite Rules со связкой "полный УРЛ" <=> "передача однозначного id в скрипт" при добавлении нового раздела, но это ж тоже ужас.
Не вижу никаких проблем в том чтобы сделать explode REDIRECT_URI и пройтись 2-4 запросами по базе от
1 WHERE url='level1' AND pid=0
2 WHERE url='level2' AND pid=@id
..
N WHERE url='levelN' AND pid=@id

N (как правило не более 5) простейших запросов не создают никаких проблем. Если на каком-то этапе ничего не возвращается - значит header('404 Not Found').
Уникальность детей родителя тоже очень просто решается созданием UNIQUE KEY по pid+url

P.S. Недавно тестировали на канале скорость
"ErrorDocument 404" vs "mod_rewrite". Сам механизм оказался в 5-6 медленнее, но с учетом времени выполнения скрипта разница сводится на нет. Результат можно глянуть здесь
 

Yurik

/dev/null
"полный УРЛ" <=> "передача однозначного id в скрипт"
вот упрощенный кусок моего кода который переводит УРЛ в ИД (я выкинул вытягивание языка, т.к. у меня ещё идет
/ru/about/stuff/pupkin/
)
PHP:
 function cparseurl(){
	$this->curpath=explode("/", $_SERVER['REDIRECT_URL']);
	unset($this->curpath[0]);
	if (end($this->curpath)=='') array_pop($this->curpath);
	 $refer=0;
	 foreach ($this->curpath as $current) {
		$sql="SELECT * FROM tblpagegroups WHERE ref=".$refer." AND url='".$current."'";
		$result=@mysql_query($sql) or $this->errhandler(0, mysql_error(), __LINE__, __FILE__);
		$row=mysql_fetch_array($result);
		if (!$row) $this->errhandler(404);
		$refer=(int) $row['id'];
	 }
	 $this->curpageid=$refer;
 }
после этого $obj->curpageid будет содержать ИД
[sql]
CREATE TABLE tblpagegroups (
id mediumint(8) unsigned NOT NULL auto_increment,
ref mediumint(8) unsigned NOT NULL,
url varchar(20) NOT NULL,
activate tinyint(1) NOT NULL default '1',
PRIMARY KEY (id),
UNIQUE KEY ref (ref,url,activate),
);[/sql]
 

su1d

Старожил PHPClubа
хэш -- это хорошо, но строка в 32 байта.
по-моему удобнее хранить crc32 в виде числа -- быстрее будет проходить проверка.
 

Шмуэль

Guest
не нравится мне такое, зачем в дочернем елементе хранить атрибуты родителя? А если я переименую родительский елемент, его дети станут сиротами?
Можно хранить в качестве поинтера на родителя его уникальный адрес. Тогда не возникнет проблем с переименованием родителей.
 

Silex

unitecsys
2 Yurik
Дело в том, что необходимость в смене алиаса документа возникает достаточно редко, точнее, гораздо реже, чем обращение пользователей к страницам сайта. Если понадобится поменять алиас записи, то одним запросом получаются все ее дети и затем апдейтятся поля hash_alias. Трудоемкость всей операции N+1 запросов, где N - кол-во детей. Заметь, это ТОЛЬКО в случае смены алиаса, выполняемой админом. Пользователи же всегда будут получать странички в один запрос. Хотя, при малом количестве уровней вложенности, проигрыш в скорости не должен играть никакой роли...

2 su1d
crc32 - это тоже хэш-функция?
 

Yurik

/dev/null
>crc32 - это тоже хэш-функция?
по определению - да
(но она слабо отвечает условию "H(x) is collision-free" поэтому толку от нее мало)
Пользователи же всегда будут получать странички в один запрос. Хотя, при малом количестве уровней вложенности, проигрыш в скорости не должен играть никакой роли...
полностью согласен, только лично мне проще сделать парсинг пути на странице чем включать всевозможные проверки при администрировании разделов.
 

Silex

unitecsys
Можно хранить в качестве поинтера на родителя его уникальный адрес. Тогда не возникнет проблем с переименованием родителей.
Не понял. Предлагаешь первый вариант Maxim Matyukhin? Тогда как это защитит от переименования родителей?

Вообще же в "чистом" алгоритме Nested Sets в записях не хранятся прямые указатели на родителей.

2 Yurik
Всевозможные проверки - это проверка, не поменяли ли мы алиас при редактировании раздела =)
 

Yurik

/dev/null
Всевозможные проверки - это проверка, не поменяли ли мы алиас
я имел ввиду что это может быть в разных частях администрирования, плюс никто не защитит от редактирования "в лоб" через майадмин.

ЗЫ. Шмуэль наверно имел ввиду мой вариант хранения pid...
 

Шмуэль

Guest
Yurik, Таки да - имел ввиду именно это.
Если хранить pid тогда имя родителя не имеет значение, потому что в плане детей - родитель это pid а не имя.
 

Yamamoto

Guest
сабж.

Пример. Как делаю я.
Через mod_rewrite ко мне приходит в $_GET['choosed']вызываемый uri -

PHP:
$_GET['choosed'] = 'catalogue/cd/beatles/';
Потом:

PHP:
$choosed = isset($_GET['choosed']) ? $_GET['choosed'] : 'index';
Все понятно, есть uri, то есть, нет то index.

Чтобы проверить есть ли у меня такой виртуальный uri или нет,
делаю так:

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

PHP:
...
while ($data = mysql_fetch_array($query)) {

...

$nodes[$data['id']]['uri'] =
(!empty($data['parent_id'] ? $nodes[$data['parent_id']]['uri'] : '') .
$data['uri'] .
'/';

...

}
...
Таким образом каждый узел при загрузке хранит свой полный адрес от корня, потом создаю хитрый массив:

PHP:
...
while (list($id, $data) = each($nodes)) {

...

$www[$data['uri']] = $id;

...

}
...
Получается такой вот массив:

PHP:
$www = Array(
	...
	[news/subscribe/] = 134;
	[cataloque/cd/beatles/] = 117;
	[cataloque/cd/moloko/] = 263;
	...
);
Ну а теперь осталось проверить isset($www[$choosed]),
и если да, то $www[$choosed] и будет id выбранного узла...

И все:)

Просто и логично, наверное:)
 

new_darkbear

Guest
Получается такой вот массив:


$www = Array(
...
[news/subscribe/] = 134;
[cataloque/cd/beatles/] = 117;
[cataloque/cd/moloko/] = 263;
...
);
Мне кажется что такой вариант приемлем лишь только для небольшой структуры сайта, для большой структуры, перекачивание в массив всей структуры, это лишняя трата времени и памяти.

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

По крайней мере я у себя так делаю :))
 

Yurik

/dev/null
Получается такой вот массив:
т.е. ты грузишь всю карту сайта в массив, независимо от того нужно это будет или нет. Да, наверно доступ к массиву быстрее чем потом несколько запросов к базе (каждый раз!), но не уверен насчет больших деревьев.

PHP:
while ($data = mysql_fetch_array($query)) { 
$nodes[$data['id']]['uri'] = 
(!empty($data['parent_id'] ? $nodes[$data['parent_id']]['uri'] : '') . 
$data['uri'] . 
'/'; 
}
только здесь обязательно нужно в запросе написать
.. ORDER BY parent_id, ...
разделов-контейнеров, которые не содержат информации, а служат лишь логическими уровнями в структуре
тогда вопрос по интерфейсу, а что выводить на странице при вызове такого контейнера?
 

Yamamoto

Guest
Автор оригинала: new_darkbear
Мне кажется что такой вариант приемлем лишь только для небольшой структуры сайта, для большой структуры, перекачивание в массив всей структуры, это лишняя трата времени и памяти.

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

По крайней мере я у себя так делаю :))
Но тогда при смене uri у предка надо менять uri у всех его детей, а это малость не красиво, алгоритм и логика кривая:) Я считаю что хранить надо только то, что реально нужно.

А на скорость времени и загрузку памяти это особо не повлияет; когда надо строить, к примеру, меню с выпадающими подменю нескольких уровней (собственно вся структура сайта), то грузятся все узлы и не меньше:)

Я проверял как то, что 1 узел, что 1000, разницы на глаз не заметил:) Это же ведь простые операции с текстом, и не больше, это должно летать:) Кстати сейчас протестю, напишу!

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

Вот проверил!
Внес 3000 узлов - 1000 узлов 1 уровня, 1000 узлов 2 уровня, 1000 узлов 3 уровня.
Время обработки составило 0.3 сек, при том что у меня комп так себе.
 

Yamamoto

Guest
Автор оригинала: Yurik
т.е. ты грузишь всю карту сайта в массив, независимо от того нужно это будет или нет. Да, наверно доступ к массиву быстрее чем потом несколько запросов к базе (каждый раз!), но не уверен насчет больших деревьев.


только здесь обязательно нужно в запросе написать
.. ORDER BY parent_id, ...

тогда вопрос по интерфейсу, а что выводить на странице при вызове такого контейнера?
С большим деревом проверил на 3000 узлах, прочти я обновил свою 2 запись. Про ORDER BY все верно, так и есть. Про контейнеры в моем посте ничего не написано, это кто-то раньше писал, так что это не ко мне:)

Проверил еще на 15000 узлах (3 уровня вложенности), время выполнения 1.2 - 1.5 сек (комп так себе). Не быстро, но зачем нам 15000 узлов:)
 

Silex

unitecsys
2 Yamamoto

алгоритм и логика кривая
Ну, Кнут бы тебя тоже по головке не погладил :) Ты часто меняешь алиасы своих разделов? Я уже приводил пример, сколько запросв потребуется для этого, при условии, что хранишь дерево по алгоритму Nested Sets, а не по принципу "ребенок хранит айди родителя". А ты шерстишь ВСЮ базу. Потом еще и ассоциативный массив с ключами любой длины в общем случае... Опять-таки, учитывая скорость РНР и базы, это может и не сказывается, но все-таки...

2 Yurik

тогда вопрос по интерфейсу, а что выводить на странице при вызове такого контейнера?
Свою 404-ю страничку - ссылок-то на эту страницу-контейнер нигде нет.

Типичный пример. Есть меню "About US" с выпадающими подменю "History", "People" и т.п. Ссылки на "About US" нет и страницы такой тоже, но наличие такого контейнера необходимо при выводе навигации типа home >> about >> history, при этом пустой 'hash_alias' как раз и будет указывать на то, что ссылку на этот раздел в навигации делать не нужно.

Как вариант - выводить по запросу страницы-контейнера что-то типа "В данном разделе вы можете ознакомиться с такими документами и подразделами" и вывести список детей этого уровня. Все ж таки приятнее 404-й... Повторюсь, что на эту страницу никто нигде не ссылается.
 
Сверху