【问题标题】:Multi-tiered Comment Replies: Display and Storage多层评论回复:显示和存储
【发布时间】:2010-10-31 05:22:17
【问题描述】:

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

这是我的数据库的简化版本: ID int(11) NOT NULL AUTO_INCREMENT, DatePosted 日期时间不为空, InReplyTo int(11) NOT NULL DEFAULT '0',

对不起,如果这有点令人困惑,我不知道如何用不同的方式来表达它。几个月来我一直在想这个问题,每次我解决一个问题时,我都会遇到另一个问题......

【问题讨论】:

    标签: php mysql


    【解决方案1】:

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

    数据库

    考虑以下数据库结构:

    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 排序通常会按照您在生成页面时需要的顺序生成结果;但是您需要确保在 cmets 表上有一个可以正确支持这一点的索引 - 否则查询可以工作,但它确实非常非常低效:

    create index comments_hier_idx on comments (parent_path, date_posted);
    

    对于任何给定的单个评论,很容易获得该评论的整个 child-cmets 树。只需添加一个 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。如果您移动 cmets(这无疑是一个奇怪的用例),您必须确保手动更新从属于移动评论的每个评论的每个 parent_path。 ...但这些都是相当容易跟上的事情。

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

    甚至可以将parent_path 拆分为一个单独的表,该表完全由 cmets 表上的触发器管理,并带有一些视图或存储过程来实现您需要的各种查询。从而将您的中间层代码与了解或关心存储层次结构信息的机制完全隔离开来。

    当然,任何花哨的东西都不是必需的——通常只需将 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 个“根级”cmets。

    [编辑:你可能会发现你必须玩一些东西才能完全按照你喜欢的方式订购和/或限制东西。这可能包括在以parent_path 编码的层次结构中添加信息。例如:代替/{id}/{id2}/{id3}/,您可能决定将post_date 作为父路径的一部分:/{id}:{post_date}/{id2}:{post_date2}/{id3}:{post_date3}/。这将使获得所需的顺序和层次结构变得非常容易,但代价是必须预先填充字段,并在数据更改时对其进行管理]

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

    【讨论】:

    • 这个我没试过,但我必须承认“路径”是个好主意!
    • 您能否指出有关创建自动构建路径字段的触发器的任何线索?
    • 查看this section of the mysql manual 的语法。至于触发器将实现的实际算法,这是一个完全不同的问题。从语法和示例开始,看看你想出了什么——如果你不能把它整理出来,回来发布一个新的 SO 问题,很多人(包括我)会很乐意提供帮助. :-) 干杯!
    • 嗨,“按 parent_path 排序,date_posted 通常会按照您需要的顺序产生结果”这句话让我感到困惑。我正在测试这个,但我得到的结果的顺序是:评论 - 2 评论 - 1 评论回答 1 (/1/),我们称之为 X 评论回答 2 (/2/) 评论回答评论 X ( /1/X) 我想要这样:评论 2 对评论 2 的回答 (/2/) 评论 1 对评论 1 的回答 (/1/) 评论 X ( /1/X/ )
    • 我不完全按照你的意思说......但如果它的出现顺序与你想要的相反,只需在order by 中添加ASC 或DESC子句强制订单按照你的意愿。例如:ORDER BY parent_path ASC, date_posted DESC,或ORDER BY parent_path DESC, date_posted DESC,...或您需要的水。
    【解决方案2】:

    您应该考虑将 cmets 嵌套在树中 - 我对数据树不太熟悉,但我可以完成一些相对容易的事情 - 我愿意接受任何优化代码的建议(和解释) - 但是想法是这样的:

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

    我希望您能大致了解,并且我相信这里的其他一些用户可以就我的建议提供一些经验丰富的想法并帮助优化它。

    【讨论】:

    • 嗯,这当然应该有效,而且它可能比我能做的任何事情都更优化。谢谢!
    • @NewCod3r,我不明白这个问题?这和HTML无关?
    【解决方案3】:

    在数据库中,您可以创建一个具有外键列(parent_comment)的表,该表引用 cmets 表本身。例如:

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

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

    【讨论】:

      【解决方案4】:

      我会考虑一个嵌套集,用于存储这种类型的分层数据。示例见http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/。

      【讨论】:

        【解决方案5】:

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

        完整的脚本可以在这里找到: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);
        

        【讨论】:

          猜你喜欢
          • 2018-12-15
          • 1970-01-01
          • 2013-06-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-08-07
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多