Bạn cần "chiếu" kết quả khớp ở đây vì tất cả những gì truy vấn MongoDB thực hiện là tìm kiếm "tài liệu" có "ít nhất một phần tử" đó là "lớn hơn" điều kiện bạn yêu cầu.
Vì vậy, việc lọc một "mảng" không giống như điều kiện "truy vấn" mà bạn có.
Một "phép chiếu" đơn giản sẽ chỉ trả lại mục phù hợp "đầu tiên" với điều kiện đó. Vì vậy, nó có thể không phải là những gì bạn muốn, nhưng là một ví dụ:
Order.find({ "articles.quantity": { "$gte": 5 } })
.select({ "articles.$": 1 })
.populate({
"path": "articles.article",
"match": { "price": { "$lte": 500 } }
}).exec(function(err,orders) {
// populated and filtered twice
}
)
"Loại" đó thực hiện những gì bạn muốn, nhưng vấn đề thực sự là sẽ chỉ quay lại nhiều nhất một phần tử trong "articles"
mảng.
Để làm điều này đúng cách, bạn cần .aggregate()
để lọc nội dung mảng. Lý tưởng nhất là điều này được thực hiện với MongoDB 3.2 và $filter
. Nhưng cũng có một cách đặc biệt để .populate()
tại đây:
Order.aggregate(
[
{ "$match": { "artciles.quantity": { "$gte": 5 } } },
{ "$project": {
"orderdate": 1,
"articles": {
"$filter": {
"input": "$articles",
"as": "article",
"cond": {
"$gte": [ "$$article.quantity", 5 ]
}
}
},
"__v": 1
}}
],
function(err,orders) {
Order.populate(
orders.map(function(order) { return new Order(order) }),
{
"path": "articles.article",
"match": { "price": { "$lte": 500 } }
},
function(err,orders) {
// now it's all populated and mongoose documents
}
)
}
)
Vì vậy, những gì xảy ra ở đây là "lọc" mảng thực sự xảy ra trong .aggregate()
nhưng tất nhiên kết quả từ đây không còn là "tài liệu mongoose" vì một khía cạnh của .aggregate()
là nó có thể "thay đổi" cấu trúc tài liệu và vì lý do này mongoose "giả định" trường hợp đó và chỉ trả về một "đối tượng thuần túy".
Đó thực sự không phải là vấn đề, vì khi bạn nhìn thấy $project
giai đoạn này, chúng tôi thực sự đang yêu cầu tất cả các trường giống nhau có trong tài liệu theo lược đồ đã xác định. Vì vậy, mặc dù nó chỉ là một "đối tượng thuần túy", không có vấn đề gì khi "truyền" nó trở lại tài liệu mongoose.
Đây là nơi chứa .map()
xuất hiện, vì nó trả về một mảng "tài liệu" đã chuyển đổi, sau đó rất quan trọng cho giai đoạn tiếp theo.
Bây giờ bạn gọi Model.populate()
sau đó có thể chạy thêm "dân số" trên "mảng tài liệu mongoose".
Kết quả cuối cùng là những gì bạn muốn.
MongoDB phiên bản cũ hơn 3.2.x
Điều duy nhất thực sự thay đổi ở đây là quy trình tổng hợp, Vì vậy, đó là tất cả những gì cần được đưa vào cho ngắn gọn.
MongoDB 2.6 - Có thể lọc mảng với sự kết hợp của $map
và $setDifference
. Kết quả là một "set" nhưng đó không phải là vấn đề khi mongoose tạo _id
trên tất cả các mảng tài liệu con theo mặc định:
[
{ "$match": { "artciles.quantity": { "$gte": 5 } } },
{ "$project": {
"orderdate": 1,
"articles": {
"$setDiffernce": [
{ "$map": {
"input": "$articles",
"as": "article",
"in": {
"$cond": [
{ "$gte": [ "$$article.price", 5 ] },
"$$article",
false
]
}
}},
[false]
]
},
"__v": 1
}}
],
Các bản sửa đổi cũ hơn phải sử dụng $unwind
:
[
{ "$match": { "artciles.quantity": { "$gte": 5 } }},
{ "$unwind": "$articles" },
{ "$match": { "artciles.quantity": { "$gte": 5 } }},
{ "$group": {
"_id": "$_id",
"orderdate": { "$first": "$orderdate" },
"articles": { "$push": "$articles" },
"__v": { "$first": "$__v" }
}}
],
Giải pháp thay thế $ lookup
Một cách thay thế khác là thay vào đó chỉ làm mọi thứ trên "máy chủ". Đây là một tùy chọn với $lookup
của MongoDB 3.2 trở lên:
Order.aggregate(
[
{ "$match": { "artciles.quantity": { "$gte": 5 } }},
{ "$project": {
"orderdate": 1,
"articles": {
"$filter": {
"input": "$articles",
"as": "article",
"cond": {
"$gte": [ "$$article.quantity", 5 ]
}
}
},
"__v": 1
}},
{ "$unwind": "$articles" },
{ "$lookup": {
"from": "articles",
"localField": "articles.article",
"foreignField": "_id",
"as": "articles.article"
}},
{ "$unwind": "$articles.article" },
{ "$group": {
"_id": "$_id",
"orderdate": { "$first": "$orderdate" },
"articles": { "$push": "$articles" },
"__v": { "$first": "$__v" }
}},
{ "$project": {
"orderdate": 1,
"articles": {
"$filter": {
"input": "$articles",
"as": "article",
"cond": {
"$lte": [ "$$article.article.price", 500 ]
}
}
},
"__v": 1
}}
],
function(err,orders) {
}
)
Và mặc dù đó chỉ là những tài liệu thuần túy, nhưng nó chỉ là những kết quả giống như những gì bạn sẽ nhận được từ .populate()
cách tiếp cận. Và tất nhiên, bạn luôn có thể truy cập và "truyền" lại các tài liệu mongoose trong mọi trường hợp nếu bạn thực sự phải làm.
Đường dẫn "ngắn nhất"
Điều này thực sự quay trở lại câu lệnh ban đầu, nơi về cơ bản bạn chỉ "chấp nhận" rằng "truy vấn" không có nghĩa là "lọc" nội dung mảng. .populate()
thực sự có thể làm như vậy bởi vì nó chỉ là một "truy vấn" khác và đang nhồi nhét "tài liệu" một cách thuận tiện.
Vì vậy, nếu bạn thực sự không tiết kiệm "bucketload" của băng thông bằng cách loại bỏ các thành viên mảng bổ sung trong mảng tài liệu gốc, thì chỉ cần .filter()
chúng ra trong mã xử lý bài:
Order.find({ "articles.quantity": { "$gte": 5 } })
.populate({
"path": "articles.article",
"match": { "price": { "$lte": 500 } }
}).exec(function(err,orders) {
orders = orders.filter(function(order) {
order.articles = order.articles.filter(function(article) {
return (
( article.quantity >= 5 ) &&
( article.article != null )
)
});
return order.aricles.length > 0;
})
// orders has non matching entries removed
}
)