Database design 使用MongoDB的类似Twitter的应用程序

Database design 使用MongoDB的类似Twitter的应用程序,database-design,mongodb,software-design,Database Design,Mongodb,Software Design,我正在制作一个应用程序,它使用了经典的“跟随”机制(Twitter和许多其他网络应用程序都使用这个机制)。我在用MongoDB。 不过,我的系统有一个区别:一个用户可以跟随一组用户。这意味着,如果您关注一个组,您将自动关注该组中的所有用户。当然,用户可以属于多个组 这就是我想到的: 当用户A跟随用户B时,用户B的id将添加到用户A文档中的嵌入式数组(称为跟随) 为了取消跟踪,我从following数组中删除了跟踪用户的id 组的工作方式相同:当用户A跟随组X时,组X的id被添加到跟随的数组中。

我正在制作一个应用程序,它使用了经典的“跟随”机制(Twitter和许多其他网络应用程序都使用这个机制)。我在用MongoDB。 不过,我的系统有一个区别:一个用户可以跟随一组用户。这意味着,如果您关注一个组,您将自动关注该组中的所有用户。当然,用户可以属于多个组

这就是我想到的:

  • 用户A跟随用户B时,用户B的id将添加到用户A文档中的嵌入式数组(称为
    跟随
  • 为了取消跟踪,我从
    following
    数组中删除了跟踪用户的id
  • 组的工作方式相同:当用户A跟随组X时,组X的id被添加到
    跟随的
    数组中。(我实际上添加了一个
    DBRef
    ,这样我就知道连接是到用户还是到组。)

  • 当我必须检查用户A是否跟随组X时,我只需在用户A的跟随数组中搜索组id

  • 当我必须检查用户A是否跟随用户B时,事情变得有点棘手。每个用户的文档都有一个嵌入式数组,列出用户所属的所有组。因此,我使用
    $或
    条件来检查用户A是直接跟踪用户B还是通过组跟踪用户B。这样地:
    
    db.users.find({'$or':{'following.ref.$id':$user\id,'following.ref.$ref','users'},{'following.ref.$id':{'$in':$group\uids},'following.ref.$ref':'groups'})

这很好,但我想我有一些问题。例如,如何显示特定用户的关注者列表,包括分页?我不能对嵌入文档使用skip()和limit()

我可以更改设计并使用
userfollow
集合,这与嵌入的
following
文档的工作相同。我尝试过的这种方法的问题是,在我前面使用的
$或
条件下,在包含同一用户的两个组之后的用户将被列出两次。为了避免这一点,我可以使用group或MapReduce,我确实这样做了,而且效果很好,但为了让事情更简单,我希望避免使用这一点。也许我需要跳出框框思考。或者我在两次尝试中都采取了错误的方法。有人已经做过类似的事情并提出了更好的解决方案吗


(这实际上是我的后续行动。我决定发布一个新问题来更好地解释我的新情况;我希望这不是问题。)

你有两种可能的方式,一个用户可以跟随另一个用户;直接或间接通过组,在这种情况下,用户直接跟随组。让我们从存储用户和组之间的这些直接关系开始:

{
  _id: "userA",
  followingUsers: [ "userB", "userC" ],
  followingGroups: [ "groupX", "groupY" ]
}
现在,您希望能够快速找出用户A直接或间接跟踪的用户。为了实现这一点,您可以对用户A所跟随的组进行非规范化。假设组X和Y的定义如下:

{
  _id: "groupX",
  members: [ "userC", "userD" ]
},
{
  _id: "groupY",
  members: [ "userD", "userE" ]
}
基于这些组和用户A的直接关系,您可以在用户之间生成订阅。订阅的来源与每个订阅一起存储。对于示例数据,订阅如下所示:

// abusing exclamation mark to indicate a direct relation
{ ownerId: "userA", userId: "userB", origins: [ "!" ] },
{ ownerId: "userA", userId: "userC", origins: [ "!", "groupX" ] },
{ ownerId: "userA", userId: "userD", origins: [ "groupX", "groupY" ] },
{ ownerId: "userA", userId: "userE", origins: [ "groupY" ] }
使用针对单个用户的map-reduce finalize调用,可以非常轻松地生成这些订阅。如果更新了组,则只需为跟随该组的所有用户重新运行map reduce,订阅将再次更新

地图缩小 以下map reduce函数将为单个用户生成订阅

map = function () {
  ownerId = this._id;

  this.followingUsers.forEach(function (userId) {
    emit({ ownerId: ownerId, userId: userId } , { origins: [ "!" ] });
  });

  this.followingGroups.forEach(function (groupId) {
    group = db.groups.findOne({ _id: groupId });

    group.members.forEach(function (userId) {
      emit({ ownerId: ownerId, userId: userId } , { origins: [ group._id ] });
    });
  });
}

reduce = function (key, values) {
  origins = [];

  values.forEach(function (value) {
    origins = origins.concat(value.origins);
  });

  return { origins: origins };
}

finalize = function (key, value) {
  db.subscriptions.update(key, { $set: { origins: value.origins }}, true);
}
然后,您可以通过指定查询为单个用户运行map reduce,在本例中为
userA

db.users.mapReduce(map, reduce, { finalize: finalize, query: { _id: "userA" }})
请注意:

  • 在为某个用户运行map reduce之前,应该删除该用户以前的订阅
  • 如果更新组,则应为跟随该组的所有用户运行map reduce

我应该注意到,这些map reduce函数比我想象的要复杂得多,因为MongoDB不支持数组作为reduce函数的返回值。理论上,这些函数可以简单得多,但与MongoDB不兼容。但是,如果有必要,这个更复杂的解决方案可以用于在一次调用中映射减少整个
用户
集合。

我的投票是使用map将关注者列表写入临时集合。我听说map/reduce可能很慢,所以我不能在每次加载页面时都这样做。这意味着关注者列表不会是最新的,所以我宁愿避免这种解决方案……这听起来是个不错的解决方案,谢谢。但是分页问题仍然存在:我不能对嵌入文档使用skip()/limit()。基本上正如我在问题中所说的,我需要列出用户正在关注的所有内容(与Twitter非常相似)。@Brainfeeder:您可以将每个订阅作为文档存储在单独的集合中,以绕过跳过/限制限制。然后,
“userA”
将是我提到的每个订阅的
所有者ID
,例如,
{ownerId:“userA”,userId:“userB”,origins:[“!”]}
。这正是我所想的。非常感谢@Brainfeeder:这是整个系列地图缩减的唯一情况。但是map reduce一次只针对一个用户。您没有减少整个
用户
集合,而是只减少一个文档,因此不会太慢。我将用一个例子更新我的答案,看看你的另一个问题。@Brainfeeder:为了解决这个问题,我必须引入一个对象来保存
origins
数组,并使用
concat()
函数在reduce函数中合并这些值。