问题来了
这些天给 uCourse 加动态功能。系统本身设计贴近知乎,目前分为评课系统 + 论坛发帖。动态功能仔细想想竟然牵扯四层嵌套——
- 一级结构:
- A 发布了 一个帖子
- 二级结构:
- B 点赞了 A 的帖子
- B 评论了 A 的帖子
- B 测评了 某个课程
- 三级结构:
- C 点赞了 B 在 A 的帖子下的评论
- C 回复了 B 在 A 的帖子下的评论
- C 赞同了 B 在某个课程下的评测
- C 评论了 B 在某个课程下的评测
- 四级结构:
- D 点赞了 在某个课程中 B 的评测下的 C 的评论
- D 回复了 在某个课程中 B 的评测下的 C 的评论
嵌套关系分析:
- 帖子 => 评论 => 回复
- 课程 => 评测 => 评论 => 回复
初次尝试
查了一些资料后,由于不同的原因,决定舍弃掉一些场景,并大量使用冗余字段,在一张 activity
表中实现。
该表结构如下:
1 | id => 动态 ID |
具体场景的实现如下:
符号规定:
- 用户 A, B, C
- 帖子 P (Post)
- 测评 E (Evaluation)
- 课程 L (Lesson)
- 评论 M (coMment)
一级结构(1/1 实现):
A 发布了 帖子。即 A 发布 P。【查询 2 次】
1
2
3
4user_id: A 【查询用户 A】
action: POST
object_id: post_id 【查询帖子 P】
object_type: POST
二级结构(3/3 实现):
B 点赞了 A 的帖子。即 A 发布 P,B 对 P 点赞。【查询 2 次】
1
2
3
4user_id: B 【查询用户 B】
action: LIKE
object_id: post_id 【查询帖子 P】
object_type: POSTB 评论了 A 的帖子。即 A 发布 P,B 对 P 发布 M。【查询 3 次】
1
2
3
4
5
6user_id: B 【查询用户 B】
action: COMMENT
object_id: comment_id 【查询评论 M】
object_type: COMMENT
parent_object_id: post_id 【查询帖子 P】
parent_object_type: POSTB 测评了 某个课程。即有一个 L,B 对 L 发布 E。 【查询 3 次】
1
2
3
4
5
6user_id: B 【查询用户 B】
action: EVAL
object_id: eval_id 【查询测评 E】
object_type: EVAL
parent_object_id: course_id 【查询课程 L】
parent_object_type: COURSE
三级结构(2/4 实现):
C 赞同了 B 在某个课程下的评测。即有一个 L,B 对 L 发布测评 E,C 对 E 赞同。【查询 3 次】
1
2
3
4
5
6user_id: C 【查询用户 C】
action: VOTE
object_id: eval_id 【查询测评 E】
object_type: EVAL
parent_object_id: course_id 【查询课程 L】
parent_object_type: COURSEC 点赞了 B 在 A 的帖子下的评论。即 A 发布 P,B 对 P 发布 M,C 对 M 点赞。【查询 3 次】
1
2
3
4
5
6user_id: C 【查询用户 C】
action: LIKE
object_id: comment_id 【查询评论 M】
object_type: COMMENT
parent_object_id: post_id 【查询帖子 P】
parent_object_type: POST点赞评论的实现原理和「C 赞同了 B 在某个课程下的评测」的一样。多亏了「LIKE」和「VOTE」本身无实质内容的特性,我们无需去查询它,可以将父级降低一级,来减少一层嵌套。但是,即使在技术上实现了点赞评论,从业务角度考虑,我们也并不打算将其放入动态流。具体原因是我们无法实现接下来更为重要的「回复」场景,导致「点赞」的实现意义不大。
未实现的场景及思考
三级结构(2/4 未实现):
C 回复了 B 在 A 的帖子下的评论。即 A 发布 P,B 对 P 发布 M1,C 对 M1 发布 M2。
1
2
3
4
5
6
7user_id: C 【查询用户 C】
action: COMMENT
object_id: comment_id 【查询评论 M2】
object_type: COMMENT
parent_object_id: comment_id 【查询评论 M1】
parent_object_type: COMMENT
// 未能关联 帖子 P如果需要关联帖子 P,按照之前的逻辑,我们还得加上
grandparent_object_…
字段:1
2
3
4
5
6
7
8user_id: C 【查询用户 C】
action: COMMENT
object_id: comment_id 【查询评论 M2】
object_type: COMMENT
parent_object_id: comment_id 【查询评论 M1】
parent_object_type: COMMENT
grandparent_object_id: post_id 【查询帖子 P】
grandparent_object_type: POST这种做法实在比较愚蠢。
C 评论了 B 在某个课程下的评测。即有一个 L,B 对 L 发布 E,C 对 E 发布 M:
1
2
3
4
5
6
7
8
9
10user_id: C 【查询用户 C】
action: COMMENT
object_id: comment_id 【查询评论 M】
object_type: COMMENT
parent_object_id: eval_id 【查询评测 E】
parent_object_type: EVAL
// 无法关联 课程 L
// 傻蛋做法:
// grandparent_object_id: course_id 【查询课程 L】
// grandparent_object_type: COURSE
四级结构:
D 点赞了 在某个课程中 B 的评测下的 C 的评论。即有一个 L,B 对 L 发布 E,C 对 E 发布 M,D 对 M 点赞。
1
2
3
4
5
6
7
8
9
10user_id: D 【查询用户 D】
action: LIKE
object_id: comment_id 【查询评论 M】
object_type: COMMENT
parent_object_id: eval_id 【查询评测 E】
parent_object_type: EVAL
// 无法关联 课程 L
// 傻蛋做法:
// grandparent_object_id: course_id 【查询课程 L】
// grandparent_object_type: COURSED 回复了 在某个课程中 B 的评测下的 C 的评论。即有一个 L,B 对 L 发布 E,C 对 E 发布 M1,D 对 M1 发布 M2 。
1
2
3
4
5
6
7
8
9
10
11
12user_id: D 【查询用户 D】
action: COMMENT
object_id: comment_id 【查询评论 M2】
object_type: COMMENT
parent_object_id: comment_id 【查询评论 M1】
parent_object_type: COMMENT
// 无法关联 评测 E 和 课程 L
// 傻蛋蛋做法:
// grandparent_object_id: eval_id 【查询评测 E】
// grandparent_object_type: EVAL
// great_grandparent_object_id: course_id 【查询课程 L】
// great_grandparent_object_id: COURSE
反思优化
写到这里,发现我才是傻蛋。
让我们回到一开始,稍微换一个角度思考问题。既然 LIKE
和 VOTE
可以降级作为一层嵌套,我们为什么不把 action
为 COMMENT
的动态类型也进行「降级」处理呢?而至于评论本身的内容,考虑到应用场景,用户几乎不也不应该被允许编辑评论,所以评论内容可以单独拿出来作为一个字段。同理,课程名也不会变动,也可以单独作为字段。
而回复一个评论,我们可以直接当成回复该评论的父级。这样,在动态流上也方便展示。
至于点赞评论,这种操作由于过于频繁,会干扰动态流的信息性,我最终选择将其舍弃。
于是,嵌套关系的逻辑看起来清爽许多:
新的嵌套结构:
- 一级结构(帖子):
- A 发布了 一个帖子
- B 点赞了 一个帖子 => 考虑要不要舍弃
- B 评论了 一个帖子
- 二级结构(帖子 => 评论;课程 => 评测):
- 对 一个帖子,C 点赞了 一个评论 => 舍弃
- 对 一个帖子,C 回复了 一个评论 => 可转化为一级嵌套:「C 评论了 一个帖子」
- 对 一个课程,B 发布了 一个评测
- 对 一个课程,C 赞同了 一个评测
- 对 一个课程,C 评论了 一个评测
- 三级结构(课程 => 评测 => 评论):
- 对 一个课程的评测,D 点赞了 一个评论 => 舍弃
- 对 一个课程的评测,D 回复了 一个评论 => 可转化为二级嵌套:「C 评论了 一个课程的评测」
新的表结构:
1 | id => 动态 ID |
让我们来看一下具体的解决方案:
一级结构(帖子):
A 发布了 一个帖子。 【查询 2 次】
1
2
3
4user_id: A 【查询用户】
action: NEW
object_id: post_id 【查询帖子】
object_type: POSTB 点赞了 一个帖子。 【查询 2 次】
1
2
3
4user_id: B 【查询用户】
action: LIKE
object_id: post_id 【查询帖子】
object_type: POSTB 评论了 一个帖子。 【查询 2 次】
1
2
3
4
5user_id: B 【查询用户】
action: COMMENT
object_id: post_id 【查询帖子】
object_type: POST
content: comment_content
二级结构(帖子 => 评论;课程 => 评测):
对 一个课程,B 发布了 一个评测。 【查询 2 次】
1
2
3
4
5user_id: B 【查询用户】
action: NEW
object_id: eval_id 【查询评测】
object_type: EVAL
course: course_name对 一个课程,C 赞同了 一个评测。 【查询 2 次】
1
2
3
4
5user_id: C 【查询用户】
action: VOTE
object_id: eval_id 【查询评测】
object_type: EVAL
course: course_name对 一个课程,C 评论了 一个评测。 【查询 2 次】
1
2
3
4
5
6user_id: C 【查询用户】
action: COMMENT
object_id: eval_id 【查询评测】
object_type: EVAL
content: comment_content
course: course_name
我们从最初的 10 个场景中,舍弃了 2 个点赞评论的场景,转化了 2 个回复评论的场景,最终实现了以下 6 个:
- A 发布了帖子
- B 点赞了帖子 => 考虑要不要舍弃
- B 评论了帖子
- B 在课程 L 下发布了评测
- C 赞同了课程 L 下的评测
- C 评论了课程 L 下的评测
这显然算不得是个最佳实践,也不怎么优雅。而时间上的优化,换来的也是空间上的牺牲。
如果说还有什么可以优化的地方,那就是把 action
和 object_type
这两个字段变成 int 类型的 id,而不是字符串。但这在节省空间的同时,也可能会导致反语义化造成的易错。在数据库业务操作设计,和与前端的 API 沟通上,需要很好地维护文档或数据字典。