在数组中填充对象

Posted

技术标签:

【中文标题】在数组中填充对象【英文标题】:Populate Object In an Array 【发布时间】:2018-11-14 08:40:00 【问题描述】:

在填充我的用户时遇到问题。 案例:

var User = new mongoose.Schema(
name: 
  type: String,
  lowercase: true,
  unique: true
,
portfolio:[
  
      name: String,
      formatType:  type: mongoose.Schema.Types.ObjectId, ref: 'FormatType' ,
  
]
);

这是我的 Mongoose 命令:

User.findById(req.payload.id)
    .populate(
        path:'portfolio',
        populate:
            path: 'formatType',
            model: 'FormatType'
        
    )
    .then(user =>  ... 

所以我们这里有一个模型 - 在 Obect 内部 - 在数组内部 - 在实体内部。

网上找不到答案,万分感谢~!

【问题讨论】:

您认为提供的答案中是否有某些内容无法解决您的问题?如果是这样,请对答案发表评论,以澄清究竟需要解决哪些尚未解决的问题。如果它确实回答了您提出的问题,请注意Accept your Answers您提出的问题 【参考方案1】:

您在这里基本上错过的是您想要populate() 的字段的“路径”实际上是'portfolio.formatType',而不仅仅是您键入的'portfolio'。由于这个错误和结构,您可能会有一些普遍的误解。

填充校正

基本修正只需要正确的路径,您不需要model 参数,因为这已经隐含在架构中:

User.findById(req.params.id).populate('portfolio.formatType');

然而,在数组中“混合”“嵌入”数据和“引用”数据通常不是一个好主意,您实际上应该嵌入所有内容或简单地引用所有内容。如果您的意图是引用,那么通常在文档中保留一系列引用也是一种“反模式”,因为您的理由不应该是导致文档增长超过 16MB BSON 限制。在您的数据永远不会达到该限制的情况下,通常最好“完全嵌入”。这确实是一个更广泛的讨论,但您应该注意一些事情。

这里的下一个要点是populate() 本身有点“旧帽子”,实际上并不是大多数新用户认为的“神奇”事物。要明确populate()NOT A JOIN,它所做的只是向服务器执行另一个查询以返回“相关”项目,然后将该内容合并到从上一个查询。

$lookup 替代方案

如果您正在寻找“连接”,那么实际上您可能想要如前所述的“嵌入”。这实际上是处理“关系”但将所有“相关”数据放在一个文档中的“MongoDB 方式”。数据位于不同集合中的另一种“连接”方式是通过现代版本中的$lookup 运算符。

由于您的“混合”内容数组形式,这变得有点复杂,但通常可以表示为:

// Aggregation pipeline don't "autocast" from schema
const  Types:  ObjectId   = require("mongoose");

User.aggregate([
   "$match":  _id: ObjectId(req.params.id)   ,
   "$lookup": 
    "from": FormatType.collection.name,
    "localField": "portfolio.formatType",
    "foreignField": "_id",
    "as": "formats"
  ,
   "$project": 
    "name": 1,
    "portfolio": 
      "$map": 
        "input": "$portfolio",
        "in": 
          "name": "$$this.name",
          "formatType": 
            "$arrayElemAt": [
              "$formats",
               "$indexOfArray": [ "$formats._id", "$$this.formatType" ] 
            ]
          
        
      
    
  
]);

或者自 MongoDB 3.6 起使用更具表现力的 $lookup 形式:

User.aggregate([
   "$match":  _id: ObjectId(req.params.id)   ,
   "$lookup": 
    "from": FormatType.collection.name,
    "let":  "portfolio": "$portfolio" ,
    "as": "portfolio",
    "pipeline": [
       "$match": 
        "$expr": 
          "$in": [ "$_id", "$$portfolio.formatType" ]
        
      ,
       "$project": 
        "_id": 
          "$arrayElemAt": [
            "$$portfolio._id",
             "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] 
          ]
        ,
        "name": 
          "$arrayElemAt": [
            "$$portfolio.name",
             "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] 
          ]
        ,
        "formatType": "$$ROOT",
      
    ]
  
]);

这两种方法的工作方式略有不同,但基本上都使用返回匹配的“相关”条目然后“重新映射”到现有数组内容的概念,以便与“嵌入”"name" 属性合并阵内。这实际上是主要的并发症,否则是一种相当简单的检索方法。

这与populate() 在“客户端”上实际执行的过程几乎相同,但在“服务器”上执行。因此,比较使用$indexOfArray 运算符来查找匹配的ObjectId 值在哪里,然后通过$arrayElemAt 操作从匹配的“索引”处的数组中返回一个属性。

唯一的区别是,在 MongoDB 3.6 兼容版本中,我们在“外来”内容中进行“替换”“之前”将连接的结果返回到父母。在以前的版本中,我们返回整个匹配的外部数组,然后使用 $map 将两者“结合”以形成一个单一的“合并”数组。

虽然这些最初看起来“更复杂”,但这里的最大优势是它们构成了对服务器的“单个请求”“单一响应”,而不是像 populate() 那样发出和接收“多个”请求。这实际上节省了大量的网络流量开销,并大大增加了响应时间。

此外,这些是“真正的连接”,因此您可以做的更多事情是“多个查询”无法实现的。例如,您可以对“加入”的结果进行“排序”,并且只返回排名靠前的结果,而使用 populate() 需要拉入“所有父母”,然后才能查找要返回结果的“孩子”。对子“加入”的“过滤”条件也是如此。

Querying after populate in Mongoose 上对此有更多详细信息,关于一般限制以及您实际上可以做什么来“自动化”在需要时生成此类“复杂”聚合管道语句。

演示

执行这些“连接”和理解引用模式的另一个常见问题是,人们经常在存储引用的位置和时间以及它是如何工作的方面出现错误的概念。因此,以下清单可作为此类数据的存储和检索的演示。

在旧版 NodeJS 的原生 Promises 实现中:

const  Schema  = mongoose = require('mongoose');

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

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

const formatTypeSchema = new Schema(
  name: String
);

const portfolioschema = new Schema(
  name: String,
  formatType:  type: Schema.Types.ObjectId, ref: 'FormatType' 
);

const userSchema = new Schema(
  name: String,
  portfolio: [portfolioSchema]
);

const FormatType = mongoose.model('FormatType', formatTypeSchema);
const User = mongoose.model('User', userSchema);

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

(function() 

  mongoose.connect(uri).then(conn => 

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

    return db.command( buildInfo: 1 ).then(( version ) => 
      version = parseFloat(version.match(new RegExp(/(?:(?!-).)*/))[0]);

      return Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()))
        .then(() => FormatType.insertMany(
          [ 'A', 'B', 'C' ].map(name => ( name ))
        )
        .then(([A, B, C]) => User.insertMany(
          [
            
              name: 'User 1',
              portfolio: [
                 name: 'Port A', formatType: A ,
                 name: 'Port B', formatType: B 
              ]
            ,
            
              name: 'User 2',
              portfolio: [
                 name: 'Port C', formatType: C 
              ]
            
          ]
        ))
        .then(() => User.find())
        .then(users => log( users ))
        .then(() => User.findOne( name: 'User 1' )
          .populate('portfolio.formatType')
        )
        .then(user1 => log( user1 ))
        .then(() => User.aggregate([
           "$match":  "name": "User 2"  ,
           "$lookup": 
            "from": FormatType.collection.name,
            "localField": "portfolio.formatType",
            "foreignField": "_id",
            "as": "formats"
          ,
           "$project": 
            "name": 1,
            "portfolio": 
              "$map": 
                "input": "$portfolio",
                "in": 
                  "name": "$$this.name",
                  "formatType": 
                    "$arrayElemAt": [
                      "$formats",
                       "$indexOfArray": [ "$formats._id", "$$this.formatType" ] 
                    ]
                  
                
              
            
          
        ]))
        .then(user2 => log( user2 ))
        .then(() =>
          ( version >= 3.6 ) ?
            User.aggregate([
               "$lookup": 
                "from": FormatType.collection.name,
                "let":  "portfolio": "$portfolio" ,
                "as": "portfolio",
                "pipeline": [
                   "$match": 
                    "$expr": 
                      "$in": [ "$_id", "$$portfolio.formatType" ]
                    
                  ,
                   "$project": 
                    "_id": 
                      "$arrayElemAt": [
                        "$$portfolio._id",
                         "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] 
                      ]
                    ,
                    "name": 
                      "$arrayElemAt": [
                        "$$portfolio.name",
                         "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] 
                      ]
                    ,
                    "formatType": "$$ROOT",
                  
                ]
              
            ]).then(users => log( users )) : ''
        );
  )
  .catch(e => console.error(e))
  .then(() => mongoose.disconnect());

)()

对于较新的 NodeJS 版本(包括当前的 LTS v.8.x 系列)使用 async/await 语法:

const  Schema  = mongoose = require('mongoose');

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

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

const formatTypeSchema = new Schema(
  name: String
);

const portfolioSchema = new Schema(
  name: String,
  formatType:  type: Schema.Types.ObjectId, ref: 'FormatType' 
);

const userSchema = new Schema(
  name: String,
  portfolio: [portfolioSchema]
);

const FormatType = mongoose.model('FormatType', formatTypeSchema);
const User = mongoose.model('User', userSchema);

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]);
    log(version);

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

    // Insert some things
    let [ A, B, C ] = await FormatType.insertMany(
      [ 'A', 'B', 'C' ].map(name => ( name ))
    );

    await User.insertMany(
      [
        
          name: 'User 1',
          portfolio: [
             name: 'Port A', formatType: A ,
             name: 'Port B', formatType: B 
          ]
        ,
        
          name: 'User 2',
          portfolio: [
             name: 'Port C', formatType: C 
          ]
        
      ]
    );


    // Show plain users
    let users = await User.find();
    log( users );

    // Get user with populate

    let user1 = await User.findOne( name: 'User 1' )
      .populate('portfolio.formatType');

    log( user1 );

    // Get user with $lookup
    let user2 = await User.aggregate([
       "$match":  "name": "User 2"  ,
       "$lookup": 
        "from": FormatType.collection.name,
        "localField": "portfolio.formatType",
        "foreignField": "_id",
        "as": "formats"
      ,
       "$project": 
        "name": 1,
        "portfolio": 
          "$map": 
            "input": "$portfolio",
            "in": 
              "name": "$$this.name",
              "formatType": 
                "$arrayElemAt": [
                  "$formats",
                   "$indexOfArray": [ "$formats._id", "$$this.formatType" ] 
                ]
              
            
          
        
      
    ]);

    log( user2 );

    // Expressive $lookup
    if ( version >= 3.6 ) 
      let users = await User.aggregate([
         "$lookup": 
          "from": FormatType.collection.name,
          "let":  "portfolio": "$portfolio" ,
          "as": "portfolio",
          "pipeline": [
             "$match": 
              "$expr": 
                "$in": [ "$_id", "$$portfolio.formatType" ]
              
            ,
             "$project": 
              "_id": 
                "$arrayElemAt": [
                  "$$portfolio._id",
                   "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] 
                ]
              ,
              "name": 
                "$arrayElemAt": [
                  "$$portfolio.name",
                   "$indexOfArray": [ "$$portfolio.formatType", "$_id" ] 
                ]
              ,
              "formatType": "$$ROOT",
            
          ]
        
      ]);
      log( users )
    

    mongoose.disconnect();    
   catch(e) 
    console.log(e)
   finally 
    process.exit()
  

)()

