2017-10-21 113 views
1

我有以下模式内部联接两个字段

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

这是非常有可能的一个请求,这与MongoDB的基本工具是$lookup

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

最优内部加入反

。假定实际的“模型”名字是什么上面列出:

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”你想从结果,这意味着“所有用户必须提到至少有一首歌”在涉及的两个属性。

$setUnion将“唯一列表”(ObjectId是唯一的)组合这两个。所以如果一个“作者”也是一个“合作者”,那么他们只会列出一首歌。

$setIntersection将列表从该组合列表“过滤”到仅在查询条件中指定的列表。这将删除所有不在选择中的其他“协作者”条目。

$lookup对该组合数据进行“加入”以获取用户,并且$unwind已完成,因为您希望User成为主要细节。所以我们基本上将“用户数组”转换为结果中的“歌曲数组”。

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


可选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 JOIN你居然$filter数组内容“后” $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预览

一个新的“子流水线”语法可$lookup从MongoDB的3.6版本意味着,而非如图所示为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允许的使用在"let"中声明的变量与歌曲集合中的每个条目进行比较,以仅选择那些与其他查询条件相匹配的条目。结果只是那些匹配的歌曲,每个用户或一个空数组。因此,使整个“子流水线”只是一个$match表达式,这与附加逻辑几乎相同,而不是固定的本地和外键。

所以你甚至可以在$lookup之后的管道中添加一个阶段来过滤掉任何“空”数组结果,从而使整个结果成为INNER加入。


因此,我个人认为可以采取第一种方法,只使用第二种方法。


注:这里有几个选择并不真正适用。第一种是$lookup + $unwind + $match coalescence的特例,其中虽然基本情况适用于最初的INNER Join示例,但它不能用于LEFT Join Case。

这是因为,为了使LEFT JOIN来获得的$unwind的使用必须preserveNullAndEmptyArrays: true实现,这打破了该应用程序的规则unwindingmatching不能内的“卷起” $lookup并应用于“返回结果之前”的国外集合。

因此,为什么它不在样本中应用,我们在返回的数组上使用$filter,因为没有可用于返回结果的“之前”的外部集合的最佳操作,并且没有任何操作会停止所有结果为只匹配外键的歌曲返回。 INNER加入当然是不同的。另一种情况是.populate()与猫鼬。最重要的区别是.populate()不是单个请求,但只是一个编程“实际上发出多个查询的简写”。所以无论如何,实际上会发出多个查询,并且总是需要所有结果才能应用任何过滤。

这导致了实际应用过滤的限制,通常意味着当您使用需要应用于外部收集的条件的“客户端连接”时,您无法真正实现“分页”概念。

关于Querying after populate in Mongoose的更多详细信息,以及实际演示了如何将基本功能作为自定义方法连接到猫鼬模式中,但实际上使用下面的流水线处理$lookup

+0

非常感谢,非常感谢。 –