Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to populate documents with unlimited nested levels using mongoose

I'm designing a web application that manages organizational structure for parent and child companies. There are two types of companies: 1- Main company, 2 -Subsidiary company.The company can belong only to one company but can have a few child companies. My mongoose Schema looks like this:

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);

I store all my companies in one collection and each company has an array with references to its child companies. Then I want to display all companies as a tree(on client side). I want query all Main companies that populates their children and children populate their children and so on,with unlimited nesting level. How can I do that? Or maybe you know better approach. Also I need ability to view,add,edit,delete any company.

Now I have this:

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);
        }
    })
});

But it populates only one nested level. I appreciate any help

like image 730
Олег Павлюк Avatar asked Jul 07 '17 10:07

Олег Павлюк


People also ask

How does populate work in Mongoose?

Mongoose Populate() Method. In MongoDB, Population is the process of replacing the specified path in the document of one collection with the actual document from the other collection.

What is _v in Mongoose?

In Mongoose the “_v” field is the versionKey is a property set on each document when first created by Mongoose. This is a document inserted through the mongo shell in a collection and this key-value contains the internal revision of the document.24-Jun-2021.

What does .save do Mongoose?

Mongoose | save() Function The save() function is used to save the document to the database. Using this function, new documents can be added to the database.

What is the difference between schema and model in Mongoose?

A Mongoose schema defines the structure of the document, default values, validators, etc., whereas a Mongoose model provides an interface to the database for creating, querying, updating, deleting records, etc.


1 Answers

You can do this in latest Mongoose releases. No plugins required:

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();
  }
)

Or a more modern Promise version with async/await:

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();
  }

})()

Produces:

{
  "_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": []
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Note that the async parts are not actually required at all and are just here for setting up the data for demonstration. It's the .pre() hooks that allow this to actually happen as we "chain" each .populate() which actually calls either .find() or .findOne() under the hood to another .populate() call.

So this:

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

Is the part being invoked that is actually doing the work.

All done with "middleware hooks".


Data State

To make it clear, this is the data in the collection which is set up. It's just references pointing to each subsidiary in plain flat documents:

{
        "_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
}
like image 186
Neil Lunn Avatar answered Sep 19 '22 01:09

Neil Lunn