Многоуровневые комментарии Ответы: отображение и хранение

Поэтому я пытаюсь создать систему комментариев, в которой вы можете ответить на комментарии, которые уже отвечают (что позволяет создавать теоретически бесконечные потоки ответов). Я хочу, чтобы они отображались в хронологическом порядке (самые новые сверху), но, конечно, ответы должны быть непосредственно под оригинальным комментарием. Если есть несколько комментариев, отвечающих на один и тот же комментарий, ответы также должны быть в хронологическом порядке (все еще под оригинальным комментарием). Я также хочу ограничить количество групп комментариев (набор комментариев с единственным комментарием, который вообще не является ответом), например, 25. Как мне настроить таблицу MySQL и какой запрос я буду использовать? извлечь то, что я хочу?

Вот упрощенная версия моего DB: ID int (11) NOT NULL AUTO_INCREMENT, DatePosted datetime NOT NULL, InReplyTo int (11) NOT NULL DEFAULT '0',

Извините, если это немного сбивает с толку, я не знаю, как это сказать по-другому. У меня была эта проблема в глубине души в течение пары месяцев, и каждый раз, когда я решаю одну проблему, я получаю другую …

Есть много способов. Вот один из подходов, который мне нравится (и используется на регулярной основе).

База данных

Рассмотрим следующую структуру базы данных:

 CREATE TABLE comments ( id int(11) unsigned NOT NULL auto_increment, parent_id int(11) unsigned default NULL, parent_path varchar(255) NOT NULL, comment_text varchar(255) NOT NULL, date_posted datetime NOT NULL, PRIMARY KEY (id) ); 

ваши данные будут выглядеть так:

 +-----+-------------------------------------+--------------------------+---------------+ | id | parent_id | parent_path | comment_text | date_posted | +-----+-------------------------------------+--------------------------+---------------+ | 1 | null | / | I'm first | 1288464193 | | 2 | 1 | /1/ | 1st Reply to I'm First | 1288464463 | | 3 | null | / | Well I'm next | 1288464331 | | 4 | null | / | Oh yeah, well I'm 3rd | 1288464361 | | 5 | 3 | /3/ | reply to I'm next | 1288464566 | | 6 | 2 | /1/2/ | this is a 2nd level reply| 1288464193 | ... and so on... 

Довольно легко выбрать все по доступной цене:

 select id, parent_path, parent_id, comment_text, date_posted from comments order by parent_path, date_posted; 

заказывая по parent_path, date_posted обычно приводит к результатам в том порядке, в котором они вам понадобятся при создании вашей страницы; но вы хотите быть уверены, что у вас есть индекс в таблице комментариев, который будет правильно поддерживать это – в противном случае запрос работает, но он действительно, действительно неэффективен:

 create index comments_hier_idx on comments (parent_path, date_posted); 

Для любого данного отдельного комментария легко получить полное дерево комментариев этого комментария. Просто добавьте предложение where:

 select id, parent_path, parent_id, comment_text, date_posted from comments where parent_path like '/1/%' order by parent_path, date_posted; 

добавленное предложение where будет использовать тот же самый индекс, который мы уже определили, поэтому нам хорошо идти.

Обратите внимание, что мы еще не использовали parent_id . На самом деле это не обязательно. Но я включаю это, потому что он позволяет нам определять традиционный внешний ключ для обеспечения ссылочной целостности и реализации каскадных удалений и обновлений, если мы этого хотим. Ограничения внешнего ключа и каскадные правила доступны только в таблицах INNODB:

 ALTER TABLE comments ENGINE=InnoDB; ALTER TABLE comments ADD FOREIGN KEY ( parent_id ) REFERENCES comments ON DELETE CASCADE ON UPDATE CASCADE; 

Управление иерархией

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

Если вы действительно хотите получить фантазию (и если ваш db ее поддерживает), вы можете писать триггеры для прозрачного управления parent_path – я оставлю это упражнение для читателя, но основная идея заключается в том, что триггеры вставки и обновления будут срабатывать перед установкой новой вставки. они будут подниматься по дереву (используя parent_id внешнего ключа parent_id ) и соответственно перестраивать значение parent_path .

Также возможно разбить parent_path на отдельную таблицу, которая полностью управляется триггерами в таблице комментариев, с несколькими видами или хранимыми процедурами для реализации различных запросов, которые вам нужны. Таким образом, полностью изолировать ваш код среднего уровня от необходимости знать или заботиться о механизме хранения информации о иерархии.

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


Ограничения

MySQL (и некоторые другие базы данных) позволяет вам выбирать «страницы» данных с помощью предложения LIMIT :

 SELECT * FROM mytable LIMIT 25 OFFSET 0; 

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

 -- the following will NOT work as intended select id, parent_path, parent_id, comment_text, date_posted from comments order by parent_path, date_posted LIMIT 25 OFFSET 0; 

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

Что-то вроде этого:

 select a.* from comments a join (select id, parent_path from comments where parent_id is null order by parent_path, post_date DESC limit 25 offset 0) roots on a.parent_path like concat(roots.parent_path,roots.id,'/%') or a.id=roots.id) order by a.parent_path , post_date DESC; 

Обратите внимание на limit 25 offset 0 инструкции limit 25 offset 0 , заложенный в середине внутреннего выбора. Этот оператор будет получать последние 25 «корневых» комментариев.

