如何实现分级评论?

37
我正在开发一个能够支持分级评论的网络应用程序。我需要根据收到的投票数重新排列评论的能力。(类似于reddit中的分级评论工作方式)
我希望能听到 SO 社区在如何实现这一点方面的建议。
我应该如何设计 "comments" 表格?以下是我现在使用的结构:
Comment
    id
    parent_post
    parent_comment
    author
    points

这个结构需要做哪些改变?

我应该如何从这个表中获取细节以正确地显示它们? (欢迎使用任何编程语言进行实现。我只想知道如何以最佳方式完成它)

在实现此功能时需要注意哪些内容,以便减少CPU/数据库的负载?

提前感谢。

4个回答

17

在数据库中存储树形结构有很多不同的解决方案。这取决于您是否想要检索子层次结构(例如 X 项的所有子代)或者只是想获取整个层次结构集并使用字典在内存中以 O(n) 的方式构建树。

您的表具有在一个请求中获取帖子下所有评论的优点,通过过滤 parentpost 可以实现。但是,由于您以教科书/朴素的方式定义了评论的父级,因此您必须在内存中构建树(见下文)。如果想从数据库中获取树形结构,则需要不同的树形结构存储方式:

参见我在这里描述的基于预计算的方法: http://www.llblgen.com/tinyforum/GotoMessage.aspx?MessageID=17746&ThreadID=3208 或者采用 CELKO 在此处描述的平衡树方式: using balanced trees described by CELKO here:

还有另一种方法: http://www.sqlteam.com/article/more-trees-hierarchies-in-sql

如果将整个层次结构在内存中获取并在那里构建树,则可以更有效率,因为查询非常简单:select .. from Comment where ParentPost = @id ORDER BY ParentComment ASC

在该查询之后,您可以使用一个仅保留元组 CommentID - Comment 的字典,在内存中构建树。现在,只需遍历结果集并即时构建树:每当处理到一条评论,您可以在字典中查找其父评论,然后还可以将当前正在处理的评论存储在该字典中。


“在内存中”是指在应用程序级别吗? - Ced
注意:llblgen的链接现在为https://www.llblgen.com/tinyforum/Thread/3208#17746 - awreccan

7

还有几件事需要考虑...

1)当你说“像Reddit一样排序”是基于排名还是日期,你指的是顶级还是整个内容?

2)当你删除一个节点时,分支会发生什么?你会重新设置它们的父级吗?在我的实现中,我认为编辑们会决定——要么隐藏该节点并将其显示为“评论已隐藏”,同时显示可见的子级,要么隐藏评论及其子级,或者彻底删除整棵树。重新设置父级应该很容易(只需将子级的父级设置为被删除的父级),但任何涉及整棵树的操作似乎都难以在数据库中实现。

我一直在研究PostgreSQL的ltree模块。它应该可以让涉及树的数据库操作更快一些。它基本上允许您在表中设置一个字段,看起来像:

ltreetest=# select path from test where path <@ 'Top.Science';
                path                
------------------------------------
 Top.Science
 Top.Science.Astronomy
 Top.Science.Astronomy.Astrophysics
 Top.Science.Astronomy.Cosmology

然而,它本身并不能确保任何引用完整性。换句话说,您可以拥有“Top.Science.Astronomy”的记录,而没有“Top.Science”或“Top”的记录。但是它能让您做的事情是:

-- hide the children of Top.Science
UPDATE test SET hide_me=true WHERE path @> 'Top.Science';

或者

-- nuke the cosmology branch
DELETE FROM test WHERE path @> 'Top.Science.Cosmology';

如果与传统的“comment_id”/“parent_id”方法结合使用存储过程,我认为你可以兼顾两者的优点。你可以使用“path”快速遍历数据库中的评论树,并通过“comment_id”/“parent_id”确保引用完整性。我想象中的实现方式是:
CREATE TABLE comments (
comment_id SERIAL PRIMARY KEY,
parent_comment_id int REFERENCES comments(comment_id) ON UPDATE CASCADE ON DELETE CASCADE,
thread_id int NOT NULL  REFERENCES threads(thread_id) ON UPDATE CASCADE ON DELETE CASCADE,
path ltree NOT NULL,
comment_body text NOT NULL,
hide boolean not null default false
);

评论路径字符串看起来像这样:
<thread_id>.<parent_id_#1>.<parent_id_#2>.<parent_id_#3>.<my_comment_id>

因此,帖子“102”的根评论,其评论ID为“1”,其路径如下:

102.1

而其评论ID为“3”的子级,则为:

102.1.3

一些ID为“31”和“54”的“3”的子级则为:

102.1.3.31
102.1.3.54

为了隐藏节点“3”及其子节点,您需要执行以下操作:
UPDATE comments SET hide=true WHERE path @> '102.1.3';

但我不确定--这可能会增加不必要的开销。另外,我不知道ltree维护得如何。


1
我认为删除节点不是一个好主意。如果“评论”被删除,内容应该被丢弃或设置一个标志。这样渲染器就知道如何响应。 - tlt

5

对于小型层次结构(少于一千个项),您当前的设计基本上是可以的。

如果您想在特定级别或深度上获取,请向您的结构添加一个“级别”项,并在保存时计算它。

如果性能是一个问题,请使用一个良好的缓存。


我不明白“level”是什么意思? - Yasar Arafath
2
@YasarArafath 他的意思是每个评论都应该知道它在树状结构中的深度。这可以让你只查询前三级评论,例如。只有当用户展开评论树时,其余的评论才会被加载。 - tlt
你会如何“分页”或遍历这个问题?假设你的顶层有15k条评论?简单的order by limit方法行不通。 - dessalines

4
我会添加以下新字段到上面的表格中:
  • thread_id:特定对象附加的所有评论的标识符

  • date:评论日期(允许按顺序获取评论)

  • rank:评论排名(允许按排名获取评论)

使用这些字段,您将能够:
  1. 在单个操作中获取线程中的所有评论
  2. 按日期或排名对线程中的评论进行排序
不幸的是,如果您想保持查询DB接近SQL标准,则必须在内存中重新创建树。一些DB提供了用于分层数据的特殊查询(例如Oracle)。
./alex

Alex,感谢你的回答,但我没有理解你的一些观点。我认为thread_id与post_id相同,日期可以用自增id替换,rank = points。这就是我在我的设计中所做的。你能否澄清一下我的设计和建议设计之间的差异? - Niyaz
@Niyaz:我猜你可能需要编辑你的问题,因为我没有看到post_id(实际上我误解了points)。thread_id:所有评论的唯一ID在一个线程中(附加到一篇内容)。自动递增可以提供顺序,但不等同于日期(检查几乎所有论坛)。 - alexpopescu
我也感到困惑。如果“parent_comment”指向父评论的ID,则我会将其命名为“parent_comment_id”以消除歧义。我不确定“parent_post”的意思,以及它与“parent_comment”有何不同。 - Cory R. King
Parent_post指的是原始帖子(而不是任何评论)。我以为这样可以通过单个查询获取与帖子相关的所有评论,而无需递归遍历父-子评论结构。 - Niyaz
@Niyaz:就我所知,parent_post等同于thread_id。因此,似乎只剩下需要添加的是:日期、用户详细信息(如果需要)。 - alexpopescu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接