13

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

这就是我想出的:

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

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

  • 当我必须检查用户 A是否跟随用户 B时,事情变得有点棘手。每个用户的文档都有一个嵌入式数组,列出了用户所属的所有组。因此,我使用$or条件来检查用户 A 是直接关注用户 B 还是通过群组关注用户 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,我确实这样做了并且它有效,但我很想避免这种情况以使事情变得更简单。也许我只需要跳出框框思考。或者,也许我两次尝试都采取了错误的方法。任何人都必须做类似的事情并提出更好的解决方案?

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

4

1 回答 1

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在一次调用中对整个集合进行 map-reduce。

于 2010-10-28T14:25:34.467 回答