mongoose递归填充

时间:2014-09-25 14:33:31

标签: node.js mongodb mongoose

我一直在寻找,但我找不到任何好的答案。我有 n-deep 树,我存储在数据库中,我想填充所有父母,所以最后我得到完整的树

node
 -parent
  -parent
    .
    .
    -parent

到目前为止,我填充到第2级,正如我所提到的,我需要达到 n 的水平。

Node.find().populate('parent').exec(function (err, items) {
   if (!err) {
     Node.populate(items, {path: 'parent.parent'}, function (err, data) {
       return res.send(data);
     });
   } else {
     res.statusCode = code;
     return res.send(err.message);
   }
 });

7 个答案:

答案 0 :(得分:17)

您现在可以执行此操作(使用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

简单的替代方案,没有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
    });

答案 1 :(得分:11)

另一种方法是利用Model.populate()返回承诺的事实,并且你可以履行另一个承诺的承诺。

您可以通过以下方式递归填充相关节点:

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

populateParents可能如下所示:

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

这不是最高效的方法,但如果你的N很小,那就行了。

答案 2 :(得分:4)

现在Mongoose 4可以做到这一点。现在你可以进行更深层次的递归。

示例

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

您可以从here找到Mongoose Deep Populate的官方文档

答案 3 :(得分:2)

请不要:)

没有好方法可以做到这一点。即使你做了一些map-reduce,它也会有糟糕的性能和分片问题,如果你有它或者需要它。

Mongo作为NoSQL数据库非常适合存储树文档。您可以存储整个树,然后使用map-reduce从中获取一些特定的叶子,如果您没有很多“查找特定叶子”查询。如果这对您不起作用,请使用两个集合:

  1. 简化树结构:{_id: "tree1", tree: {1: [2, {3: [4, {5: 6}, 7]}]}}。数字只是节点的ID。这样,您将在一个查询中获得整个文档。然后,您只需提取所有ID并运行第二个查询。

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

  3. 然后你可以编写简单的循环函数,它将第一个集合中的节点ID替换为第二个集合中的数据。 2个查询和简单的客户端处理。

    小更新:

    您可以将第二个集合扩展为更灵活:

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

    通过这种方式,您可以从任何叶子开始搜索。然后,使用map-reduce来到树的这一部分的顶部或底部。

答案 4 :(得分:0)

我尝试了@ fzembow的解决方案,但它似乎从最深的填充路径返回对象。在我的情况下,我需要递归填充对象,但然后返回相同的对象。我这样做了:

// Schema definition
const NodeSchema = new Schema({
        name: { type: String, unique: true, required: true },
        parent: { type: Schema.Types.ObjectId, ref: 'Node' },
    });

const Node =  mongoose.model('Node', NodeSchema);





// method
const Promise = require('bluebird');

const recursivelyPopulatePath = (entry, path) => {
    if (entry[path]) {
        return Node.findById(entry[path])
            .then((foundPath) => {
                return recursivelyPopulatePath(foundPath, path)
                    .then((populatedFoundPath) => {
                        entry[path] = populatedFoundPath;
                        return Promise.resolve(entry);
                    });
            });
    }
    return Promise.resolve(entry);
};


//sample usage
Node.findOne({ name: 'someName' })
        .then((category) => {
            if (category) {
                recursivelyPopulatePath(category, 'parent')
                    .then((populatedNode) => {
                        // ^^^^^^^^^^^^^^^^^ here is your object but populated recursively
                    });
            } else {
                ...
            }
        })

注意它效率不高。如果您需要经常或深层次地运行此类查询,那么您应该重新考虑您的设计

答案 5 :(得分:0)

这是解决caub的问题和解决方案的更直接的方法。一开始我觉得很难理解,所以我将这个版本放在一起。

重要的是,您需要同时使用“ findOne”和“ find”中间件挂钩才能使该解决方案正常工作。 *

*另外,模型定义必须位于中间件定义之后*

const mongoose = require('mongoose');

const NodeSchema = new mongoose.Schema({
    children: [mongoose.Schema.Types.ObjectId],
    name: String
});

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

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


const Node = mongoose.model('Node', NodeSchema)

const root = new Node({ name: '1' })
const main = new Node({ name: '3' })
const foo = new Node({ name: 'foo' })

root.children = [main]
main.children = [foo]


mongoose.connect('mongodb://localhost:27017/try', { useNewUrlParser: true }, async () => {
    await Node.remove({});

    await foo.save();
    await main.save();
    await root.save();

    const result = await Node.findOne({ name: '1' });

    console.log(result.children[0].children[0].name);
});

答案 6 :(得分:0)

也许晚了很多,但猫鼬对此有一些文档:

我认为第一个更适合你,因为你正在寻找填充父母。

使用该解决方案,您可以使用一个正则表达式查询,搜索与您设计的输出树匹配的所有文档。

您将使用此架构设置文档:

Tree: {
 name: String,
 path: String
}

Paths 字段将是树中的绝对路径:

/mens
/mens/shoes
/mens/shoes/boots
/womens
/womens/shoes
/womens/shoes/boots

例如,您可以使用一个查询搜索节点“/mens/shoes”的所有子节点:

await Tree.find({ path: /^\/mens/shoes })

它将返回路径以 /mens/shoes 开头的所有文档:

/mens/shoes
/mens/shoes/boots

然后你只需要一些客户端逻辑来安排它在树结构中(一个map-reduce)