You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在MongoDB中使用$lookup关联数组内的文档?

MongoDB嵌套数组关联查询解决方案

问题背景

我有三个MongoDB集合:Posts、Languages和Translations,结构定义如下:

Post: {
    id: ObjectID
    title: String,
    translated_versions: ObjectID
}

TranslatedVersion: {
    id: ObjectID
    translations: Translation[]
}

// 仅为TranslatedVersion.translations中的对象结构,非独立集合
Translation: {
    post: ObjectID
    language: ObjectID
}

Language {
    id: ObjectID
    name: String
}

需求目标

希望将TranslatedVersion集合文档关联到Post中,同时把TranslatedVersion.translations数组内每个元素对应的Post和Language对象也关联展开,最终得到如下结构:

Post {
    title: "Test title",
    translated_versions: {
        id: 'some-id',
        translations: [
            {
                language: {
                    id: 'some-id',
                    name: 'Turkish'
                },
                post: {
                    id: 'some-id',
                    title: 'Turkish post'
                }
            },
            {
                language: {
                    id: 'some-id',
                    name: 'English'
                },
                post: {
                    id: 'some-id',
                    title: 'English post'
                }
            }
        ]
    }
}

尝试代码及问题

使用Golang开发时,我尝试了以下聚合管道代码:

lookup_translations_group := bson.D{
    {
        Key: "$lookup", Value: bson.D{
            {Key: "from", Value: Models.Translation},
            {Key: "localField", Value: "translations_group_id"},
            {Key: "foreignField", Value: "_id"},
            {Key: "as", Value: "translations"},
        },
    },
}
unwind_translations_group := bson.D{
    {
        Key: "$unwind", Value: bson.D{
            {Key: "path", Value: "$translations"},
            {Key: "preserveNullAndEmptyArrays", Value: true},
        },
    },
}
lookup_translations_posts := bson.D{
    {
        Key: "$lookup", Value: bson.D{
            {Key: "from", Value: Models.Post},
            {Key: "localField", Value: "translations.translations.post"},
            {Key: "foreignField", Value: "_id"},
            {Key: "as", Value: "translations.translations.post"},
        },
    },
}

执行mongo.Pipeline后,得到的post字段是数组而非单个对象,不符合预期。推测需要在lookup_translations_group阶段添加子管道,但不知道正确实现方式。

更新后示例数据

db.posts.insertMany([
   { "_id" : 1, "title": "English post", language: 1, category: 1, "translations": 1 },
   { "_id" : 2, "title": "Turkish post", language: 2, category: 1, "translations": 1 },
   { "_id" : 3, "title": "Finnish post", language: 3, category: 1, "translations": 1 }
])

db.languages.insertMany([
   { "_id" : 1, "name": "English" },
   { "_id" : 2, "name": "Turkish" },
   { "_id" : 3, "name": "Finnish" }
])

db.translations.insert({
   "_id" : 1, 
   translations: [
      { language: 1, post: 1 },
      { language: 2, post: 2 },
      { language: 3, post: 3 }
   ] 
})

db.categories.insert({
   "_id" : 1, 
   "name": "Random category"
})

期望最终结果

{
   "_id": 1,
   "title": "English post",
   "language": {
      "_id": 1,
      "name": "English"
   },
   "category": {
      "_id": 1,
      "name": "Random category"
   },
   "translations": {
      "_id": 1,
      translations: [
         {
            "language": {
               "_id": 1,
               "name": "English"
            },
            "post": {
               "_id": 1,
               "title": "English post",
               "category": {
                  "_id": 1,
                  "name": "Random category"
               }
            }
         },
         {
            "language": {
               "_id": 2,
               "name": "Turkish"
            },
            "post": {
               "_id": 2,
               "title": "Turkish post",
               "category": {
                  "_id": 1,
                  "name": "Random category"
               }
            }
         },
         {
            "language": {
               "_id": 3,
               "name": "Finnish"
            },
            "post": {
               "_id": 3,
               "title": "Finnish post",
               "category": {
                  "_id": 1,
                  "name": "Random category"
               }
            }
         }
      ]
   }
}

解决方案

问题核心在于普通$lookup始终返回数组,而我们需要单个对象。处理嵌套数组的关联查询,需结合子管道、$unwind和$group等操作,将关联结果转为单个对象。

方案一:分步关联展开

以下是完整的Golang聚合管道实现:

