Node.js 在嵌套数组的每个项中重新映射objectid数组

Node.js 在嵌套数组的每个项中重新映射objectid数组,node.js,mongodb,mongoose,mongodb-query,aggregation-framework,Node.js,Mongodb,Mongoose,Mongodb Query,Aggregation Framework,我有一个文档,其中包含用户生成的标签,还有条目,每个条目都有一个标签ID数组(或者可能没有): 如何使用标记数组中的相应值“填充”每个条目的标记数组?我尝试了$lookup和aggregate,但它太复杂了,无法正确执行。从实际数据的外观来看,没有必要在此处填充(),因为您要“加入”的数据不仅在同一集合中,而且实际上在同一文档中。这里您想要的是,甚至只是在文档的一个数组中获取值并将它们合并到另一个数组中 聚合$map转换 这里需要做的基本情况是转换输出中的每个数组。这些是“条目”,在每个“条目”

我有一个文档,其中包含用户生成的
标签
,还有
条目
,每个条目都有一个标签ID数组(或者可能没有):


如何使用标记数组中的相应值“填充”每个条目的标记数组?我尝试了$lookup和aggregate,但它太复杂了,无法正确执行。

从实际数据的外观来看,没有必要在此处填充(),因为您要“加入”的数据不仅在同一集合中,而且实际上在同一文档中。这里您想要的是,甚至只是在文档的一个数组中获取值并将它们合并到另一个数组中

聚合$map转换 这里需要做的基本情况是转换输出中的每个数组。这些是
“条目”
,在每个“条目”中,通过将值与父文档的
“标记”
数组中的值相匹配来转换
“标记”

Project.aggregate([
  { "$project": {
    "entries": {
      "$map": {
        "input": "$entries",
        "as": "e",
        "in": {
          "someField": "$$e.someField",
          "otherField": "$$e.otherField",
          "tags": {
            "$map": {
              "input": "$$e.tags",
              "as": "t",
              "in": {
                "$arrayElemAt": [
                  "$tags",
                  { "$indexOfArray": [ "$tags._id", "$$t" ] }
                ]
              }
            }
          }
        }
      }
    }
  }}
])
请注意,
“someField”
“otherField”
作为字段的占位符,这些字段“可能”出现在数组的每个“条目”文档中的该级别。唯一需要注意的是,参数中的
“指定的是实际得到的唯一的输出,因此需要明确命名“变量键”结构中的每个潜在字段,并包括
“标记”

在MongoDB 3.6之后的现代版本中,与此相反的是使用MongoDB 3.6,它允许将
“标记”
的“重新映射”内部数组“合并”到每个数组成员的“条目”文档中:

Project.aggregate([
  { "$project": {
    "entries": {
      "$map": {
        "input": "$entries",
        "as": "e",
        "in": {
          "$mergeObjects": [
            "$$e",
            { "tags": {
              "$map": {
                "input": "$$e.tags",
                "as": "t",
                "in": {
                  "$arrayElemAt": [
                    "$tags",
                    { "$indexOfArray": [ "$tags._id", "$$t" ] }
                  ]
                }
              }
            }}
          ]
        }
      }
    }
  }}
])
对于
“tags”
的“内部”数组上的实际值,您可以在这里使用运算符根据
\u id
属性与该“内部”数组的当前条目值匹配的位置,与
“tags”
的“根级别”字段进行比较。返回该“索引”后,操作符然后从匹配的“索引”位置“提取”实际数组项,并将中的当前数组项与该元素一起移植

这里唯一需要注意的是,由于某种原因,两个数组实际上没有匹配的条目。如果您已经注意到了这一点,那么这里的代码就可以了。如果存在不匹配,您可能需要匹配元素并采用at索引
0

    "in": {
      "$arrayElemAt": [
        { "$filter": {
          "input": "$tags",
          "cond": { "$eq": [ "$$this._id", "$$t" ] }
        }},
        0
      ]
    }
原因是这样做允许一个不匹配的
null
,但将返回
-1
,与一起使用的返回“last”数组元素。当然,“最后”元素在该场景中不是“匹配”结果,因为没有匹配

客户端转换 因此,从“仅”返回
“条目”
内容“重新映射”并从文档根中丢弃
“标记”
的角度来看,聚合过程(如果可能)是更好的选择,因为服务器只返回您实际需要的元素

如果您不能这样做,或者根本不关心是否还返回了现有的
“tags”
元素,那么这里就根本不需要进行聚合转换。事实上,“服务器”不需要做任何事情,而且考虑到所有数据都已经在文档中,“附加”转换只是增加了文档的大小,所以可能“不应该”

