如何使用 MongoDB 聚合 `$lookup` 作为 `findOne()`

Posted

技术标签:

【中文标题】如何使用 MongoDB 聚合 `$lookup` 作为 `findOne()`【英文标题】:How to use MongoDBs aggregate `$lookup` as `findOne()` 【发布时间】:2016-10-08 02:03:36 【问题描述】:

众所周知,find() 返回一个结果数组,findOne() 只返回一个简单的对象。

对于 Angular,这会产生巨大的差异。我可以直接写myresult.name,而不是myresult[0].name

我发现聚合管道中的$lookup 方法返回一个结果数组,而不仅仅是一个对象。

例如,我有两个集合:

users收藏:

[
  "firstName": "John",
  "lastName": "Smith",
  "country": 123
, 
  "firstName": "Luke",
  "lastName": "Jones",
  "country": 321
]

countries收藏:

[
  "name": "Australia",
  "code": "AU",
  "_id": 123
, 
  "name": "New Zealand",
  "code": "NZ",
  "_id": 321
]

我的聚合$lookup

db.users.aggregate([
  $project: 
    "fullName": 
      $concat: ["$firstName", " ", "$lastName"]
    ,
    "country": "$country"
  
, 
  $lookup: 
    from: "countries",
    localField: "country",
    foreignField: "_id",
    as: "country"
  
])

查询结果:

[
  "fullName": "John Smith",
  "country": [
    "name": "Australia",
    "code": "AU",
    "_id": 123
  ]
, 
 "fullName": "Luke Jones",
 "country": [
   "name": "New Zealand",
   "code": "NZ",
   "_id": 321
 ]
]

从上面的结果可以看出,每个country 都是一个数组,而不是像"country": .... 这样的单个对象。

我怎样才能让我的 $lookup 返回单个对象而不是数组,因为它只会匹配单个文档?

【问题讨论】:

我也觉得这应该从他们的角度来处理。如果只有一个匹配,它应该返回一个对象,而不是一个包含单个对象的数组。 【参考方案1】:
db.users.aggregate([
    
        $lookup: 
            from: 'countries',
            localField: 'country',
            foreignField: '_id',
            as: 'country'
        
    ,
    
        $unwind: '$country'
    
]).pretty()

您可以使用这个 mongo 查询来获取国家/地区对象

【讨论】:

【参考方案2】:

当您不想重复项目中的所有字段时,只需使用 $addFields 覆盖有问题的字段即可:

db.users.aggregate([
       "$project":      
        "fullName":        
            "$concat": [ "$firstName", " ", "$lastName"]     
        ,
        "country": "$country"   
    , 
     "$lookup":      
        "from": "countries",     
        "localField": "country",     
        "foreignField": "_id",     
        "as": "countryInfo"   
    ,
     "$addFields": 
        "countryInfo": 
            "$arrayElemAt": [ "$countryInfo", 0 ]
        
    
])

【讨论】:

这是我提供的解决方案。 $project 在向集合中添加更多字段时使代码的可维护性降低。如果你在做$unwind,请确保"preserveNullAndEmptyArrays": true。但是,如果找到多个匹配项,则会返回意外结果。这个答案应该适用于findOne 关系的所有情况【参考方案3】:

你也可以使用"preserveNullAndEmptyArrays"

像这样:

 db.users.aggregate(
        [
               "$project":      
                "fullName":        
                    "$concat": [ "$firstName", " ", "$lastName"]     
                ,
                "country": "$country"   
            , 
             "$lookup":      
                    "from": "countries",     
                    "localField": "country",     
                    "foreignField": "_id",     
                    "as": "countryInfo"   
            , 
            "$unwind": 
                    "path": "$countryInfo",
                    "preserveNullAndEmptyArrays": true
                
            ,
        ]
    )

【讨论】:

$unwind: path: '$countryInfo' 就足够了。 为了方便使用,为什么不创建视图并将其作为普通集合引用(有一些限制)docs.mongodb.com/manual/core/views $unwind: '$countryInfo' 也够了 @benipsen 如果您没有 1-1 关系,将忽略 preserveNullAndEmptyArrays 过滤掉没有 countryInfo 的身份或将 countryInfo 未定义? @CervEd 感谢您提出这个可能的问题。我测试过,确实这似乎是一个问题——在这种情况下,$unwind 实际上是一个完全连接,如果数组为空,它将删除整个条目。所以,"preserveNullAndEmptyArrays": true 是必要的。【参考方案4】:

您快到了,您需要将另一个$project 阶段添加到您的管道并使用$arrayElemAt 返回数组中的单个元素。

db.users.aggregate(
    [
           "$project":      
            "fullName":        
                "$concat": [ "$firstName", " ", "$lastName"]     
            ,
            "country": "$country"   
        , 
         "$lookup":      
                "from": "countries",     
                "localField": "country",     
                "foreignField": "_id",     
                "as": "countryInfo"   
        , 
         "$project":  
            "fullName": 1, 
            "country": 1, 
            "countryInfo":  "$arrayElemAt": [ "$countryInfo", 0 ]  
         
    ]
)

【讨论】:

现在你也可以使用$unwinddocs.mongodb.com/manual/reference/operator/aggregation/unwind 作为游戏后期的 2 美分附加组件,我喜欢 $lookup$unwind 的配对。我的一部分大脑喜欢认为这种组合可以在服务器端以类似于$limit$sort 组合的方式进行优化(其中sort 操作仅跟踪limit 记录)。 如果你想控制哪些字段包含在使用$arrayElemAt 选择的元素中——无需执行另一个$project 阶段——你可以使用$let,如下所述:***.com/a/39650846/483520跨度> 用 $set 代替 $project 怎么样?

以上是关于如何使用 MongoDB 聚合 `$lookup` 作为 `findOne()`的主要内容,如果未能解决你的问题,请参考以下文章

Mongodb 聚合管道限制 $lookup 字段

MongoDB 聚合 - $lookup 性能

Mongodb 聚合 $lookup 和 group 非常非常慢

使用聚合 $lookup 加入 mongodb

MongoDB在具有附加字段的对象数组上聚合$lookup

MongoDB在具有附加字段的对象数组上聚合$lookup