如果对每个阶段进行评论以解释各个部分,则后一个列表,您至少可以通过比较看到两种语法形式是如何相互关联的。

请注意,“表达性”$lookup 示例仅在连接到的 MongoDB 服务器实际支持语法的情况下运行。

还有那些懒得自己运行代码的人的“输出”:

Mongoose: formattypes.remove(, )
Mongoose: users.remove(, )
Mongoose: formattypes.insertMany([  _id: 5b1601d8be9bf225554783f5, name: 'A', __v: 0 ,  _id: 5b1601d8be9bf225554783f6, name: 'B', __v: 0 ,  _id: 5b1601d8be9bf225554783f7, name: 'C', __v: 0  ], )
Mongoose: users.insertMany([  _id: 5b1601d8be9bf225554783f8, name: 'User 1', portfolio: [  _id: 5b1601d8be9bf225554783fa, name: 'Port A', formatType: 5b1601d8be9bf225554783f5 ,  _id: 5b1601d8be9bf225554783f9, name: 'Port B', formatType: 5b1601d8be9bf225554783f6  ], __v: 0 ,  _id: 5b1601d8be9bf225554783fb, name: 'User 2', portfolio: [  _id: 5b1601d8be9bf225554783fc, name: 'Port C', formatType: 5b1601d8be9bf225554783f7  ], __v: 0  ], )
Mongoose: users.find(,  fields:  )

  "users": [
    
      "_id": "5b1601d8be9bf225554783f8",
      "name": "User 1",
      "portfolio": [
        
          "_id": "5b1601d8be9bf225554783fa",
          "name": "Port A",
          "formatType": "5b1601d8be9bf225554783f5"
        ,
        
          "_id": "5b1601d8be9bf225554783f9",
          "name": "Port B",
          "formatType": "5b1601d8be9bf225554783f6"
        
      ],
      "__v": 0
    ,
    
      "_id": "5b1601d8be9bf225554783fb",
      "name": "User 2",
      "portfolio": [
        
          "_id": "5b1601d8be9bf225554783fc",
          "name": "Port C",
          "formatType": "5b1601d8be9bf225554783f7"
        
      ],
      "__v": 0
    
  ]

