多层评论回复:显示和存储

时间:2010-10-30 08:48:41

标签: php mysql

所以我正在尝试创建一个评论系统,您可以在其中回复已经回复的评论(允许您创建理论上无限的回复帖子)。我希望它们按时间顺序显示(最新的),但当然回复应该直接在原始评论下面。如果有多条评论回复同一条评论,则回复也应按时间顺序排列(仍在原评论的下方)。我还想限制评论组的数量(一组评论只有一个评论,而不是一个回复),比方说,25。我应该如何设置MySQL表,以及我将使用哪种查询提取我想要的东西?

这是我的数据库的简化版本: ID int(11)NOT NULL AUTO_INCREMENT, DatePosted datetime NOT NULL, InReplyTo int(11)NOT NULL DEFAULT'0',

很抱歉,如果这有点令人困惑,我不确定如何区别对待。我已经在脑海里养了这个问题几个月了,每当我解决一个问题时,我最终都会遇到另一个......

5 个答案:

答案 0 :(得分:5)

有很多方法。这是我喜欢的一种方法(并定期使用)。

数据库

考虑以下数据库结构:

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。如果你移动评论(这肯定是一个奇怪的用例),你必须确保你手动更新从属于移动的评论的每个评论的每个parent_path。 ......但这些都是很容易跟上的事情。

如果你真的想要得到它(并且如果你的数据库支持它),你可以编写触发器来透明地管理parent_path - 我将为读者留下一个练习,但基本的想法是插入和更新触发器将在提交新插入之前触发。他们会走在树上(使用parent_id外键关系),并相应地重建parent_path的值。

甚至可以将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,埋在内部选择的中间。该语句将检索最新的25个“根级”注释。

[编辑:你可能会发现你需要玩一些东西来获得按照你喜欢的方式订购和/或限制事物的能力。这可能包括在parent_path中编码的层次结构中添加信息。例如:您可能决定将post_date包含在parent_path中,而不是/{id}/{id2}/{id3}/,而不是/{id}:{post_date}/{id2}:{post_date2}/{id3}:{post_date3}/。{{1}}。这样可以很容易地获得所需的订单和层次结构,代价是必须预先填充字段,并在数据更改时对其进行管理]

希望这会有所帮助。 祝你好运!

答案 1 :(得分:3)

你应该考虑在树中嵌套你的评论 - 我对数据树并不熟悉,但我可以相对容易地完成一些事情 - 我对任何优化代码的建议(和解释)持开放态度 - 但是想法会是这样的:

<?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);


?>

我希望你能得到一般的想法,我确信这里的其他一些用户可以提供一些有关我的建议的经验,并帮助优化它。

答案 2 :(得分:0)

在数据库中,您可以创建一个包含外键列(parent_comment)的表,该表引用了注释表本身。例如:

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

为了向单个项目显示注释,您必须选择特定项目的所有注释,并使用深度优先算法在脚本中递归地解析它们。在遍历算法中应该考虑时间顺序。

答案 3 :(得分:0)

我会考虑使用嵌套集来存储这种类型的分层数据。有关示例,请参阅http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/

答案 4 :(得分:0)

您可能会发现此方法很有用,它涉及单一调用非递归存储过程。

可在此处找到完整脚本: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);