类知乎社区动态的数据库实现

问题来了

这些天给 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
2
3
4
5
6
7
id                    => 动态 ID
user_id => 动态实施者的用户 ID
action => 动态类别,可选常量:"POST", "EVAL", "LIKE", "VOTE", "COMMENT"
object_id => 对象 ID,如果 action 是 "LIKE", "VOTE" 类型,指向父级
object_type => 对象类别,如果 action 是 "LIKE", "VOTE" 类型,指向父级
parent_object_id => 父级对象 ID,如果 action 是 "LIKE", "VOTE" 类型,指向父级的父级
parent_object_type => 父级对象类别,如果 action 是 "LIKE", "VOTE" 类型,指向父级的父级

具体场景的实现如下:

符号规定:

  • 用户 A, B, C
  • 帖子 P (Post)
  • 测评 E (Evaluation)
  • 课程 L (Lesson)
  • 评论 M (coMment)
  • 一级结构(1/1 实现):

    • A 发布了 帖子。即 A 发布 P。【查询 2 次】

      1
      2
      3
      4
      user_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
      4
      user_id: B 【查询用户 B】
      action: LIKE
      object_id: post_id 【查询帖子 P】
      object_type: POST
    • B 评论了 A 的帖子。即 A 发布 P,B 对 P 发布 M。【查询 3 次】

      1
      2
      3
      4
      5
      6
      user_id: B 【查询用户 B】
      action: COMMENT
      object_id: comment_id 【查询评论 M】
      object_type: COMMENT
      parent_object_id: post_id 【查询帖子 P】
      parent_object_type: POST
    • B 测评了 某个课程。即有一个 L,B 对 L 发布 E。 【查询 3 次】

      1
      2
      3
      4
      5
      6
      user_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
      6
      user_id: C 【查询用户 C】
      action: VOTE
      object_id: eval_id 【查询测评 E】
      object_type: EVAL
      parent_object_id: course_id 【查询课程 L】
      parent_object_type: COURSE
    • C 点赞了 B 在 A 的帖子下的评论。即 A 发布 P,B 对 P 发布 M,C 对 M 点赞。【查询 3 次】

      1
      2
      3
      4
      5
      6
      user_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
      7
      user_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
      8
      user_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
      10
      user_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
      10
      user_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: COURSE
    • D 回复了 在某个课程中 B 的评测下的 C 的评论。即有一个 L,B 对 L 发布 E,C 对 E 发布 M1,D 对 M1 发布 M2 。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      user_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

反思优化

写到这里,发现我才是傻蛋。

让我们回到一开始,稍微换一个角度思考问题。既然 LIKEVOTE 可以降级作为一层嵌套,我们为什么不把 actionCOMMENT 的动态类型也进行「降级」处理呢?而至于评论本身的内容,考虑到应用场景,用户几乎不也不应该被允许编辑评论,所以评论内容可以单独拿出来作为一个字段。同理,课程名也不会变动,也可以单独作为字段。

而回复一个评论,我们可以直接当成回复该评论的父级。这样,在动态流上也方便展示。

至于点赞评论,这种操作由于过于频繁,会干扰动态流的信息性,我最终选择将其舍弃。

于是,嵌套关系的逻辑看起来清爽许多:

新的嵌套结构:

  • 一级结构(帖子):
    • A 发布了 一个帖子
    • B 点赞了 一个帖子 => 考虑要不要舍弃
    • B 评论了 一个帖子
  • 二级结构(帖子 => 评论;课程 => 评测):
    • 对 一个帖子,C 点赞了 一个评论 => 舍弃
    • 对 一个帖子,C 回复了 一个评论 => 可转化为一级嵌套:「C 评论了 一个帖子」
    • 对 一个课程,B 发布了 一个评测
    • 对 一个课程,C 赞同了 一个评测
    • 对 一个课程,C 评论了 一个评测
  • 三级结构(课程 => 评测 => 评论):
    • 对 一个课程的评测,D 点赞了 一个评论 => 舍弃
    • 对 一个课程的评测,D 回复了 一个评论 => 可转化为二级嵌套:「C 评论了 一个课程的评测」

新的表结构:

1
2
3
4
5
6
7
id                    => 动态 ID
user_id => 动态实施者的用户 ID
action => 操作类别,可选常量:"NEW", "LIKE", "VOTE", "COMMENT"
object_id => 对象 ID
object_type => 对象类别,可选常量;"POST", "EVAL", "COMMENT"
content => 评论内容,仅在 action 为 "COMMENT" 时有效
course => 课程名,仅在涉及课程时有效

让我们来看一下具体的解决方案:

  • 一级结构(帖子):

    • A 发布了 一个帖子。 【查询 2 次】

      1
      2
      3
      4
      user_id: A 【查询用户】
      action: NEW
      object_id: post_id 【查询帖子】
      object_type: POST
    • B 点赞了 一个帖子。 【查询 2 次】

      1
      2
      3
      4
      user_id: B 【查询用户】
      action: LIKE
      object_id: post_id 【查询帖子】
      object_type: POST
    • B 评论了 一个帖子。 【查询 2 次】

      1
      2
      3
      4
      5
      user_id: B 【查询用户】
      action: COMMENT
      object_id: post_id 【查询帖子】
      object_type: POST
      content: comment_content
  • 二级结构(帖子 => 评论;课程 => 评测):

    • 对 一个课程,B 发布了 一个评测。 【查询 2 次】

      1
      2
      3
      4
      5
      user_id: B 【查询用户】
      action: NEW
      object_id: eval_id 【查询评测】
      object_type: EVAL
      course: course_name
    • 对 一个课程,C 赞同了 一个评测。 【查询 2 次】

      1
      2
      3
      4
      5
      user_id: C 【查询用户】
      action: VOTE
      object_id: eval_id 【查询评测】
      object_type: EVAL
      course: course_name
    • 对 一个课程,C 评论了 一个评测。 【查询 2 次】

      1
      2
      3
      4
      5
      6
      user_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 下的评测

这显然算不得是个最佳实践,也不怎么优雅。而时间上的优化,换来的也是空间上的牺牲。

如果说还有什么可以优化的地方,那就是把 actionobject_type 这两个字段变成 int 类型的 id,而不是字符串。但这在节省空间的同时,也可能会导致反语义化造成的易错。在数据库业务操作设计,和与前端的 API 沟通上,需要很好地维护文档或数据字典。