Mongoose: users.findOne( name: 'User 1' ,  fields:  )
Mongoose: formattypes.find( _id:  '$in': [ ObjectId("5b1601d8be9bf225554783f5"), ObjectId("5b1601d8be9bf225554783f6") ]  ,  fields:  )

  "user1": 
    "_id": "5b1601d8be9bf225554783f8",
    "name": "User 1",
    "portfolio": [
      
        "_id": "5b1601d8be9bf225554783fa",
        "name": "Port A",
        "formatType": 
          "_id": "5b1601d8be9bf225554783f5",
          "name": "A",
          "__v": 0
        
      ,
      
        "_id": "5b1601d8be9bf225554783f9",
        "name": "Port B",
        "formatType": 
          "_id": "5b1601d8be9bf225554783f6",
          "name": "B",
          "__v": 0
        
      
    ],
    "__v": 0
  

Mongoose: users.aggregate([  '$match':  name: 'User 2'  ,  '$lookup':  from: 'formattypes', localField: 'portfolio.formatType', foreignField: '_id', as: 'formats'  ,  '$project':  name: 1, portfolio:  '$map':  input: '$portfolio', in:  name: '$$this.name', formatType:  '$arrayElemAt': [ '$formats',  '$indexOfArray': [ '$formats._id', '$$this.formatType' ]  ]       ], )

  "user2": [
    
      "_id": "5b1601d8be9bf225554783fb",
      "name": "User 2",
      "portfolio": [
        
          "name": "Port C",
          "formatType": 
            "_id": "5b1601d8be9bf225554783f7",
            "name": "C",
            "__v": 0
          
        
      ]
    
  ]