因此,一旦结果返回到客户机,这一切实际上都是可能的,对于文档的简单转换,正如上面的聚合管道示例所示,您实际需要的唯一代码是:

let results = await Project.find().lean();

results = results.map(({ entries, tags, ...r }) =>
  ({
    ...r,
    entries: entries.map(({ tags: etags, ...e }) =>
      ({
        ...e,
        tags: etags.map( tid => tags.find(t => t._id.equals(tid)) )
      })
    ),
    // tags
  })
);
这将提供完全相同的结果,甚至可以通过删除注释将
标记保留在其中。它甚至基本上是“完全相同的过程”,在每个数组上使用,以便对每个数组进行转换

使用现代JavaScript,“merge”的语法要简单得多,总体而言,该语言要简洁得多。为了“查找”两个数组中与
标记匹配的内容,需要注意的另一件事是方法,它需要实际比较这两个值并内置到返回的类型中

当然,由于您正在“转换”文档,为了实现这一点,您可以在任何mongoose操作上使用它来返回要处理的结果,因此返回的数据实际上是普通的JavaScript对象,而不是绑定到模式的mongoose
Document
类型,这是默认返回

结论与论证 这里的一般经验是,如果您希望在返回的响应中“减少数据”,那么
aggregate()
方法适合您。但是,如果您决定仍然需要“整个”文档数据,并且只想在响应中“增加”这些其他数组项,那么只需将数据带回“客户机”并在那里进行转换即可。考虑到在这种情况下,“添加”只是增加有效载荷响应的重量,理想情况下尽可能“向前”

完整的示范清单包括:

const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

const uri = 'mongodb://localhost/test';

mongoose.Promise = global.Promise;
mongoose.set('debug', true);

const tagSchema = new Schema({
  name: String,
  color: String
});

const projectSchema = new Schema({
  entries: [],
  tags: [tagSchema]
});

const Project = mongoose.model('Project', projectSchema);

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {
    const conn = await mongoose.connect(uri);

    let db = conn.connections[0].db;

    let { version } = await db.command({ buildInfo: 1 });
    version = parseFloat(version.match(new RegExp(/(?:(?!-).)*/))[0]);

    await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));

    await Project.insertMany(data);

    let pipeline = [
      { "$project": {
        "entries": {
          "$map": {
            "input": "$entries",
            "as": "e",
            "in": {
              "someField": "$$e.someField",
              "otherField": "$$e.otherField",
              "tags": {
                "$map": {
                  "input": "$$e.tags",
                  "as": "t",
                  "in": {
                    "$arrayElemAt": [
                      "$tags",
                      { "$indexOfArray": [ "$tags._id", "$$t" ] }
                    ]
                  }
                }
              }
            }
          }
        }
      }}
    ];

    let other = [
      {
        ...(({ $project: { entries: { $map: { input, as, ...o } } } }) =>
          ({
            $project: {
              entries: {
                $map: {
                  input,
                  as,
                  in: {
                    "$mergeObjects": [ "$$e", { tags: o.in.tags } ]
                  }
                }
              }
            }
          })
        )(pipeline[0])
      }
    ];

    let tests = [
      { name: 'Standard $project $map', pipeline },
      ...(version >= 3.6) ?
        [{ name: 'With $mergeObjects', pipeline: other }] : []
    ];

    for ( let { name, pipeline } of tests ) {
      let results = await Project.aggregate(pipeline);
      log({ name, results });
    }


    // Client Manipulation

    let results = await Project.find().lean();

    results = results.map(({ entries, tags, ...r }) =>
      ({
        ...r,
        entries: entries.map(({ tags: etags, ...e }) =>
          ({
            ...e,
            tags: etags.map( tid => tags.find(t => t._id.equals(tid)) )
          })
        )
      })
    );

    log({ name: 'Client re-map', results });

    mongoose.disconnect();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})();

// Data

const data =[
  {
    "_id": ObjectId("5ae5afc93e1d0d2965a4f2d7"),
    "entries" : [
      {
        "_id" : ObjectId("5b159ebb0ed51064925dff24"),
        "someField": "someData",
        "tags" : [
          ObjectId("5b142ab7e419614016b8992d")
        ]
      },
    ],
    "tags" : [
      {
        "_id" : ObjectId("5b142608e419614016b89925"),
        "name" : "Outdated",
        "color" : "#3498db"
      },
      {
        "_id" : ObjectId("5b142ab7e419614016b8992d"),
        "name" : "Shitake",
        "color" : "#95a5a6"
      },
    ]
  },
  {
    "_id": ObjectId("5b1b1ad07325c4c541e8a972"),
    "entries" : [
      {
        "_id" : ObjectId("5b1b1b267325c4c541e8a973"),
        "otherField": "otherData",
        "tags" : [
          ObjectId("5b142608e419614016b89925"),
          ObjectId("5b142ab7e419614016b8992d")
        ]
      },
    ],
    "tags" : [
      {
        "_id" : ObjectId("5b142608e419614016b89925"),
        "name" : "Outdated",
        "color" : "#3498db"
      },
      {
        "_id" : ObjectId("5b142ab7e419614016b8992d"),
        "name" : "Shitake",
        "color" : "#95a5a6"
      },
    ]
  }
];
这将提供完整的输出(支持MongoDB 3.6实例的可选输出),如下所示:


