线程消息系统数据库架构设计

Posted

技术标签:

【中文标题】线程消息系统数据库架构设计【英文标题】:thread messaging system database schema design 【发布时间】:2011-09-26 08:17:28 【问题描述】:

我正在努力实现此处所解释的内容: Creating a threaded private messaging system like facebook and gmail, 但是我不完全理解乔尔布朗的回答。谁能解释一下。

这是我的数据库表与示例数据的样子(我假设我出于演示目的正确填写了它):

    我需要显示基于 LoginId 的线程列表(最新的在顶部)查询在 LINQ 中会是什么样子? (我要问的是在一组消息线程中,给我每个线程中的 1 条最新消息) - 就像在 facebook 上完成的那样。

    我需要在消息线程 (LINQ) 中显示所有消息 -> 就像在 facebook 上完成一样,您在其中单击消息时会看到整个“对话”。

请帮忙! 谢谢

编辑 -> 继续 乔尔,这样对吗?

Joel,我有点困惑,你能解释一下吗(cmets/questions 以粗体显示):

这里的想法是,每次用户启动一个全新的线程/消息时,它都会从 THREAD 表中的一条新记录开始。然后将用户添加为 THREAD_PARTICIPANT,并将消息的内容添加到指向包含 THREAD 的 MESSAGE。从 MESSAGE 到 USER 的 FK 表示消息的作者。

LoginId 1 向 LoginId2 发送消息 => 将新记录插入到 MessageThread 表中。还将一条记录插入到 MessageThreadParticipant 记录中,MessageThreadId = 1,LoginId = 1(发送者)。并在 Message 表中插入一条新记录,MessageId =1,MessageThreadid =1,SenderLoginId = 1(正确??)

这是我在那次迭代之后所拥有的:

我想我很困惑,因为 Loginid 2 无法知道有消息给他。 ??或者也许我需要在 MessageThreadParticipant 中插入 2 条记录? (发送者和接收者)-> 这样都可以看到整个“对话”??

EDIT2: 乔,我想我可以这样做:

SELECT
  Message.MessageId, Message.CreateDate, Message.Body, Login.Username, Message.SenderLoginId
, (SELECT MessageReadState.ReadDate 
   FROM MessageReadState 
   WHERE MessageReadState.MessageId = Message.MessageId 
     ) as ReadDate
FROM Message 
    INNER JOIN Login ON Message.SenderLoginId = Login.LoginId
    INNER JOIN MessageThreadParticipant mtp on mtp.MessageThreadId = Message.MessageThreadId 
AND ( Message.MessageId in 
        ( SELECT Max(Message.MessageId)
          FROM MessageThreadParticipant INNER JOIN Message 
            ON MessageThreadParticipant.MessageThreadId = Message.MessageThreadId
          GROUP BY MessageThreadParticipant.MessageThreadId
        )
      )
Where mtp.LoginId = 2
ORDER BY Message.CreateDate DESC;

如果我错了,请纠正我:)

【问题讨论】:

Shane,我想你明白了。查看我为扩展示例数据编辑的答案。 Shane,我认为您无法使用编辑 2 中的查询来使其正常工作。我看到了两个问题。一是您只会看到相关用户阅读的消息,因为您是在内部加入 MTP。另一个是你试图在你的 FROM (...Message.MessageId in ( SELECT Max(Message.MessageId) ...) 中进行子选择,如果它完全有效的话,这是非常非常规的,我不这样做'认为它不会,因为您不能加入多值相等。子选择属于 WHERE 子句。 【参考方案1】:

那你为什么不问问? :)

让我试着确定我对您的要求的理解。在我看来,您正在查看的线程是两个人之间消息的线性列表(而不是树)。我认为您可能希望允许更多的人进入,而不仅仅是两个。这就像 Facebook,只要有人发布一条消息,然后任何数量的人都可以阅读它,然后开始添加 cmets。当您添加评论时,它会将您放入线程中,并且您开始收到状态更新和电子邮件,告诉您线程中的活动等等。假设这就是您所追求的,那么我向Big Mike 建议的架构并不是您想要的。

