如何使用 mongoose 填充具有无限嵌套级别的文档
Posted
技术标签:
【中文标题】如何使用 mongoose 填充具有无限嵌套级别的文档【英文标题】:How to populate documents with unlimited nested levels using 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);
)
);
但它只填充一个嵌套级别。 感谢您的帮助
【问题讨论】:
有史以来使用 bluebird 的最佳解决方案:traverse a tree in mongoDB 【参考方案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。该插件允许您显式填充它们,这很好。它还允许您向任一方向移动。以上是关于如何使用 mongoose 填充具有无限嵌套级别的文档的主要内容,如果未能解决你的问题,请参考以下文章