如何在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
相关产品推荐
相关产品推荐

