Както и да погледнете на това, стига да имате нормализирана връзка като тази, тогава ще ви трябват две заявки, за да получите резултат, съдържащ подробности от колекцията „задачи“ и попълване с подробности от колекцията „проекти“. MongoDB не използва съединения по никакъв начин и mongoose не е по-различен. Mongoose предлага .populate()
, но това е само магия за удобство за това, което по същество представлява изпълнение на друга заявка и обединяване на резултатите в стойността на референтното поле.
Така че това е един случай, в който може би в крайна сметка можете да обмислите вграждането на информацията за проекта в задачата. Разбира се, ще има дублиране, но това прави моделите на заявките много по-прости с единствена колекция.
Като държите колекциите разделени с референтен модел, тогава имате два подхода. Но първо можете да използвате aggregate за да получите резултати, които отговарят повече на действителните ви изисквания:
Task.aggregate(
[
{ "$group": {
"_id": "$projectId",
"completed": {
"$sum": {
"$cond": [ "$completed", 1, 0 ]
}
},
"incomplete": {
"$sum": {
"$cond": [ "$completed", 0, 1 ]
}
}
}}
],
function(err,results) {
}
);
Това просто използва $group
тръбопровод, за да се натрупат върху стойностите на "projectid" в колекцията "tasks". За да преброим стойностите за „завършено“ и „незавършено“, ние използваме $cond
оператор, който е троичен, за да решите коя стойност да прехвърлите към $sum
. Тъй като първото условие или "if" тук е булева оценка, тогава съществуващото булево поле "complete" ще свърши работа, предавайки където true
към "then" или "else", предавайки третия аргумент.
Тези резултати са добри, но не съдържат информация от колекцията „project“ за събраните стойности „_id“. Един подход да направите изхода да изглежда по този начин е да извикате моделната форма на .populate()
от обратното извикване на резултатите от агрегирането на върнатия обект „резултати“:
Project.populate(results,{ "path": "_id" },callback);
В тази форма .populate()
call приема обект или масив от данни като първи аргумент, като вторият е документ с опции за популацията, където задължителното поле тук е за "път". Това ще обработи всички елементи и ще ги „попълни“ от модела, който се нарича вмъкване на тези обекти в данните за резултатите в обратното извикване.
Като пълен примерен списък:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
var projectSchema = new Schema({
"name": String
});
var taskSchema = new Schema({
"projectId": { "type": Schema.Types.ObjectId, "ref": "Project" },
"completed": { "type": Boolean, "default": false }
});
var Project = mongoose.model( "Project", projectSchema );
var Task = mongoose.model( "Task", taskSchema );
mongoose.connect('mongodb://localhost/test');
async.waterfall(
[
function(callback) {
async.each([Project,Task],function(model,callback) {
model.remove({},callback);
},
function(err) {
callback(err);
});
},
function(callback) {
Project.create({ "name": "Project1" },callback);
},
function(project,callback) {
Project.create({ "name": "Project2" },callback);
},
function(project,callback) {
Task.create({ "projectId": project },callback);
},
function(task,callback) {
Task.aggregate(
[
{ "$group": {
"_id": "$projectId",
"completed": {
"$sum": {
"$cond": [ "$completed", 1, 0 ]
}
},
"incomplete": {
"$sum": {
"$cond": [ "$completed", 0, 1 ]
}
}
}}
],
function(err,results) {
if (err) callback(err);
Project.populate(results,{ "path": "_id" },callback);
}
);
}
],
function(err,results) {
if (err) throw err;
console.log( JSON.stringify( results, undefined, 4 ));
process.exit();
}
);
И това ще даде резултати като следния:
[
{
"_id": {
"_id": "54beef3178ef08ca249b98ef",
"name": "Project2",
"__v": 0
},
"completed": 0,
"incomplete": 1
}
]
И така .populate()
работи добре за този вид резултат от агрегиране, дори толкова ефективно за друга заявка, и като цяло трябва да е подходящ за повечето цели. Имаше обаче конкретен пример, включен в списъка, където има създадени "два" проекта, но разбира се само "една" задача, отнасяща се само до един от проектите.
Тъй като агрегацията работи върху колекцията „задачи“, тя няма никакви познания за нито един „проект“, който не е споменат там. За да получите пълен списък от „проекти“ с изчислените суми, трябва да сте по-конкретни при изпълнението на две заявки и „сливането“ на резултатите.
Това е основно „сливане на хеш“ на отделни ключове и данни, но добър помощник за това е модул, наречен nedb , което ви позволява да прилагате логиката по начин, който е по-съвместим със заявките и операциите на MongoDB.
По принцип искате копие на данните от колекцията „projects“ с разширени полета, след което искате да „слеете“ или .update()
тази информация с резултатите от обобщаването. Отново като пълен списък за демонстрация:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema,
DataStore = require('nedb'),
db = new DataStore();
var projectSchema = new Schema({
"name": String
});
var taskSchema = new Schema({
"projectId": { "type": Schema.Types.ObjectId, "ref": "Project" },
"completed": { "type": Boolean, "default": false }
});
var Project = mongoose.model( "Project", projectSchema );
var Task = mongoose.model( "Task", taskSchema );
mongoose.connect('mongodb://localhost/test');
async.waterfall(
[
function(callback) {
async.each([Project,Task],function(model,callback) {
model.remove({},callback);
},
function(err) {
callback(err);
});
},
function(callback) {
Project.create({ "name": "Project1" },callback);
},
function(project,callback) {
Project.create({ "name": "Project2" },callback);
},
function(project,callback) {
Task.create({ "projectId": project },callback);
},
function(task,callback) {
async.series(
[
function(callback) {
Project.find({},function(err,projects) {
async.eachLimit(projects,10,function(project,callback) {
db.insert({
"projectId": project._id.toString(),
"name": project.name,
"completed": 0,
"incomplete": 0
},callback);
},callback);
});
},
function(callback) {
Task.aggregate(
[
{ "$group": {
"_id": "$projectId",
"completed": {
"$sum": {
"$cond": [ "$completed", 1, 0 ]
}
},
"incomplete": {
"$sum": {
"$cond": [ "$completed", 0, 1 ]
}
}
}}
],
function(err,results) {
async.eachLimit(results,10,function(result,callback) {
db.update(
{ "projectId": result._id.toString() },
{ "$set": {
"complete": result.complete,
"incomplete": result.incomplete
}
},
callback
);
},callback);
}
);
},
],
function(err) {
if (err) callback(err);
db.find({},{ "_id": 0 },callback);
}
);
}
],
function(err,results) {
if (err) throw err;
console.log( JSON.stringify( results, undefined, 4 ));
process.exit();
}
И резултатите тук:
[
{
"projectId": "54beef4c23d4e4e0246379db",
"name": "Project2",
"completed": 0,
"incomplete": 1
},
{
"projectId": "54beef4c23d4e4e0246379da",
"name": "Project1",
"completed": 0,
"incomplete": 0
}
]
Това изброява данните от всеки „проект“ и включва изчислените стойности от колекцията „задачи“, която е свързана с него.
Така че има няколко подхода, които можете да направите. Отново, в крайна сметка може да е най-добре просто да вградите „задачи“ в елементите „проект“ вместо това, което отново би било прост подход на агрегиране. И ако възнамерявате да вградите информацията за задачата, можете също така да поддържате броячи за „завършено“ и „незавършено“ в обекта „проект“ и просто да ги актуализирате, тъй като елементите са маркирани като завършени в масива от задачи с $inc
оператор.
var taskSchema = new Schema({
"completed": { "type": Boolean, "default": false }
});
var projectSchema = new Schema({
"name": String,
"completed": { "type": Number, "default": 0 },
"incomplete": { "type": Number, "default": 0 }
"tasks": [taskSchema]
});
var Project = mongoose.model( "Project", projectSchema );
// cheat for a model object with no collection
var Task = mongoose.model( "Task", taskSchema, undefined );
// Then in later code
// Adding a task
var task = new Task();
Project.update(
{ "task._id": { "$ne": task._id } },
{
"$push": { "tasks": task },
"$inc": {
"completed": ( task.completed ) ? 1 : 0,
"incomplete": ( !task.completed ) ? 1 : 0;
}
},
callback
);
// Removing a task
Project.update(
{ "task._id": task._id },
{
"$pull": { "tasks": { "_id": task._id } },
"$inc": {
"completed": ( task.completed ) ? -1 : 0,
"incomplete": ( !task.completed ) ? -1 : 0;
}
},
callback
);
// Marking complete
Project.update(
{ "tasks": { "$elemMatch": { "_id": task._id, "completed": false } }},
{
"$set": { "tasks.$.completed": true },
"$inc": {
"completed": 1,
"incomplete": -1
}
},
callback
);
Все пак трябва да знаете текущия статус на задачата, за да работят коректно актуализациите на брояча, но това е лесно за кодиране и вероятно трябва да имате поне тези подробности в обект, преминаващ към вашите методи.
Лично аз бих премоделирал до последната форма и бих го направил. Можете да направите „сливане“ на заявка, както е показано в два примера тук, но това, разбира се, има цена.