Mongoose递归填充

21

我已经搜索了一段时间,但没有找到任何好的答案。我有一个存储在数据库中的 n层深度 的树,我想要填充所有父节点,以便最终获得完整的树。

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个回答

26

现在您可以使用Mongoose Node.js ODM 4.0版本实现此操作。

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
这个简直像魔法一样,我一直在寻找这个解决方案。谢谢你。 - Ninja Coding
你应该因此获得奖励。 - Olubodun Agbalaya

13

另一种方法是利用 Model.populate() 返回一个 promise 的事实,并且您可以使用另一个 promise 来履行它。

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

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很小,这将起作用。


11
现在有了 Mongoose 4,这可以做到。现在你可以递归深入多个级别。 例子
User.findOne({ userId: userId })
    .populate({ 
        path: 'enrollments.course',
        populate: {
            path: 'playlists',
            model: 'Playlist',
            populate: {
                path: 'videos',
                model: 'Video'
            }
        } 
    })
    .populate('degrees')
    .exec()

您可以在此处找到Mongoose Deep Populate的官方文档


非常感谢,你救了我的一天。 你可以告诉我在哪里找到相关的参考资料吗? 谢谢。 - Nomura Nori
Mongoose深度填充 http://mongoosejs.com/docs/populate.html#deep-populate 但是文档在我看来不够全面 :) - Shanika Ediriweera
这真是救命稻草。非常感谢。 - Waweru Kamau
不是对递归人口的答案 - undefined

3

这是对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);
});

3

千万不要这样做 :)

没有好的方法来处理这个问题。即使你使用一些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"}

然后你可以编写一个简单的递归函数,用第二个集合中的数据替换第一个集合中的节点ID。2个查询和简单的客户端处理。

小更新:

你可以扩展第二个集合,使其更加灵活:

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

这样你就可以从任何叶子节点开始搜索。然后使用map-reduce到达树的顶部或底部的这一部分。


谢谢。这不完全是我要找的,但还是谢谢你。我会考虑一下的。 - Michal Majernik
Mongoose深度填充是更好的解决方案。http://mongoosejs.com/docs/populate.html#deep-populate 您可以在我的答案下找到一个示例。 - Shanika Ediriweera

0

也许已经晚了很多,但mongoose有一些关于此的文档:

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

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

您将使用此模式设置文档:

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)


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 {
                ...
            }
        })

注意,这并不是非常高效的。如果您需要经常运行此类查询或在深层级别上运行,则应重新考虑您的设计。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接