pipeline := mongo.Pipeline{
    // 关联Translations集合,获取翻译组文档
    {
        {Key: "$lookup", Value: bson.D{
            {Key: "from", Value: "translations"},
            {Key: "localField", Value: "translations"},
            {Key: "foreignField", Value: "_id"},
            {Key: "as", Value: "translations"},
        }},
    },
    // 展开translations数组(将lookup返回的数组转为单个对象)
    {
        {Key: "$unwind", Value: bson.D{
            {Key: "path", Value: "$translations"},
            {Key: "preserveNullAndEmptyArrays", Value: true},
        }},
    },
    // 关联translations中的language
    {
        {Key: "$lookup", Value: bson.D{
            {Key: "from", Value: "languages"},
            {Key: "localField", Value: "translations.translations.language"},
            {Key: "foreignField", Value: "_id"},
            {Key: "as", Value: "translations.translations.language"},
        }},
    },
    // 展开language数组为单个对象
    {
        {Key: "$unwind", Value: bson.D{
            {Key: "path", Value: "$translations.translations.language"},
            {Key: "preserveNullAndEmptyArrays", Value: true},
        }},
    },
    // 关联translations中的post
    {
        {Key: "$lookup", Value: bson.D{
            {Key: "from", Value: "posts"},
            {Key: "localField", Value: "translations.translations.post"},
            {Key: "foreignField", Value: "_id"},
            {Key: "as", Value: "translations.translations.post"},
        }},
    },
    // 展开post数组为单个对象
    {
        {Key: "$unwind", Value: bson.D{
            {Key: "path", Value: "$translations.translations.post"},
            {Key: "preserveNullAndEmptyArrays", Value: true},
        }},
    },
    // 关联post中的category
    {
        {Key: "$lookup", Value: bson.D{
            {Key: "from", Value: "categories"},
            {Key: "localField", Value: "translations.translations.post.category"},
            {Key: "foreignField", Value: "_id"},
            {Key: "as", Value: "translations.translations.post.category"},
        }},
    },
    // 展开category数组为单个对象
    {
        {Key: "$unwind", Value: bson.D{
            {Key: "path", Value: "$translations.translations.post.category"},
            {Key: "preserveNullAndEmptyArrays", Value: true},
        }},
    },
    // 关联当前post的language
    {
        {Key: "$lookup", Value: bson.D{
            {Key: "from", Value: "languages"},
            {Key: "localField", Value: "language"},
            {Key: "foreignField", Value: "_id"},
            {Key: "as", Value: "language"},
        }},
    },
    {
        {Key: "$unwind", Value: bson.D{
            {Key: "path", Value: "$language"},
            {Key: "preserveNullAndEmptyArrays", Value: true},
        }},
    },
    // 关联当前post的category
    {
        {Key: "$lookup", Value: bson.D{
            {Key: "from", Value: "categories"},
            {Key: "localField", Value: "category"},
            {Key: "foreignField", Value: "_id"},
            {Key: "as", Value: "category"},
        }},
    },
    {
        {Key: "$unwind", Value: bson.D{
            {Key: "path", Value: "$category"},
            {Key: "preserveNullAndEmptyArrays", Value: true},
        }},
    },
}

方案二:子管道简化嵌套关联

如果希望代码更紧凑,可以在$lookup中使用子管道,一次性完成嵌套数组的展开与关联:

pipeline := mongo.Pipeline{
    // 关联当前post的language
    {
        {Key: "$lookup", Value: bson.D{
            {Key: "from", Value: "languages"},
            {Key: "localField", Value: "language"},
            {Key: "foreignField", Value: "_id"},
            {Key: "as", Value: "language"},
        }},
    },
    {
        {Key: "$unwind", Value: bson.D{
            {Key: "path", Value: "$language"},
            {Key: "preserveNullAndEmptyArrays", Value: true},
        }},
    },
    // 关联当前post的category
    {
        {Key: "$lookup", Value: bson.D{
            {Key: "from", Value: "categories"},
            {Key: "localField", Value: "category"},
            {Key: "foreignField", Value: "_id"},
            {Key: "as", Value: "category"},
        }},
    },
    {
        {Key: "$unwind", Value: bson.D{
            {Key: "path", Value: "$category"},
            {Key: "preserveNullAndEmptyArrays", Value: true},
        }},
    },
    // 关联Translations并处理嵌套数组
    {
        {Key: "$lookup", Value: bson.D{
            {Key: "from", Value: "translations"},
            {Key: "localField", Value: "translations"},
            {Key: "foreignField", Value: "_id"},
            {Key: "as", Value: "translations"},
            {Key: "pipeline", Value: bson.A{
                // 展开嵌套的translations数组
                bson.D{{Key: "$unwind", Value: "$translations"}},
                // 关联language
                bson.D{
                    {Key: "$lookup", Value: bson.D{
                        {Key: "from", Value: "languages"},
                        {Key: "localField", Value: "translations.language"},
                        {Key: "foreignField", Value: "_id"},
                        {Key: "as", Value: "translations.language"},
                    }},
                },
                bson.D{{Key: "$unwind", Value: "$translations.language"}},
                // 关联post
                bson.D{
                    {Key: "$lookup", Value: bson.D{
                        {Key: "from", Value: "posts"},
                        {Key: "localField", Value: "translations.post"},
                        {Key: "foreignField", Value: "_id"},
                        {Key: "as", Value: "translations.post"},
                    }},
                },
                bson.D{{Key: "$unwind", Value: "$translations.post"}},
                // 关联post的category
                bson.D{
                    {Key: "$lookup", Value: bson.D{
                        {Key: "from", Value: "categories"},
                        {Key: "localField", Value: "translations.post.category"},
                        {Key: "foreignField", Value: "_id"},
                        {Key: "as", Value: "translations.post.category"},
                    }},
                },
                bson.D{{Key: "$unwind", Value: "$translations.post.category"}},
                // 重新组合translations数组
                bson.D{{Key: "$group", Value: bson.D{
                    {Key: "_id", Value: "$_id"},
                    {Key: "translations", Value: bson.D{{Key: "$push", Value: "$translations"}}},
                }}},
            }},
        }},
    },
    // 展开translations数组为单个对象
    {
        {Key: "$unwind", Value: bson.D{
            {Key: "path", Value: "$translations"},
            {Key: "preserveNullAndEmptyArrays", Value: true},
        }},
    },
}

关键说明

  • 普通$lookup的局限性:普通$lookup返回的始终是数组,因此每次关联后必须用$unwind展开,才能得到单个对象。
  • 嵌套数组处理顺序:先处理外层关联,再逐层处理内部嵌套数组的关联与展开,避免结构混乱。
  • 子管道优势:将嵌套数组的关联逻辑封装在子管道中,减少外层管道的步骤,代码更简洁易维护。

内容的提问来源于stack exchange,提问作者Andrew Rusinas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 05:31:23