我有多个系列,我使用了单独的系列&外键方法,我想加入这个集合来构建嵌套集合。 这是我的收藏模式:
const SurveySchema = new Schema({
_id:{ type: Schema.ObjectId, auto: true },
name: String,
enabled: {type: Boolean, Default: true},
created_date:{type: Date, Default: Date.now},
company: {type: Schema.Types.ObjectId, ref: 'Company'},});
const GroupSchema = new Schema({
_id:{ type: Schema.ObjectId, auto: true },
name: String,
order: String,
created_date:{type: Date, Default: Date.now},
questions: [{type: Schema.Types.ObjectId, ref: 'Question'}],
survey: {type: Schema.Types.ObjectId, ref: 'Survey'}
});
const ResponseSchema = new Schema({
_id:{ type: Schema.ObjectId, auto: true },
response_text: String,
order: String,
created_date:{type: Date, Default: Date.now},
question:{type: Schema.Types.ObjectId, ref: 'Question'}
});
这是构建这个嵌套对象的代码:
Survey.aggregate([
{ $match: {} },
{ $lookup: {
from: 'groups',
localField: '_id',
foreignField: 'survey',
as: 'groupsofquestions',
}},
{ $unwind: {
path: "$groupsofquestions",
preserveNullAndEmptyArrays: true
}},
{ $lookup: {
from: 'questions',
localField: 'groupsofquestions._id',
foreignField: 'group',
as: 'questionsofgroup',
}},
{ $lookup: {
from: 'response',
localField: 'questionsofgroup._id',
foreignField: 'question',
as: 'responses',
}},
{ $group: {
_id: "$_id",
name: {$first: "$name"},
groups: {$push: {
id: "$groupsofquestions._id",
name: "$groupsofquestions.name",
questions: "$questionsofgroup",
reponses: "$responses"
}}
}}
])
我想按如下结构(也使用外部链接):
http://jsoneditoronline.org/?id=d7d1779b3b95e3acb28f8a2be0785423
[
{
"__v": 0,
"_id": "59b6715725dcd2060da7f591",
"company": "59b6715725dcd2060da7f58f",
"created_date": "2017-09-11T11:19:51.709Z",
"enabled": true,
"name": "function String() { [native code] }",
"groups": [
{
"_id": "59b6715725dcd2060da7f592",
"name": "groupe 1 des question",
"order": "1",
"created_date": "2017-09-11T11:19:51.709Z",
"survey": "59b6715725dcd2060da7f591",
"__v": 0,
"questions": [
{
"_id": "59b6715725dcd2060da7f594",
"question_text": "question 1 group 1",
"order": "1",
"created_date": "2017-09-11T11:19:51.709Z",
"group": "59b6715725dcd2060da7f592",
"__v": 0,
"responses": [
{
"_id": "59b6715725dcd2060da7f598",
"response_text": "reponse 1 question 1 group 1",
"order": "1",
"created_date": "2017-09-11T11:19:51.710Z",
"question": "59b6715725dcd2060da7f594",
"__v": 0
},
{
"_id": "59b6715725dcd2060da7f599",
"response_text": "reponse 2 question 1 group 1",
"order": "2",
"created_date": "2017-09-11T11:19:51.710Z",
"question": "59b6715725dcd2060da7f594",
"__v": 0
}
]
},
{
"_id": "59b6715725dcd2060da7f595",
"question_text": "question 2 group 1",
"order": "2",
"created_date": "2017-09-11T11:19:51.710Z",
"group": "59b6715725dcd2060da7f592",
"__v": 0,
"responses": [
{
"_id": "59b6715725dcd2060da7f59a",
"response_text": "reponse 1 question 2 group 1",
"order": "1",
"created_date": "2017-09-11T11:19:51.710Z",
"question": "59b6715725dcd2060da7f595",
"__v": 0
},
{
"_id": "59b6715725dcd2060da7f59b",
"response_text": "reponse 2 question 2 group 1",
"order": "2",
"created_date": "2017-09-11T11:19:51.710Z",
"question": "59b6715725dcd2060da7f595",
"__v": 0
}
]
}
]
},
{
"_id": "59b6715725dcd2060da7f593",
"name": "groupe 2 des question",
"order": "2",
"created_date": "2017-09-11T11:19:51.709Z",
"survey": "59b6715725dcd2060da7f591",
"__v": 0,
"questions": [
{
"_id": "59b6715725dcd2060da7f596",
"question_text": "question 1 group 1",
"order": "1",
"created_date": "2017-09-11T11:19:51.710Z",
"group": "59b6715725dcd2060da7f592",
"__v": 0,
"responses": [
{
"_id": "59b6715725dcd2060da7f59c",
"response_text": "reponse 1 question 1 group 2",
"order": "1",
"created_date": "2017-09-11T11:19:51.710Z",
"question": "59b6715725dcd2060da7f596",
"__v": 0
},
{
"_id": "59b6715725dcd2060da7f59d",
"response_text": "reponse 2 question 1 group 2",
"order": "2",
"created_date": "2017-09-11T11:19:51.710Z",
"question": "59b6715725dcd2060da7f596",
"__v": 0
}
]
},
{
"_id": "59b6715725dcd2060da7f597",
"question_text": "question 2 group 1",
"order": "2",
"created_date": "2017-09-11T11:19:51.710Z",
"group": "59b6715725dcd2060da7f592",
"__v": 0,
"responses": [
{
"_id": "59b6715725dcd2060da7f59e",
"response_text": "reponse 1 question 2 group 2",
"order": "1",
"created_date": "2017-09-11T11:19:51.710Z",
"question": "59b6715725dcd2060da7f597",
"__v": 0
},
{
"_id": "59b6715725dcd2060da7f59f",
"response_text": "reponse 2 question 2 group 2",
"order": "2",
"created_date": "2017-09-11T11:19:51.710Z",
"question": "59b6715725dcd2060da7f597",
"__v": 0
}
]
}
]
}
]
}
]
有人可以帮我构建响应,如示例所示吗?
答案 0 :(得分:0)
大多数情况下,您需要在使用$group
处理后使用$unwind
来“重建”,以便再次嵌套数组输出。还有一些提示:
Survey.aggregate([
{ "$lookup": {
"from": Group.collection.name,
"localField": "_id",
"foreignField": "survey",
"as": "groups"
}},
{ "$unwind": "$groups" },
{ "$lookup": {
"from": Question.collection.name,
"localField": "groups.questions",
"foreignField": "_id",
"as": "groups.questions"
}},
{ "$unwind": "$groups.questions" },
{ "$lookup": {
"from": Response.collection.name,
"localField": "groups.questions._id",
"foreignField": "question",
"as": "groups.questions.responses"
}},
{ "$group": {
"_id": {
"_id": "$_id",
"company": "$company",
"created_date": "$created_date",
"enabled": "$enabled",
"name": "$name",
"groups": {
"_id": "$groups._id",
"name": "$groups.name",
"order": "$groups.order",
"created_date": "$groups.created_date",
"survey": "$groups.survey"
}
},
"questions": { "$push": "$groups.questions" }
}},
{ "$sort": { "_id": 1 } },
{ "$group": {
"_id": "$_id._id",
"company": { "$first": "$_id.company" },
"created_date": { "$first": "$_id.created_date" },
"enabled": { "$first": "$_id.enabled" },
"name": { "$first": "$_id.name" },
"groups": {
"$push": {
"_id": "$_id.groups._id",
"name": "$_id.groups.name",
"order": "$_id.groups.order",
"created_date": "$_id.groups.created_date",
"survey": "$_id.groups.survey",
"questions": "$questions"
}
}
}},
{ "$sort": { "_id": 1 } }
]);
这就是重建阵列的方法,你可以一步一步地完成,而不是一次性完成所有这些。它可能是最难理解的概念,但“管道”意味着你实际上可以“多次”做事,将一个动作链接到另一个的输出。
所以第一个$group
是在“群组”详细级别完成的,因为你想要"questions"
数组的$push
个项目,这是"responses"
数组的最后一个“解构” 3}}。请注意,_id
仍然是最后$unwind
阶段结果的数组。但除了数组内容之外,其他所有内容都在Survey
“分组键”。
在“第二个”$lookup
上,您实际使用$group
之类的运算符来构建"groups"
级别的特定字段属性。 _id
数组再次使用$first
构造,因此前一阶段“分组键”中的每个属性都以Group.collection.name
为前缀,因此这里引用它们的方式
此外,作为技术立场,如果您有预期的订单,则在每次调用$push
后应始终$sort
。分组键上的集合不能以任何特定顺序保证(尽管通常是反向堆栈顺序)。如果您需要订单,请指定订单,特别是在应用$group
重建$push
后的数组时。
原始$group
之前没有$sort
的原因是因为前面的管道阶段实际上对现有订单没有任何影响。因此始终保留发现的顺序。
一些提示:
像preserveNullAndEmptyArrays
之类的东西实际上使用在猫鼬模型上定义的属性来执行“获取集合名称”之类的操作。这样可以避免硬编码到$group
本身,并且与代码运行时在模型上注册的内容保持一致。
如果您打算将属性输出为数组,或者甚至在模式上通过某个名称拥有现有的“引用数组”,则“保留该名称”。为路径制作临时名称确实没有多大意义,除非您在管道阶段专门为了在以后阶段“重新排序”字段输出的目的而这样做。否则,只需使用您想要输出的名称,就像在所有情况下一样。通过这种方式阅读和解释意图要容易得多。
除非你的意思是真的,否则不要使用像const fs = require('fs'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
const uri = 'mongodb://localhost/nested',
options = { useMongoClient: true };
const responseSchema = new Schema({
response_text: String,
order: String,
created_date: Date,
question: { type: Schema.Types.ObjectId, ref: 'Question' }
});
const questionSchema = new Schema({
question_text: String,
order: String,
created_date: Date,
group: { type: Schema.Types.ObjectId, ref: 'Group' }
});
const groupSchema = new Schema({
name: String,
order: String,
created_date: Date,
survey: { type: Schema.Types.ObjectId, ref: 'Survey' },
questions: [{ type: Schema.Types.ObjectId, ref: 'Question' }]
});
const surveySchema = new Schema({
company: { type: Schema.Types.ObjectId, ref: 'Company' },
created_date: Date,
enabled: Boolean,
name: String
});
const companySchema = new Schema({
});
const Company = mongoose.model('Company', companySchema);
const Survey = mongoose.model('Survey', surveySchema);
const Group = mongoose.model('Group', groupSchema);
const Question = mongoose.model('Question', questionSchema);
const Response = mongoose.model('Response', responseSchema);
function log(data) {
console.log(JSON.stringify(data,undefined,2))
}
(async function() {
try {
const conn = await mongoose.connect(uri,options);
await Promise.all(
Object.keys(conn.models).map( m => conn.models[m].remove() )
);
// Initialize data
let content = JSON.parse(fs.readFileSync('./jsonSurveys.json'));
//log(content);
for ( let item of content ) {
let survey = await Survey.create(item);
let company = await Company.create({ _id: survey.company });
for ( let group of item.groups ) {
await Group.create(group);
for ( let question of group.questions ) {
await Question.create(question);
for ( let response of question.responses ) {
await Response.create(response);
}
}
}
}
// Run aggregation
let results = await Survey.aggregate([
{ "$lookup": {
"from": Group.collection.name,
"localField": "_id",
"foreignField": "survey",
"as": "groups"
}},
{ "$unwind": "$groups" },
{ "$lookup": {
"from": Question.collection.name,
"localField": "groups.questions",
"foreignField": "_id",
"as": "groups.questions"
}},
{ "$unwind": "$groups.questions" },
{ "$lookup": {
"from": Response.collection.name,
"localField": "groups.questions._id",
"foreignField": "question",
"as": "groups.questions.responses"
}},
{ "$group": {
"_id": {
"_id": "$_id",
"company": "$company",
"created_date": "$created_date",
"enabled": "$enabled",
"name": "$name",
"groups": {
"_id": "$groups._id",
"name": "$groups.name",
"order": "$groups.order",
"created_date": "$groups.created_date",
"survey": "$groups.survey"
}
},
"questions": { "$push": "$groups.questions" }
}},
{ "$sort": { "_id": 1 } },
{ "$group": {
"_id": "$_id._id",
"company": { "$first": "$_id.company" },
"created_date": { "$first": "$_id.created_date" },
"enabled": { "$first": "$_id.enabled" },
"name": { "$first": "$_id.name" },
"groups": {
"$push": {
"_id": "$_id.groups._id",
"name": "$_id.groups.name",
"order": "$_id.groups.order",
"created_date": "$_id.groups.created_date",
"survey": "$_id.groups.survey",
"questions": "$questions"
}
}
}},
{ "$sort": { "_id": 1 } }
]);
log(results);
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})();
这样的选项。有一种“特殊方式”,实际处理$lookup
+ $lookup
的组合,并且真正在“单一阶段”执行,而不是在“展开”之前检索所有结果。您可以在聚合管道的“explain”输出中看到这一点。简而言之,如果您总是有关系匹配,那么请不要使用该选项。最好不要。
作为完整的列表和概念证明,我们可以加载您的源JSON,将其存储在数据库中的单独集合中,然后使用聚合语句来检索和重建所需的结构:
.populate()
另外值得注意的是,通过一些小的架构更改,可以通过使用 let alternate = await Survey.find().populate({
path: 'groups',
populate: {
path: 'questions',
populate: {
path: 'responses'
}
}
});
的嵌套调用来实现相同的结果:
Mongoose: groups.find({ survey: { '$in': [ ObjectId("59b6715725dcd2060da7f591") ] } }, { fields: {} })
Mongoose: questions.find({ _id: { '$in': [ ObjectId("59b6715725dcd2060da7f594"), ObjectId("59b6715725dcd2060da7f595"), ObjectId("59b6715725dcd2060da7f596"), ObjectId("59b6715725dcd2060da7f597") ] } }, { fields: {} })
Mongoose: responses.find({ question: { '$in': [ ObjectId("59b6715725dcd2060da7f594"), ObjectId("59b6715725dcd2060da7f595"), ObjectId("59b6715725dcd2060da7f596"), ObjectId("59b6715725dcd2060da7f597") ] } }, { fields: {} })
虽然它看起来简单得多,但它实际上引入了更多的负载,因为它会向数据库发出多个查询以便检索数据,而不是一次调用:
const fs = require('fs'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
const uri = 'mongodb://localhost/nested',
options = { useMongoClient: true };
const responseSchema = new Schema({
response_text: String,
order: String,
created_date: Date,
question: { type: Schema.Types.ObjectId, ref: 'Question' }
});
const questionSchema = new Schema({
question_text: String,
order: String,
created_date: Date,
group: { type: Schema.Types.ObjectId, ref: 'Group' }
},{
toJSON: {
virtuals: true,
transform: function(doc,obj) {
delete obj.id;
return obj;
}
}
});
questionSchema.virtual('responses',{
ref: 'Response',
localField: '_id',
foreignField: 'question'
});
const groupSchema = new Schema({
name: String,
order: String,
created_date: Date,
survey: { type: Schema.Types.ObjectId, ref: 'Survey' },
questions: [{ type: Schema.Types.ObjectId, ref: 'Question' }]
});
const surveySchema = new Schema({
company: { type: Schema.Types.ObjectId, ref: 'Company' },
created_date: Date,
enabled: Boolean,
name: String
},{
toJSON: {
virtuals: true,
transform: function(doc,obj) {
delete obj.id;
return obj;
}
}
});
surveySchema.virtual('groups',{
ref: 'Group',
localField: '_id',
foreignField: 'survey'
});
const companySchema = new Schema({
});
const Company = mongoose.model('Company', companySchema);
const Survey = mongoose.model('Survey', surveySchema);
const Group = mongoose.model('Group', groupSchema);
const Question = mongoose.model('Question', questionSchema);
const Response = mongoose.model('Response', responseSchema);
function log(data) {
console.log(JSON.stringify(data,undefined,2))
}
(async function() {
try {
const conn = await mongoose.connect(uri,options);
await Promise.all(
Object.keys(conn.models).map( m => conn.models[m].remove() )
);
// Initialize data
let content = JSON.parse(fs.readFileSync('./jsonSurveys.json'));
//log(content);
for ( let item of content ) {
let survey = await Survey.create(item);
let company = await Company.create({ _id: survey.company });
for ( let group of item.groups ) {
await Group.create(group);
for ( let question of group.questions ) {
await Question.create(question);
for ( let response of question.responses ) {
await Response.create(response);
}
}
}
}
// Run aggregation
let results = await Survey.aggregate([
{ "$lookup": {
"from": Group.collection.name,
"localField": "_id",
"foreignField": "survey",
"as": "groups"
}},
{ "$unwind": "$groups" },
{ "$lookup": {
"from": Question.collection.name,
"localField": "groups.questions",
"foreignField": "_id",
"as": "groups.questions"
}},
{ "$unwind": "$groups.questions" },
{ "$lookup": {
"from": Response.collection.name,
"localField": "groups.questions._id",
"foreignField": "question",
"as": "groups.questions.responses"
}},
{ "$group": {
"_id": {
"_id": "$_id",
"company": "$company",
"created_date": "$created_date",
"enabled": "$enabled",
"name": "$name",
"groups": {
"_id": "$groups._id",
"name": "$groups.name",
"order": "$groups.order",
"created_date": "$groups.created_date",
"survey": "$groups.survey"
}
},
"questions": { "$push": "$groups.questions" }
}},
{ "$sort": { "_id": 1 } },
{ "$group": {
"_id": "$_id._id",
"company": { "$first": "$_id.company" },
"created_date": { "$first": "$_id.created_date" },
"enabled": { "$first": "$_id.enabled" },
"name": { "$first": "$_id.name" },
"groups": {
"$push": {
"_id": "$_id.groups._id",
"name": "$_id.groups.name",
"order": "$_id.groups.order",
"created_date": "$_id.groups.created_date",
"survey": "$_id.groups.survey",
"questions": "$questions"
}
}
}},
{ "$sort": { "_id": 1 } }
]);
log(results);
let alternate = await Survey.find().populate({
path: 'groups',
populate: {
path: 'questions',
populate: {
path: 'responses'
}
}
});
log(alternate);
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})();
您可以看到架构更改(仅添加连接的虚拟字段)以及修订列表中的代码:
{{1}}