请考虑以下内容:

这里的想法是,每次用户启动一个全新的线程/消息时,它都会从 THREAD 表中的一条新记录开始。然后将用户添加为 THREAD_PARTICIPANT,并将消息的内容添加到指向包含 THREAD 的 MESSAGE。从 MESSAGE 到 USER 的 FK 表示消息的作者。

当用户阅读消息时,他们会在 MESSAGE_READ_STATE 表中获得一个条目,以表明他们已将消息标记为已读,无论是显式还是隐式,具体取决于您的要求。

当有人在线程中的初始消息上遇到问题时,第二个 MESSAGE 会添加一个 FK 返回到原始 THREAD,并且回复作者(用户)被添加到 THREAD_PARTICIPANT 表中。当消息被一个、两个甚至更多的参与者添加到线程中时,情况也是如此。

要获取任何线程中的最新消息,只需从 MESSAGE 中取出按创建日期(或身份密钥)降序排序的前 1 条消息,其中消息 FK 是发给感兴趣线程的。

要获取用户最近更新的线程,请从消息中获取与前 1 个相关的线程,该消息按创建日期降序排序,其中消息位于用户是 THREAD_PARTICIPANT 的线程中。

恐怕我永远无法在 LINQ 中陈述这些事情而不打破 LinqPad。如果您无法从上面理解我的偏差,我可以用表定义和一些 SQL 来充实答案。在 cmets 中询问即可。

编辑:需求和实施说明

澄清要求:最初我考虑的是公开发布消息并有机会发表评论,而 Shane 则更多的是直接消息功能。在这种情况下,最初的收件人需要一开始就包含在 THREAD_PARTICIPANT 表中。

为了清楚起见,让我们在表格中放置几行。这是场景,(为了纪念加拿大国庆日):用户 1 向用户 2 发送消息,询问关于开会喝啤酒的事情。用户 2 回答了一个关于在哪里见面和用户 1 回答的问题。表格看起来像这样:(可能过于简单)

编辑 #2:访问 SQL 以获取线程中所有消息的列表,具有读取状态...

使用@OP 的模式,此SQL 将获取给定线程中的消息列表,并指示给定用户是否已阅读每条消息。消息是最新的第一顺序。

SELECT 
  Message.MessageId
, Message.CreateDate
, Message.Body
, Login.Username
, (SELECT MessageReadState.ReadDate 
   FROM MessageReadState 
   WHERE MessageReadState.MessageId = Message.MessageId 
     and MessageReadState.LoginId = 2) as ReadState
FROM (Message INNER JOIN Login ON Message.SenderLoginId = Login.LoginId) 
WHERE (((Message.MessageThreadId)=10))
ORDER BY Message.CreateDate DESC;

请注意,如果可以公平地说,诀窍是读取状态是通过子选择获取的。这是必要的,因为获取读取状态的部分标准需要外连接无法满足的 where 子句。因此,您可以使用子选择从 MessageReadState 子表中确定您想要哪个(可能缺少)值。

编辑 3:SQL 用于获取给定用户的所有线程以及每个线程的最新消息...

要获取给定用户参与的所有线程的列表,首先按最新消息排序,仅显示最新消息(每个线程 1 条消息),然后您可以使用与上面的一个,除了不是通过他们的 FK 过滤消息到感兴趣的线程,您通过子查询过滤消息,该子查询在感兴趣的用户参与的每个线程中找到最新消息。它看起来像这样:

SELECT
  Message.MessageId
, Message.CreateDate
, Message.Body
, Login.Username
, (SELECT MessageReadState.ReadDate 
   FROM MessageReadState 
   WHERE MessageReadState.MessageId = Message.MessageId 
     and MessageReadState.LoginId = 2) AS ReadState
