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

时间:2010-10-28 12:29:42

标签: database-design mongodb software-design

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

这就是我提出的:

  • 用户A 跟随用户B 时,用户B的ID被添加到用户A文档中的嵌入式数组(名为following
  • 要取消关注,我会从following数组
  • 中删除所关注用户的ID
  • 群组的工作方式相同:当用户A 跟随群组X 时,群组X的ID会添加到following数组中。 (我实际上添加了DBRef,所以我知道连接是针对用户还是组。)

  • 当我必须检查用户A 是否跟在组X 之后,我只是在用户A 中搜索群组的ID在数组之后。

  • 当我必须检查用户A 是否跟随用户B 时,事情变得有点棘手。每个用户的文档都有一个嵌入式数组,列出了用户所属的所有组。因此,我使用$or条件来检查用户A是直接跟踪用户B还是通过组跟踪。像这样:
      

    db.users.find({'$or':{'following.ref.$id':$user_id,'following.ref.$ref','users'},{'following.ref.$id':{'$in':$group_ids},'following.ref.$ref':'groups'}}})

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

我可以更改设计并使用userfollow集合,这将对嵌入式following文档执行相同的工作。我尝试过这种方法的问题是,使用我之前使用的$or条件,跟随两个包含相同用户的组的用户将被列出两次。为了避免这种情况,我可以使用group或MapReduce,我实际上做了它并且它可以工作,但是我希望避免这样做以保持简单。也许我只需要开箱即用。或者也许我在两次尝试时采取了错误的方法。任何人都必须做类似的事情并提出更好的解决方案?

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

1 个答案:

答案 0 :(得分:15)

用户可以通过两种方式关注其他用户;直接或间接通过组,在这种情况下,用户直接跟随该组。我们首先在用户和组之间存储这些直接关系:

{
  _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不兼容。但是,如果您需要,可以使用这个更复杂的解决方案来映射 - 减少整个users集合。