【问题标题】:How to populate documents with unlimited nested levels using mongoose如何使用 mongoose 填充具有无限嵌套级别的文档
【发布时间】:2017-12-11 14:27:53
【问题描述】:

我正在设计一个 Web 应用程序,用于管理母公司和子公司的组织结构。公司有两种类型:1-主公司,2-子公司。公司可以只属于一个公司,但可以有几个子公司。我的猫鼬模式如下所示:

var companySchema = new mongoose.Schema({
    companyName: {
        type: String,
        required: true
    },
    estimatedAnnualEarnings: {
        type: Number,
        required: true
    },
    companyChildren: [{type: mongoose.Schema.Types.ObjectId, ref: 'Company'}],
    companyType: {type: String, enum: ['Main', 'Subsidiary']}
})

module.exports = mongoose.model('Company', companySchema);

我将所有公司存储在一个集合中,每个公司都有一个数组,其中包含对其子公司的引用。然后我想将所有公司显示为一棵树(在客户端)。我想查询所有填充其子代和子代填充其子代的主要公司,等等,嵌套级别不受限制。我怎样才能做到这一点?或者,也许您知道更好的方法。我还需要查看、添加、编辑、删除任何公司的能力。

现在我有了这个:

router.get('/companies', function(req, res) {
    Company.find({companyType: 'Main'}).populate({path: 'companyChildren'}).exec(function(err, list) {
        if(err) {
            console.log(err);
        } else {
            res.send(list);
        }
    })
});

但它只填充一个嵌套级别。 感谢您的帮助

【问题讨论】:

标签: node.js mongodb mongoose mongoose-schema mongoose-populate


【解决方案1】:

您可以在最新的 Mongoose 版本中执行此操作。无需插件:

const async = require('async'),
      mongoose = require('mongoose'),
      Schema = mongoose.Schema;

const uri = 'mongodb://localhost/test',
      options = { use: MongoClient };

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

function autoPopulateSubs(next) {
  this.populate('subs');
  next();
}

const companySchema = new Schema({
  name: String,
  subs: [{ type: Schema.Types.ObjectId, ref: 'Company' }]
});

companySchema
  .pre('findOne', autoPopulateSubs)
  .pre('find', autoPopulateSubs);


const Company = mongoose.model('Company', companySchema);

function log(data) {
  console.log(JSON.stringify(data, undefined, 2))
}

async.series(
  [
    (callback) => mongoose.connect(uri,options,callback),

    (callback) =>
      async.each(mongoose.models,(model,callback) =>
        model.remove({},callback),callback),

    (callback) =>
      async.waterfall(
        [5,4,3,2,1].map( name =>
          ( name === 5 ) ?
            (callback) => Company.create({ name },callback) :
            (child,callback) =>
              Company.create({ name, subs: [child] },callback)
        ),
        callback
      ),

    (callback) =>
      Company.findOne({ name: 1 })
        .exec((err,company) => {
          if (err) callback(err);
          log(company);
          callback();
        })

  ],
  (err) => {
    if (err) throw err;
    mongoose.disconnect();
  }
)

或者更现代的带有 async/await 的 Promise 版本:

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

mongoose.set('debug',true);
mongoose.Promise = global.Promise;
const uri = 'mongodb://localhost/test',
      options = { useMongoClient: true };

const companySchema = new Schema({
  name: String,
  subs: [{ type: Schema.Types.ObjectId, ref: 'Company' }]
});

function autoPopulateSubs(next) {
  this.populate('subs');
  next();
}

companySchema
  .pre('findOne', autoPopulateSubs)
  .pre('find', autoPopulateSubs);

const Company = mongoose.model('Company', companySchema);

function log(data) {
  console.log(JSON.stringify(data, undefined, 2))
}

(async function() {

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

    // Clean data
    await Promise.all(
      Object.keys(conn.models).map(m => conn.models[m].remove({}))
    );

    // Create data
    await [5,4,3,2,1].reduce((acc,name) =>
      (name === 5) ? acc.then( () => Company.create({ name }) )
        : acc.then( child => Company.create({ name, subs: [child] }) ),
      Promise.resolve()
    );

    // Fetch and populate
    let company = await Company.findOne({ name: 1 });
    log(company);

  } catch(e) {
    console.error(e);
  } finally {
    mongoose.disconnect();
  }

})()

生产:

