内部加入两个领域

时间:2017-10-21 10:01:29

标签: mongodb mongoose aggregation-framework

我有以下架构

var User = mongoose.Schema({
    email:{type: String, trim: true, index: true, unique: true, sparse: true},
    password: String,
    name:{type: String, trim: true, index: true, unique: true, sparse: true},
    gender: String,
});

var Song = Schema({
    track: { type: Schema.Types.ObjectId, ref: 'track' },//Track can be deleted
    author: { type: Schema.Types.ObjectId, ref: 'user' },
    url: String,
    title: String,
    photo: String,
        publishDate: Date,
    views: [{ type: Schema.Types.ObjectId, ref: 'user' }],
    likes: [{ type: Schema.Types.ObjectId, ref: 'user' }],
    collaborators: [{ type: Schema.Types.ObjectId, ref: 'user' }],
});

我想选择所有用户(没有密码值),但我希望每个用户都拥有他作为作者或其中一个合作者的所有歌曲,并且是在过去两周内发布的。

执行此操作的最佳策略是什么(user.id和song .collaborators之间的绑定)?可以在一个选择中完成吗?

1 个答案:

答案 0 :(得分:2)

在一个请求中很有可能,MongoDB的基本工具是$lookup

我认为从Song集合查询实际上更有意义,因为您的标准是它们必须列在该集合的两个属性之一中。

最佳INNER加入 - 反转

假设实际的"模型"名称是上面列出的:

var today = new Date.now(),
    oneDay = 1000 * 60 * 60 * 24,
    twoWeeksAgo = new Date(today - ( oneDay * 14 ));

var userIds;   // Should be assigned as an 'Array`, even if only one

Song.aggregate([
  { "$match": { 
    "$or": [
      { "author": { "$in": userIds } },
      { "collaborators": { "$in": userIds } }
    ],
    "publishedDate": { "$gt": twoWeeksAgo }
  }},
  { "$addFields": { 
    "users": { 
      "$setIntersection": [ 
        userIds,
        { "$setUnion": [ ["$author"], "$collaborators" ] }
      ]
    }
  }},
  { "$lookup": {
    "from": User.collection.name,
    "localField": "users",
    "foreignField": "_id",
    "as": "users"
  }},
  { "$unwind": "$users" },
  { "$group": {
    "_id": "$users._id",
    "email": { "$first": "$users.email" },
    "name": { "$first": "$users.name" },
    "gender": { "$first": "$users.gender" },
    "songs": {
      "$push": {
        "_id": "$_id",
        "track": "$track",
        "author": "$author",
        "url": "$url",
        "title": "$title",
        "photo": "$photo",
        "publishedDate": "$publishedDate",
        "views": "$views",
        "likes": "$likes",
        "collaborators": "$collaborators"
      }
    }
  }}
])

对我而言,这是最符合逻辑的课程,只要它是一个内部联盟"" INNER JOIN"你想要的是结果,这意味着"所有用户必须在所涉及的两个属性中至少提及一首歌曲#34;

$setUnion采用"唯一列表" (ObjectId无论如何都是独特的)将这两者结合起来。所以如果一个"作者"也是一个"合作者"然后他们只为那首歌列出一次。

$setIntersection"过滤器"该组合列表中的列表仅包含在查询条件中指定的列表。这将删除任何其他"协作者"那些不在选择中的条目。

$lookup执行"加入"在用于获取用户的组合数据上,$unwind已完成,因为您希望User成为主要细节。所以我们基本上颠倒了#34;用户阵列"进入"歌曲阵列"在结果中。

此外,由于主要标准来自Song,因此从该集合中查询作为方向是有意义的。

可选LEFT加入

反过来说,这是" LEFT JOIN"被通缉,成为"所有用户"无论是否有任何相关歌曲:

User.aggregate([
  { "$lookup": {
    "from": Song.collection.name,
    "localField": "_id",
    "foreignField": "author",
    "as": "authors"
  }},
  { "$lookup": {
    "from": Song.collection.name,
    "localField": "_id",
    "foreignField": "collaborators",
    "as": "collaborators"
  }},
  { "$project": {
    "email": 1,
    "name": 1,
    "gender": 1,
    "songs": { "$setUnion": [ "$authors", "$collaborators" ] }
  }}
])

所以声明的列表"看起来"更短,但它强制"两个" $lookup阶段,以便获得可能的"作者"和"合作者"而不是一个。所以实际的"加入"操作在执行时间上可能会很昂贵。