FROM Message INNER JOIN Login ON Message.SenderLoginId = Login.LoginId
WHERE ( Message.MessageId in 
        ( SELECT Max(Message.MessageId)
          FROM MessageThreadParticipant INNER JOIN Message 
            ON MessageThreadParticipant.MessageThreadId = Message.MessageThreadId
          WHERE MessageThreadParticipant.LoginId=2
          GROUP BY MessageThreadParticipant.MessageThreadId
        )
      )
ORDER BY Message.CreateDate DESC;

【讨论】:

@Spock - OP 的要求没有说明用户删除消息的任何内容,但可以轻松增强模型以适应选择“删除”消息的个人用户,同时将消息留给其他用户看法。这可以在线程级别通过删除该用户的THREAD_PARTICIPANT 记录来完成,或者在消息级别通过向MESSAGE_READ_STATE 添加代码或标志来指示用户已软删除该消息。 @user1066133 - 我认为代码首先有它的位置,尤其是作为原型工具或用于构建小型或小众系统。我个人认为代码优先是对一些不以数据为中心的程序员的让步。根据我的经验,一个经过深思熟虑(即刻意设计)的数据库是任何规模或重要性的应用程序的坚实基础。如果您的项目允许,我会先从模型开始。如果您必须先编写代码,则只需确保不会跳过数据设计步骤而将自己描绘成一个非常糟糕的角落。 @compguy24 根据缺少的信息排除某些内容的常用方法是使用 sub-selectWHERE NOT IN ...。因此,例如,您可以获取基本查询并将其添加到其 where 子句 AND WHERE Message.MessageID NOT IN (select clause that pulls out the message ids of the thing that you're waiting for) @DaveHarding - 是的,如果您想区分消息 read 和消息 deleted,您可以扩展 MESSAGE_READ_STATE 表以包括一个已删除的标志,或者最好是一个已删除的日期。这将提供软删除功能(以便您可以取消删除,如果您愿意),它允许线程中的一个参与者隐藏某些内容,而不会破坏其他线程参与者的数据。 如何检查两个用户之间是否已经存在线程,假设用户 1 和用户 2 已经存在线程 id=10,因此无需创建新线程。【参考方案2】:

根据 Joel Brown 的回答,您可以将 LAST_MESSAGE_ID 列添加到 THREAD 表中,然后获取所有线程的最后一条消息 SQL 变得非常简单。您必须在每条消息发送时更新此列。

获取给定用户的所有线程以及每个线程的最新消息

SELECT *
FROM THREAD T
INNER JOIN MESSAGE M ON T.LAST_MESSAGE_ID=M.MESSAGE_ID
INNER JOIN USER SENDER ON M.USER_ID=SENDER.USER_ID
LEFT JOIN MessageReadState MRS ON M.MESSAGE_ID=MRS.MESSAGE_ID AND MRS.USER_ID=2

【讨论】:

如何获取给定两个用户的 thread_id ?我需要这种查询吗 -SELECT * FROM message_thread_has_user as t1 join message_thread_has_user as t2 on t1.message_thread_id=t2.message_thread_id where (t1.user_id = 1 || t2.user_id = 1) and (t1.user_id = 2 || t2.user_id = 2) 按 t1.message_thread_id 分组 @Vivek 你应该在 MessageThread 表中添加一个唯一的 char 字段 =“unique_field”。它存储从最小用户 ID 到最大用户 ID 排序的用户 ID,并使用分隔符连接。示例:user1=15, user2=9 和 unique_field 将是“9_15”,sql 将是 select * from MessageThread where unique_field="9_15" @Alaxander 确实有意义,这将避免连接。谢谢。 @Vivek 你也可以使用 ThreadName 字段而不是为一对一的消息线程创建“unique_field”

以上是关于线程消息系统数据库架构设计的主要内容,如果未能解决你的问题,请参考以下文章

分布式公布订阅消息系统 Kafka 架构设计

架构设计:系统间通信(31)——其他消息中间件及场景应用(下1)

架构设计:系统间通信(32)——其他消息中间件及场景应用(下2)

Kafka架构设计简介

分布式发布订阅消息系统 Kafka 架构设计

架构设计:系统存储(15)——Redis基本概念和安装使用