{
  "_id": "595f7a773b80d3114d236a8b",
  "name": "1",
  "__v": 0,
  "subs": [
    {
      "_id": "595f7a773b80d3114d236a8a",
      "name": "2",
      "__v": 0,
      "subs": [
        {
          "_id": "595f7a773b80d3114d236a89",
          "name": "3",
          "__v": 0,
          "subs": [
            {
              "_id": "595f7a773b80d3114d236a88",
              "name": "4",
              "__v": 0,
              "subs": [
                {
                  "_id": "595f7a773b80d3114d236a87",
                  "name": "5",
                  "__v": 0,
                  "subs": []
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

请注意,async 部件实际上根本不需要,只是在这里设置数据以进行演示。正是 .pre() 钩子让这种情况真正发生,因为我们“链接”每个 .populate(),它实际上调用了 .find().findOne() 到另一个 .populate() 调用。

所以这个:

function autoPopulateSubs(next) {
  this.populate('subs');
  next();
}

被调用的部分是否真正在做这项工作。

使用"middleware hooks" 完成所有操作。


数据状态

为了清楚起见,这是设置的集合中的数据。它只是在普通平面文档中指向每个子公司的引用:

{
        "_id" : ObjectId("595f7a773b80d3114d236a87"),
        "name" : "5",
        "subs" : [ ],
        "__v" : 0
}
{
        "_id" : ObjectId("595f7a773b80d3114d236a88"),
        "name" : "4",
        "subs" : [
                ObjectId("595f7a773b80d3114d236a87")
        ],
        "__v" : 0
}
{
        "_id" : ObjectId("595f7a773b80d3114d236a89"),
        "name" : "3",
        "subs" : [
                ObjectId("595f7a773b80d3114d236a88")
        ],
        "__v" : 0
}
{
        "_id" : ObjectId("595f7a773b80d3114d236a8a"),
        "name" : "2",
        "subs" : [
                ObjectId("595f7a773b80d3114d236a89")
        ],
        "__v" : 0
}
{
        "_id" : ObjectId("595f7a773b80d3114d236a8b"),
        "name" : "1",
        "subs" : [
                ObjectId("595f7a773b80d3114d236a8a")
        ],
        "__v" : 0
}

【讨论】:

  • 这很有帮助,谢谢!以防其他人知道:我将您的代码复制到我的代码库中,并逐渐对其进行修改,直到它适用于我的数据 - 这揭示了出了什么问题。 autoPopulate 中间件就像一个魅力。
  • 如果我不想填充模式的每个查询怎么办?例如。在运行预挂钩之前进行一些条件检查?我找到了this,但将标志保存到数据库似乎是一种不好的做法,如 cmets 中所述。
【解决方案2】:

我认为更简单的方法是跟踪父级,因为这是唯一的,而不是跟踪可能会变得混乱的子级数组。有一个名为mongoose-tree 的漂亮模块就是为此而构建的:

var tree = require('mongoose-tree');

var CompanySchema = new mongoose.Schema({
    companyName: {
        type: String,
        required: true
    },
    estimatedAnnualEarnings: {
        type: Number,
        required: true
    },
    companyType: {type: String, enum: ['Main', 'Subsidiary']}
})

CompanySchema.plugin(tree);
module.exports = mongoose.model('Company', CompanySchema);

设置一些测试数据:

var comp1 = new CompanySchema({name:'Company 1'});
var comp2 = new CompanySchema({name:'Company 2'});
var comp3 = new CompanySchema({name:'Company 3'});

comp3.parent = comp2;
comp2.parent = comp1;

comp1.save(function() {
   comp2.save(function() {
      comp3.save();
   });
});

然后使用 mongoose-tree 构建一个可以获取祖先或孩子的函数:

router.get('/company/:name/:action', function(req, res) {
    var name = req.params.name;
    var action = req.params.action;
    Company.find({name: name}, function(err, comp){
       //typical error handling omitted for brevity
       if (action == 'ancestors'){
          comp.getAncestors(function(err, companies) {
             // companies is an array  
             res.send(companies);
          });               
       }else if (action == 'children'){
          comp.getChildren(function(err, companies) {
             res.send(companies); 
          });
       }
    });
});

【讨论】:

  • 为什么要添加插件? Mongoose 开箱即用。
  • 我真正的意思是重构模型以使用父引用而不是子数组。无论您是在 pre hook 上还是在插件上使用 populate 都无关紧要。除了 pre hook 将始终递归地填充您可能出于性能原因不想要的 subs。该插件允许您显式填充它们,这很好。它还允许您向任一方向移动。
猜你喜欢
  • 2012-04-16
  • 2020-11-04
  • 2023-03-26
  • 2012-07-05
  • 2014-10-27
  • 2018-06-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多