[edit: вы можете обнаружить, что вам нужно немного поиграть с вещами, чтобы получить возможность заказать и / или ограничить все так, как вам нравится. это может включать добавление информации в иерархию, закодированную в parent_path . например: вместо /{id}/{id2}/{id3}/ , вы можете включить post_date как часть родительского пути: /{id}:{post_date}/{id2}:{post_date2}/{id3}:{post_date3}/ . Это упростит получение заказа и иерархии, за счет необходимости заполнения поля вверх и управления им при изменении данных]

надеюсь это поможет. удачи!

Вы должны рассмотреть возможность вложения ваших комментариев в дерево – я не так хорошо знаком с деревьями данных, но я могу сделать что-то относительно просто – я открыт для любых предложений (и объяснений) для оптимизации кода, но идея была бы что-то вроде этого:

 <?php $mysqli = new mysqli('localhost', 'root', '', 'test'); /** The class which holds the comments */ class Comment { public $id, $parent, $content; public $childs = array(); public function __construct($id, $parent, $content) { $this->id = $id; $this->parent = $parent; $this->content = $content; } public function addChild( Comment $obj ) { $this->childs[] = $obj; } } /** Function to locate an object from it's id to help nest the comments in a hieraci */ function locateObject( $id, $comments ) { foreach($comments as $commentObject) { if($commentObject->id == $id) return $commentObject; if( count($commentObject->childs) > 0 ) return locateObject($id, $commentObject->childs); } } /** Function to recursively show comments and their nested child comments */ function showComments( $commentsArray ) { foreach($commentsArray as $commentObj) { echo $commentObj->id; echo $commentObj->content; if( count($commentObj->childs) > 0 ) showComments($commentObj->childs); } } /** SQL to select the comments and order dem by their parents and date */ $sql = "SELECT * FROM comment ORDER BY parent, date ASC"; $result = $mysqli->query($sql); $comments = array(); /** A pretty self-explainatory loop (I hope) */ while( $row = $result->fetch_assoc() ) { $commentObj = new Comment($row["id"], $row["parent"], $row["content"]); if($row["parent"] == 0) { $comments[] = $commentObj; continue; } $tObj = locateObject($row["parent"], $comments); if( $tObj ) $tObj->addChild( $commentObj ); else $comments[] = $commentObj; } /** And then showing the comments*/ showComments($comments); ?> 

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

В базе данных вы можете создать таблицу с столбцом внешнего ключа (parent_comment), которая ссылается на таблицу комментариев. Например:

 CREATE TABLE comments ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, parent_comment INT FOREIGN KEY REFERENCES comments(id), date_posted DATETIME, ...) 

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

Я бы рассмотрел вложенный набор для хранения этого типа иерархических данных. См. http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/ для примера.

Вы можете найти этот метод полезным, который включает в себя один вызов нерекурсивной хранимой процедуры.

Полный скрипт можно найти здесь: http://pastie.org/1259785

Надеюсь, поможет 🙂

Пример вызова хранимой процедуры:

 call comments_hier(1); 

Пример скрипта php:

 <?php $conn = new mysqli("localhost", "foo_dbo", "pass", "foo_db", 3306); $result = $conn->query(sprintf("call comments_hier(%d)", 3)); while($row = $result->fetch_assoc()){ ... } $result->close(); $conn->close(); ?> 

SQL-скрипт:

 drop table if exists comments; create table comments ( comment_id int unsigned not null auto_increment primary key, subject varchar(255) not null, parent_comment_id int unsigned null, key (parent_comment_id) )engine = innodb; insert into comments (subject, parent_comment_id) values ('Comment 1',null), ('Comment 1-1',1), ('Comment 1-2',1), ('Comment 1-2-1',3), ('Comment 1-2-2',3), ('Comment 1-2-2-1',5), ('Comment 1-2-2-2',5), ('Comment 1-2-2-2-1',7); delimiter ; drop procedure if exists comments_hier; delimiter # create procedure comments_hier ( in p_comment_id int unsigned ) begin declare v_done tinyint unsigned default 0; declare v_depth smallint unsigned default 0; create temporary table hier( parent_comment_id smallint unsigned, comment_id smallint unsigned, depth smallint unsigned default 0 )engine = memory; insert into hier select parent_comment_id, comment_id, v_depth from comments where comment_id = p_comment_id; /* http://dev.mysql.com/doc/refman/5.0/en/temporary-table-problems.html */ create temporary table tmp engine=memory select * from hier; while not v_done do if exists( select 1 from comments c inner join hier on c.parent_comment_id = hier.comment_id and hier.depth = v_depth) then insert into hier select c.parent_comment_id, c.comment_id, v_depth + 1 from comments c inner join tmp on c.parent_comment_id = tmp.comment_id and tmp.depth = v_depth; set v_depth = v_depth + 1; truncate table tmp; insert into tmp select * from hier where depth = v_depth; else set v_done = 1; end if; end while; select c.comment_id, c.subject, p.comment_id as parent_comment_id, p.subject as parent_subject, hier.depth from hier inner join comments c on hier.comment_id = c.comment_id left outer join comments p on hier.parent_comment_id = p.comment_id order by hier.depth, hier.comment_id; drop temporary table if exists hier; drop temporary table if exists tmp; end # delimiter ; call comments_hier(1); call comments_hier(5);