Hoe je dit ook bekijkt, zolang je een genormaliseerde relatie als deze hebt, zou je twee zoekopdrachten nodig hebben om een resultaat te krijgen met details uit de "taken" collectie en in te vullen met details uit de "projecten" collectie. MongoDB maakt op geen enkele manier gebruik van joins, en mangoest is niet anders. Mongoose biedt wel .populate()
, maar dat is alleen maar gemaksmagie voor wat in wezen een andere query uitvoert en de resultaten samenvoegt met de veldwaarde waarnaar wordt verwezen.
Dit is dus een geval waarin u misschien uiteindelijk kunt overwegen om de projectinformatie in de taak in te bedden. Natuurlijk zullen er duplicaties zijn, maar het maakt de zoekpatronen veel eenvoudiger met een enkele verzameling.
Door de collecties gescheiden te houden met een model waarnaar wordt verwezen, heb je in feite twee benaderingen. Maar eerst kun je aggregate gebruiken om resultaten te krijgen die beter aansluiten bij uw werkelijke vereisten:
Task.aggregate(
[
{ "$group": {
"_id": "$projectId",
"completed": {
"$sum": {
"$cond": [ "$completed", 1, 0 ]
}
},
"incomplete": {
"$sum": {
"$cond": [ "$completed", 0, 1 ]
}
}
}}
],
function(err,results) {
}
);
Dit gebruikt alleen een $group
pijplijn om te accumuleren op de waarden van "projectid" binnen de verzameling "taken". Om de waarden voor "completed" en "incomplete" te tellen, gebruiken we de $cond
operator die een drietal is om te beslissen welke waarde moet worden doorgegeven aan $sum
. Aangezien de eerste of "if"-voorwaarde hier een booleaanse evaluatie is, is het bestaande booleaanse veld "complete" voldoende, door door te geven waar true
naar "dan" of "anders" en geef het derde argument door.
Die resultaten zijn oké, maar ze bevatten geen informatie uit de "project"-verzameling voor de verzamelde "_id"-waarden. Een manier om de uitvoer er op deze manier uit te laten zien, is door de modelvorm .populate()
. aan te roepen vanuit de aggregatieresultaten terugbellen op het geretourneerde "results"-object:
Project.populate(results,{ "path": "_id" },callback);
In deze vorm de .populate()
call neemt een object of array van gegevens als eerste argument, met als tweede een optiedocument voor de populatie, waarbij het verplichte veld hier voor "pad" is. Dit zal alle items verwerken en "bevolken" uit het model dat werd aangeroepen door die objecten in te voegen in de resultaatgegevens in de callback.
Als een complete voorbeeldlijst:
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();
}
);
En dit geeft resultaten als volgt:
[
{
"_id": {
"_id": "54beef3178ef08ca249b98ef",
"name": "Project2",
"__v": 0
},
"completed": 0,
"incomplete": 1
}
]
Dus .populate()
werkt goed voor dit soort aggregatieresultaten, zelfs even effectief voor een andere zoekopdracht, en zou over het algemeen geschikt moeten zijn voor de meeste doeleinden. Er was echter een specifiek voorbeeld opgenomen in de lijst waar er "twee" projecten zijn gemaakt, maar natuurlijk slechts "één" taak die verwijst naar slechts één van de projecten.
Aangezien aggregatie werkt aan de verzameling "taken", heeft het geen enkele kennis van enig "project" waarnaar daar niet wordt verwezen. Om een volledige lijst van "projecten" met de berekende totalen te krijgen, moet u specifieker zijn in het uitvoeren van twee zoekopdrachten en het "samenvoegen" van de resultaten.
Dit is in feite een "hash merge" op verschillende sleutels en gegevens, maar een leuke hulp hiervoor is een module genaamd nedb , waarmee u de logica kunt toepassen op een manier die consistenter is met MongoDB-query's en -bewerkingen.
In principe wil je een kopie van de gegevens uit de "projecten" collectie met uitgebreide velden, dan wil je "samenvoegen" of .update()
die informatie met de aggregatieresultaten. Nogmaals als een volledige lijst om te demonstreren:
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();
}
En de resultaten hier:
[
{
"projectId": "54beef4c23d4e4e0246379db",
"name": "Project2",
"completed": 0,
"incomplete": 1
},
{
"projectId": "54beef4c23d4e4e0246379da",
"name": "Project1",
"completed": 0,
"incomplete": 0
}
]
Dat geeft een overzicht van de gegevens van elk "project" en bevat de berekende waarden uit de verzameling "taken" die eraan is gerelateerd.
Er zijn dus een paar benaderingen die u kunt doen. Nogmaals, u kunt uiteindelijk het beste gewoon "taken" insluiten in de "project" -items, wat opnieuw een eenvoudige aggregatiebenadering zou zijn. En als u de taakinformatie gaat insluiten, kunt u net zo goed tellers voor "complete" en "incomplete" op het "project"-object bijhouden en deze eenvoudig bijwerken, aangezien items zijn gemarkeerd als voltooid in de takenreeks met de $inc
telefoniste.
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
);
U moet echter de huidige taakstatus kennen om de tellerupdates correct te laten werken, maar dit is gemakkelijk te coderen en u zou waarschijnlijk op zijn minst die details in een object moeten hebben dat in uw methoden wordt doorgegeven.
Persoonlijk zou ik opnieuw modelleren naar de laatste vorm en dat doen. U kunt een zoekopdracht "samenvoegen" uitvoeren, zoals hier in twee voorbeelden is getoond, maar er zijn natuurlijk kosten aan verbonden.