其余的在应用相同的$setUnion时非常简单,但这次是"结果数组"而不是数据的原始来源。

如果你想要类似的"查询" "过滤器"上面的条件对于"歌曲"而不是返回的实际User文档,那么对于LEFT实际上加入$filter数组内容" post" $lookup

User.aggregate([
  { "$lookup": {
    "from": Song.collection.name,
    "localField": "_id",
    "foreignField": "author",
    "as": "authors"
  }},
  { "$lookup": {
    "from": Song.collection.name,
    "localField": "_id",
    "foreignField": "collaborators",
    "as": "collaborators"
  }},
  { "$project": {
    "email": 1,
    "name": 1,
    "gender": 1,
    "songs": { 
      "$filter": {
        "input": { "$setUnion": [ "$authors", "$collaborators" ] },
        "as": "s",
        "cond": { 
          "$and": [
            { "$setIsSubset": [
              userIds
              { "$setUnion": [ ["$$s.author"], "$$s.collaborators" ] }
            ]},
            { "$gte": [ "$$s.publishedDate", oneWeekAgo ] }
          ]
        }
      }
    }
  }}
])

这意味着,通过LEFT JOIN条件,返回所有User个文档,但只返回包含任何"歌曲的文档。将是那些符合"过滤器"条件作为提供的userIds的一部分。甚至包含在列表中的那些用户也只会显示那些"歌曲"在publishedDate所需的范围内。

$filter中的主要添加内容是$setIsSubset运算符,这是将userIds中提供的列表与"组合"进行比较的简短方法。列表来自文档中的两个字段。在这里注意到"当前用户"已经不得不"相关"由于每个$lookup的早期条件。

MongoDB 3.6预览

新的"子管道" MongoDB 3.6版本中$lookup可用的语法意味着而不是"两个"对于LEFT Join变体显示的$lookup阶段,您实际上可以将其构建为"子流水线",它还可以在返回结果之前优化过滤内容:

User.aggregate([
  { "$lookup": {
    "from": Song.collection.name,
    "let": {
      "user": "$_id"
    },
    "pipeline": [
      { "$match": {
        "$or": [
          { "author": { "$in": userIds } },
          { "collaborators": { "$in": userIds } }
        ],
        "publishedDate": { "$gt": twoWeeksAgo },
        "$expr": {
          "$or": [
            { "$eq": [ "$$user", "$author" ] },
            { "$setIsSubset": [ ["$$user"], "$collaborators" ]
          ]
        }
      }}
    ],
    "as": "songs"
  }}
])

在这种情况下就是这样,因为$expr允许将$$user中声明的"let"变量用于与歌曲集合中的每个条目进行比较以进行选择只有那些除了其他查询条件之外还匹配的那些。结果只是每个用户匹配的歌曲或空数组。从而使整个"子管道"只是一个$match表达式,与固定的本地和外键相对应,与其他逻辑基本相同。

所以你甚至可以在$lookup之后向管道添加一个阶段来过滤掉任何"空的"数组结果,使整体结果为INNER Join。

所以我个人会尽可能地采用第一种方法,只在你需要的地方使用第二种方法。

  

注意:这里有几个选项并不适用。第一个是$lookup + $unwind + $match coalescence的特殊情况,其中基本情况适用于初始INNER连接示例,但它不能应用于LEFT连接案例。

     

这是因为为了获得LEFT加入,$unwind 的使用必须preserveNullAndEmptyArrays: true一起实施,这打破了应用规则unwindingmatching不能"汇总"在$lookup内并适用于外国收藏品"之前"返回结果。

     

因此,为什么它不应用于样本中,我们在返回的数组上使用$filter,因为没有可以应用于外部集合的最佳操作"之前"返回结果,没有任何内容停止所有结果匹配简单的外键返回。 INNER Joins当然不同。

     

另一种情况是.populate()有猫鼬。最重要的区别是.populate() 不是单个请求,而只是编程"简写"实际发出多个查询。因此,无论如何,实际上会发出多个查询并且始终需要所有结果才能应用任何过滤。

     

这导致了对实际应用过滤的限制,并且通常意味着您无法真正实现"分页"利用"客户端连接"这要求条件适用于国外集合。

     

Querying after populate in Mongoose上有关于此的更多细节,并且实际演示了如何将基本功能作为mongoose模式中的自定义方法连接,但实际上使用{{3下面的管道处理。