mongoose recursive populate

Another approach is to take advantage of the fact that Model.populate() returns a promise, and that you can fulfill a promise with another promise.

You can recursively populate the node in question via:

Node.findOne({ "_id": req.params.id }, function(err, node) {
  populateParents(node).then(function(){
    // Do something with node
  });
});

populateParents could look like the following:

var Promise = require('bluebird');

function populateParents(node) {
  return Node.populate(node, { path: "parent" }).then(function(node) {
    return node.parent ? populateParents(node.parent) : Promise.fulfill(node);
  });
}

It's not the most performant approach, but if your N is small this would work.


you can do this now (with https://www.mongodb.com/blog/post/introducing-version-40-mongoose-nodejs-odm)

var mongoose = require('mongoose');
// mongoose.Promise = require('bluebird'); // it should work with native Promise
mongoose.connect('mongodb://......');

var NodeSchema = new mongoose.Schema({
    children: [{type: mongoose.Schema.Types.ObjectId, ref: 'Node'}],
    name: String
});

var autoPopulateChildren = function(next) {
    this.populate('children');
    next();
};

NodeSchema
.pre('findOne', autoPopulateChildren)
.pre('find', autoPopulateChildren)

var Node = mongoose.model('Node', NodeSchema)
var root=new Node({name:'1'})
var header=new Node({name:'2'})
var main=new Node({name:'3'})
var foo=new Node({name:'foo'})
var bar=new Node({name:'bar'})
root.children=[header, main]
main.children=[foo, bar]

Node.remove({})
.then(Promise.all([foo, bar, header, main, root].map(p=>p.save())))
.then(_=>Node.findOne({name:'1'}))
.then(r=>console.log(r.children[1].children[0].name)) // foo

simple alternative, without Mongoose:

function upsert(coll, o){ // takes object returns ids inserted
    if (o.children){
        return Promise.all(o.children.map(i=>upsert(coll,i)))
            .then(children=>Object.assign(o, {children})) // replace the objects children by their mongo ids
            .then(o=>coll.insertOne(o))
            .then(r=>r.insertedId);
    } else {
        return coll.insertOne(o)
            .then(r=>r.insertedId);
    }
}

var root = {
    name: '1',
    children: [
        {
            name: '2'
        },
        {
            name: '3',
            children: [
                {
                    name: 'foo'
                },
                {
                    name: 'bar'
                }
            ]
        }
    ]
}
upsert(mycoll, root)


const populateChildren = (coll, _id) => // takes a collection and a document id and returns this document fully nested with its children
  coll.findOne({_id})
    .then(function(o){
      if (!o.children) return o;
      return Promise.all(o.children.map(i=>populateChildren(coll,i)))
        .then(children=>Object.assign(o, {children}))
    });


const populateParents = (coll, _id) => // takes a collection and a document id and returns this document fully nested with its parents, that's more what OP wanted
  coll.findOne({_id})
    .then(function(o){
      if (!o.parent) return o;
      return populateParents(coll, o.parent))) // o.parent should be an id
        .then(parent => Object.assign(o, {parent})) // replace that id with the document
    });

Just don't :)

There is no good way to do that. Even if you do some map-reduce, it will have terrible performance and problems with sharding if you have it or will ever need it.

Mongo as NoSQL database is really great for storing tree documents. You can store whole tree and then use map-reduce to get some particular leafs from it if you don't have a lot of "find particular leaf" queries. If this doesn't work for you, go with two collections:

  1. Simplified tree structure: {_id: "tree1", tree: {1: [2, {3: [4, {5: 6}, 7]}]}}. Numbers are just IDs of nodes. This way you'll get whole document in one query. Then you just extract all ids and run second query.

  2. Nodes: {_id: 1, data: "something"}, {_id: 2, data: "something else"}.

Then you can write simple recurring function which will replace node ids from first collection with data from second. 2 queries and simple client-side processing.

Small update:

You can extend second collection to be a little more flexible:

{_id: 2, data: "something", children:[3, 7], parents: [1, 12, 13]}

This way you'll be able to start your search from any leaf. And then, use map-reduce to get to the top or to the bottom of this part of tree.


Now with Mongoose 4 this can be done. Now you can recurse deeper than a single level.

Example

User.findOne({ userId: userId })
    .populate({ 
        path: 'enrollments.course',
        populate: {
            path: 'playlists',
            model: 'Playlist',
            populate: {
                path: 'videos',
                model: 'Video'
            }
        } 
    })
    .populate('degrees')
    .exec()

You can find the official documentation for Mongoose Deep Populate from here.