Dù bạn nhìn điều này theo cách nào, miễn là bạn có một mối quan hệ chuẩn hóa như thế này thì bạn sẽ cần hai truy vấn để nhận được kết quả có chứa thông tin chi tiết từ bộ sưu tập "nhiệm vụ" và điền thông tin chi tiết từ bộ sưu tập "dự án". MongoDB không sử dụng các phép nối theo bất kỳ cách nào và mongoose cũng không khác. Mongoose cung cấp .populate()
, nhưng đó chỉ là phép thuật tiện lợi cho những gì về cơ bản đang chạy một truy vấn khác và hợp nhất các kết quả trên giá trị trường được tham chiếu.
Vì vậy, đây là một trường hợp mà cuối cùng có thể bạn có thể cân nhắc việc nhúng thông tin dự án vào nhiệm vụ. Tất nhiên sẽ có sự trùng lặp, nhưng nó làm cho các mẫu truy vấn trở nên đơn giản hơn nhiều với một tập hợp số ít.
Giữ các bộ sưu tập được tách biệt bằng một mô hình được tham chiếu, về cơ bản bạn có hai cách tiếp cận. Nhưng trước hết, bạn có thể sử dụng tổng hợp để có được kết quả nhiều hơn theo yêu cầu thực tế của bạn:
Task.aggregate(
[
{ "$group": {
"_id": "$projectId",
"completed": {
"$sum": {
"$cond": [ "$completed", 1, 0 ]
}
},
"incomplete": {
"$sum": {
"$cond": [ "$completed", 0, 1 ]
}
}
}}
],
function(err,results) {
}
);
Điều này chỉ sử dụng $group
để tích lũy các giá trị của "projectid" trong bộ sưu tập "nhiệm vụ". Để đếm các giá trị cho "đã hoàn thành" và "chưa hoàn thành", chúng tôi sử dụng $cond
toán tử là một bậc ba để quyết định giá trị nào sẽ chuyển đến $sum
. Vì điều kiện đầu tiên hoặc "nếu" ở đây là đánh giá boolean, sau đó trường "hoàn thành" boolean hiện có sẽ thực hiện, chuyển vào vị trí true
thành "then" hoặc "else" chuyển đối số thứ ba.
Những kết quả đó không sao nhưng chúng không chứa bất kỳ thông tin nào từ bộ sưu tập "dự án" cho các giá trị "_id" đã thu thập. Một cách tiếp cận để làm cho đầu ra trông theo cách này là gọi dạng mô hình của .populate()
từ bên trong lệnh gọi lại kết quả tổng hợp trên đối tượng "results" được trả về:
Project.populate(results,{ "path": "_id" },callback);
Trong biểu mẫu này, .populate()
lệnh gọi nhận một đối tượng hoặc mảng dữ liệu làm đối số đầu tiên, với đối số thứ hai là tài liệu tùy chọn cho tập hợp, trong đó trường bắt buộc ở đây là "đường dẫn". Thao tác này sẽ xử lý bất kỳ mục nào và "điền" từ mô hình được gọi là chèn các đối tượng đó vào dữ liệu kết quả trong lệnh gọi lại.
Như một danh sách ví dụ hoàn chỉnh:
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();
}
);
Và điều này sẽ cho kết quả như sau:
[
{
"_id": {
"_id": "54beef3178ef08ca249b98ef",
"name": "Project2",
"__v": 0
},
"completed": 0,
"incomplete": 1
}
]
Vì vậy, .populate()
hoạt động tốt cho loại kết quả tổng hợp này, thậm chí hiệu quả như một truy vấn khác và nói chung phải phù hợp với hầu hết các mục đích. Tuy nhiên, có một ví dụ cụ thể được đưa vào danh sách nơi có "hai" dự án được tạo nhưng tất nhiên chỉ có "một" nhiệm vụ chỉ một trong các dự án.
Vì tập hợp đang làm việc trên tập hợp "nhiệm vụ", nó không có kiến thức về bất kỳ "dự án" nào không được tham chiếu ở đó. Để có được danh sách đầy đủ các "dự án" với tổng được tính toán, bạn cần phải cụ thể hơn trong việc chạy hai truy vấn và "hợp nhất" kết quả.
Về cơ bản đây là "hợp nhất băm" trên các khóa và dữ liệu riêng biệt, tuy nhiên, người trợ giúp tốt cho việc này là một mô-đun có tên nedb , cho phép bạn áp dụng logic theo cách nhất quán hơn với các truy vấn và hoạt động MongoDB.
Về cơ bản, bạn muốn một bản sao dữ liệu từ bộ sưu tập "dự án" với các trường tăng cường, sau đó bạn muốn "hợp nhất" hoặc .update()
thông tin đó với kết quả tổng hợp. Một lần nữa như một danh sách đầy đủ để chứng minh:
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();
}
Và kết quả ở đây:
[
{
"projectId": "54beef4c23d4e4e0246379db",
"name": "Project2",
"completed": 0,
"incomplete": 1
},
{
"projectId": "54beef4c23d4e4e0246379da",
"name": "Project1",
"completed": 0,
"incomplete": 0
}
]
Dữ liệu đó liệt kê dữ liệu từ mọi "dự án" và bao gồm các giá trị được tính toán từ bộ sưu tập "nhiệm vụ" có liên quan đến nó.
Vì vậy, có một số cách tiếp cận bạn có thể làm. Một lần nữa, cuối cùng bạn có thể tốt nhất chỉ nhúng "nhiệm vụ" vào các mục "dự án" thay vào đó, đây sẽ là một cách tiếp cận tổng hợp đơn giản. Và nếu bạn định nhúng thông tin nhiệm vụ, thì bạn cũng có thể duy trì các bộ đếm cho "hoàn thành" và "chưa hoàn thành" trên đối tượng "dự án" và chỉ cần cập nhật chúng khi các mục được đánh dấu là đã hoàn thành trong mảng nhiệm vụ bằng dấu $inc
nhà điều hành.
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
);
Mặc dù vậy, bạn phải biết trạng thái tác vụ hiện tại để cập nhật bộ đếm hoạt động chính xác, nhưng điều này rất dễ viết mã và bạn có thể nên có ít nhất những chi tiết đó trong một đối tượng truyền vào các phương thức của bạn.
Cá nhân tôi sẽ mô hình lại mẫu sau và làm điều đó. Bạn có thể thực hiện truy vấn "hợp nhất" như đã được trình bày trong hai ví dụ ở đây, nhưng tất nhiên phải trả phí.