Mongoose: users.aggregate([  '$lookup':  from: 'formattypes', let:  portfolio: '$portfolio' , as: 'portfolio', pipeline: [  '$match':  '$expr':  '$in': [ '$_id', '$$portfolio.formatType' ]   ,  '$project':  _id:  '$arrayElemAt': [ '$$portfolio._id',  '$indexOfArray': [ '$$portfolio.formatType', '$_id' ]  ] , name:  '$arrayElemAt': [ '$$portfolio.name',  '$indexOfArray': [ '$$portfolio.formatType', '$_id' ]  ] , formatType: '$$ROOT'   ]   ], )

  "users": [
    
      "_id": "5b1601d8be9bf225554783f8",
      "name": "User 1",
      "portfolio": [
        
          "_id": "5b1601d8be9bf225554783fa",
          "name": "Port A",
          "formatType": 
            "_id": "5b1601d8be9bf225554783f5",
            "name": "A",
            "__v": 0
          
        ,
        
          "_id": "5b1601d8be9bf225554783f9",
          "name": "Port B",
          "formatType": 
            "_id": "5b1601d8be9bf225554783f6",
            "name": "B",
            "__v": 0
          
        
      ],
      "__v": 0
    ,
    
      "_id": "5b1601d8be9bf225554783fb",
      "name": "User 2",
      "portfolio": [
        
          "_id": "5b1601d8be9bf225554783fc",
          "name": "Port C",
          "formatType": 
            "_id": "5b1601d8be9bf225554783f7",
            "name": "C",
            "__v": 0
          
        
      ],
      "__v": 0
    
  ]

【讨论】:

非常感谢您在这个答案上花费的时间,您给了我很多思考和研究。 遗憾的是 User.findById(req.params.id).populate('portfolio.formatType'); 没用,不过现在没关系 - 将改变逻辑 @Alex 我给了你一个“演示中的完整示例”,这样你就可以在自己的代码中实际看到你做错了什么。这是一个如此长的示例的原因是因为您显然没有理解几件事,而且您的数据本身很可能甚至不正确。这就是为什么你可以审视整个事情,比较和学习,然后纠正自己的错误。

以上是关于在数组中填充对象的主要内容,如果未能解决你的问题,请参考以下文章

如何在异步调用期间填充数组并将其发送到响应对象中

如何仅在猫鼬中使用聚合填充嵌套在对象数组中的字段?

Mongoose:在数组中填充对象的问题

表未从对象数组中正确填充

在托管代码中填充非托管数组

用数据库中的不同对象填充数组