注意:这包括一些额外的数据来演示“变量域”的投影。

只需在同一问题上给出详细的答案,在你询问的同一问题上给出另一个答案。使用
populate()。这篇文章解释了原因。谢谢。这是一个详细的答案!我很快会看一看,如果需要的话,会标记为dupe谢谢你的投票,我有点倾向于指导dupe,但我记得你以前标记过自我复制。还有一个连续的否决票问题(不需要细节,希望问题能自行解决)
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

const uri = 'mongodb://localhost/test';

mongoose.Promise = global.Promise;
mongoose.set('debug', true);

const tagSchema = new Schema({
  name: String,
  color: String
});

const projectSchema = new Schema({
  entries: [],
  tags: [tagSchema]
});

const Project = mongoose.model('Project', projectSchema);

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {
    const conn = await mongoose.connect(uri);

    let db = conn.connections[0].db;

    let { version } = await db.command({ buildInfo: 1 });
    version = parseFloat(version.match(new RegExp(/(?:(?!-).)*/))[0]);

    await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));

    await Project.insertMany(data);

    let pipeline = [
      { "$project": {
        "entries": {
          "$map": {
            "input": "$entries",
            "as": "e",
            "in": {
              "someField": "$$e.someField",
              "otherField": "$$e.otherField",
              "tags": {
                "$map": {
                  "input": "$$e.tags",
                  "as": "t",
                  "in": {
                    "$arrayElemAt": [
                      "$tags",
                      { "$indexOfArray": [ "$tags._id", "$$t" ] }
                    ]
                  }
                }
              }
            }
          }
        }
      }}
    ];

    let other = [
      {
        ...(({ $project: { entries: { $map: { input, as, ...o } } } }) =>
          ({
            $project: {
              entries: {
                $map: {
                  input,
                  as,
                  in: {
                    "$mergeObjects": [ "$$e", { tags: o.in.tags } ]
                  }
                }
              }
            }
          })
        )(pipeline[0])
      }
    ];

    let tests = [
      { name: 'Standard $project $map', pipeline },
      ...(version >= 3.6) ?
        [{ name: 'With $mergeObjects', pipeline: other }] : []
    ];

    for ( let { name, pipeline } of tests ) {
      let results = await Project.aggregate(pipeline);
      log({ name, results });
    }


    // Client Manipulation

    let results = await Project.find().lean();

    results = results.map(({ entries, tags, ...r }) =>
      ({
        ...r,
        entries: entries.map(({ tags: etags, ...e }) =>
          ({
            ...e,
            tags: etags.map( tid => tags.find(t => t._id.equals(tid)) )
          })
        )
      })
    );

    log({ name: 'Client re-map', results });

    mongoose.disconnect();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})();

// Data

const data =[
  {
    "_id": ObjectId("5ae5afc93e1d0d2965a4f2d7"),
    "entries" : [
      {
        "_id" : ObjectId("5b159ebb0ed51064925dff24"),
        "someField": "someData",
        "tags" : [
          ObjectId("5b142ab7e419614016b8992d")
        ]
      },
    ],
    "tags" : [
      {
        "_id" : ObjectId("5b142608e419614016b89925"),
        "name" : "Outdated",
        "color" : "#3498db"
      },
      {
        "_id" : ObjectId("5b142ab7e419614016b8992d"),
        "name" : "Shitake",
        "color" : "#95a5a6"
      },
    ]
  },
  {
    "_id": ObjectId("5b1b1ad07325c4c541e8a972"),
    "entries" : [
      {
        "_id" : ObjectId("5b1b1b267325c4c541e8a973"),
        "otherField": "otherData",
        "tags" : [
          ObjectId("5b142608e419614016b89925"),
          ObjectId("5b142ab7e419614016b8992d")
        ]
      },
    ],
    "tags" : [
      {
        "_id" : ObjectId("5b142608e419614016b89925"),
        "name" : "Outdated",
        "color" : "#3498db"
      },
      {
        "_id" : ObjectId("5b142ab7e419614016b8992d"),
        "name" : "Shitake",
        "color" : "#95a5a6"
      },
    ]
  }
];
Mongoose: projects.remove({}, {})
Mongoose: projects.insertMany([ { entries: [ { _id: 5b159ebb0ed51064925dff24, someField: 'someData', tags: [ 5b142ab7e419614016b8992d ] } ], _id: 5ae5afc93e1d0d2965a4f2d7, tags: [ { _id: 5b142608e419614016b89925, name: 'Outdated', color: '#3498db' }, { _id: 5b142ab7e419614016b8992d, name: 'Shitake', color: '#95a5a6' } ], __v: 0 }, { entries: [ { _id: 5b1b1b267325c4c541e8a973, otherField: 'otherData', tags: [ 5b142608e419614016b89925, 5b142ab7e419614016b8992d ] } ], _id: 5b1b1ad07325c4c541e8a972, tags: [ { _id: 5b142608e419614016b89925, name: 'Outdated', color: '#3498db' }, { _id: 5b142ab7e419614016b8992d, name: 'Shitake', color: '#95a5a6' } ], __v: 0 } ], {})
Mongoose: projects.aggregate([ { '$project': { entries: { '$map': { input: '$entries', as: 'e', in: { someField: '$$e.someField', otherField: '$$e.otherField', tags: { '$map': { input: '$$e.tags', as: 't', in: { '$arrayElemAt': [ '$tags', { '$indexOfArray': [Array] } ] } } } } } } } } ], {})
{
  "name": "Standard $project $map",
  "results": [
    {
      "_id": "5ae5afc93e1d0d2965a4f2d7",
      "entries": [
        {
          "someField": "someData",
          "tags": [
            {
              "_id": "5b142ab7e419614016b8992d",
              "name": "Shitake",
              "color": "#95a5a6"
            }
          ]
        }
      ]
    },
    {
      "_id": "5b1b1ad07325c4c541e8a972",
      "entries": [
        {
          "otherField": "otherData",
          "tags": [
            {
              "_id": "5b142608e419614016b89925",
              "name": "Outdated",
              "color": "#3498db"
            },
            {
              "_id": "5b142ab7e419614016b8992d",
              "name": "Shitake",
              "color": "#95a5a6"
            }
          ]
        }
      ]
    }
  ]
}
Mongoose: projects.aggregate([ { '$project': { entries: { '$map': { input: '$entries', as: 'e', in: { '$mergeObjects': [ '$$e', { tags: { '$map': { input: '$$e.tags', as: 't', in: { '$arrayElemAt': [Array] } } } } ] } } } } } ], {})
{
  "name": "With $mergeObjects",
  "results": [
    {
      "_id": "5ae5afc93e1d0d2965a4f2d7",
      "entries": [
        {
          "_id": "5b159ebb0ed51064925dff24",
          "someField": "someData",
          "tags": [
            {
              "_id": "5b142ab7e419614016b8992d",
              "name": "Shitake",
              "color": "#95a5a6"
            }
          ]
        }
      ]
    },
    {
      "_id": "5b1b1ad07325c4c541e8a972",
      "entries": [
        {
          "_id": "5b1b1b267325c4c541e8a973",
          "otherField": "otherData",
          "tags": [
            {
              "_id": "5b142608e419614016b89925",
              "name": "Outdated",
              "color": "#3498db"
            },
            {
              "_id": "5b142ab7e419614016b8992d",
              "name": "Shitake",
              "color": "#95a5a6"
            }
          ]
        }
      ]
    }
  ]
}
Mongoose: projects.find({}, { fields: {} })
{
  "name": "Client re-map",
  "results": [
    {
      "_id": "5ae5afc93e1d0d2965a4f2d7",
      "__v": 0,
      "entries": [
        {
          "_id": "5b159ebb0ed51064925dff24",
          "someField": "someData",
          "tags": [
            {
              "_id": "5b142ab7e419614016b8992d",
              "name": "Shitake",
              "color": "#95a5a6"
            }
          ]
        }
      ]
    },
    {
      "_id": "5b1b1ad07325c4c541e8a972",
      "__v": 0,
      "entries": [
        {
          "_id": "5b1b1b267325c4c541e8a973",
          "otherField": "otherData",
          "tags": [
            {
              "_id": "5b142608e419614016b89925",
              "name": "Outdated",
              "color": "#3498db"
            },
            {
              "_id": "5b142ab7e419614016b8992d",
              "name": "Shitake",
              "color": "#95a5a6"
            }
          ]
        }
      ]
    }
  ]
}