Tất nhiên, có một số cách tiếp cận tùy thuộc vào phiên bản MongoDB có sẵn của bạn. Những điều này thay đổi tùy theo các cách sử dụng khác nhau của $lookup
cho phép thao tác đối tượng trên .populate()
kết quả qua .lean()
.
Tôi yêu cầu bạn đọc kỹ các phần và lưu ý rằng tất cả có thể không giống như khi xem xét giải pháp triển khai của bạn.
MongoDB 3.6, $ lookup "lồng nhau"
Với MongoDB 3.6, $lookup
toán tử nhận được khả năng bổ sung để bao gồm một pipeline
biểu thức trái ngược với việc chỉ cần kết hợp giá trị khóa "cục bộ" với "ngoại", điều này có nghĩa là về cơ bản, bạn có thể thực hiện mỗi $lookup
là "lồng nhau" bên trong các biểu thức đường ống này
Venue.aggregate([
{ "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
{ "$lookup": {
"from": Review.collection.name,
"let": { "reviews": "$reviews" },
"pipeline": [
{ "$match": { "$expr": { "$in": [ "$_id", "$$reviews" ] } } },
{ "$lookup": {
"from": Comment.collection.name,
"let": { "comments": "$comments" },
"pipeline": [
{ "$match": { "$expr": { "$in": [ "$_id", "$$comments" ] } } },
{ "$lookup": {
"from": Author.collection.name,
"let": { "author": "$author" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$_id", "$$author" ] } } },
{ "$addFields": {
"isFollower": {
"$in": [
mongoose.Types.ObjectId(req.user.id),
"$followers"
]
}
}}
],
"as": "author"
}},
{ "$addFields": {
"author": { "$arrayElemAt": [ "$author", 0 ] }
}}
],
"as": "comments"
}},
{ "$sort": { "createdAt": -1 } }
],
"as": "reviews"
}},
])
Điều này có thể thực sự khá mạnh mẽ, như bạn thấy từ quan điểm của đường dẫn ban đầu, nó thực sự chỉ biết về việc thêm nội dung vào "reviews"
mảng và sau đó mỗi biểu thức đường ống "lồng nhau" tiếp theo cũng chỉ thấy nó là các phần tử "bên trong" từ phép nối.
Nó mạnh mẽ và theo một số khía cạnh, nó có thể rõ ràng hơn một chút vì tất cả các đường dẫn trường đều liên quan đến mức lồng nhau, nhưng nó bắt đầu thụt lề trong cấu trúc BSON và bạn cần phải biết liệu bạn có khớp với các mảng hay không hoặc các giá trị số ít trong việc duyệt qua cấu trúc.
Lưu ý rằng chúng tôi cũng có thể làm những việc ở đây như "san bằng thuộc tính tác giả" như được thấy trong "comments"
mục nhập mảng. Tất cả $lookup
đầu ra mục tiêu có thể là một "mảng", nhưng trong "đường ống con", chúng ta có thể định hình lại mảng phần tử đơn lẻ đó thành một giá trị duy nhất.
Tra cứu MongoDB $ tiêu chuẩn
Vẫn giữ "tham gia trên máy chủ", bạn thực sự có thể làm điều đó với $lookup
, nhưng nó chỉ cần xử lý trung gian. Đây là cách tiếp cận lâu đời với việc giải cấu trúc mảng với $unwind
và sử dụng $group
các giai đoạn để xây dựng lại mảng:
Venue.aggregate([
{ "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
{ "$lookup": {
"from": Review.collection.name,
"localField": "reviews",
"foreignField": "_id",
"as": "reviews"
}},
{ "$unwind": "$reviews" },
{ "$lookup": {
"from": Comment.collection.name,
"localField": "reviews.comments",
"foreignField": "_id",
"as": "reviews.comments",
}},
{ "$unwind": "$reviews.comments" },
{ "$lookup": {
"from": Author.collection.name,
"localField": "reviews.comments.author",
"foreignField": "_id",
"as": "reviews.comments.author"
}},
{ "$unwind": "$reviews.comments.author" },
{ "$addFields": {
"reviews.comments.author.isFollower": {
"$in": [
mongoose.Types.ObjectId(req.user.id),
"$reviews.comments.author.followers"
]
}
}},
{ "$group": {
"_id": {
"_id": "$_id",
"reviewId": "$review._id"
},
"name": { "$first": "$name" },
"addedBy": { "$first": "$addedBy" },
"review": {
"$first": {
"_id": "$review._id",
"createdAt": "$review.createdAt",
"venue": "$review.venue",
"author": "$review.author",
"content": "$review.content"
}
},
"comments": { "$push": "$reviews.comments" }
}},
{ "$sort": { "_id._id": 1, "review.createdAt": -1 } },
{ "$group": {
"_id": "$_id._id",
"name": { "$first": "$name" },
"addedBy": { "$first": "$addedBy" },
"reviews": {
"$push": {
"_id": "$review._id",
"venue": "$review.venue",
"author": "$review.author",
"content": "$review.content",
"comments": "$comments"
}
}
}}
])
Điều này thực sự không quá khó khăn như bạn nghĩ lúc đầu và tuân theo một mô hình đơn giản của $lookup
và $unwind
khi bạn tiến bộ qua từng mảng.
"author"
tất nhiên là chi tiết là số ít, vì vậy một khi "chưa được ràng buộc", bạn chỉ muốn để nó theo cách đó, hãy thực hiện thêm trường và bắt đầu quá trình "quay trở lại" vào các mảng.
Chỉ có hai các cấp độ để xây dựng lại Venue
ban đầu tài liệu, vì vậy mức chi tiết đầu tiên là bằng Review
để xây dựng lại "comments"
mảng. Tất cả những gì bạn cần là $push
đường dẫn của "$reviews.comments"
để thu thập những thứ này và miễn là "$reviews._id"
trong trường "grouping _id", những thứ khác bạn cần giữ lại là tất cả các trường khác. Bạn có thể đặt tất cả những thứ này vào _id
cũng như bạn có thể sử dụng $first
.
Sau đó, chỉ còn một $group
nữa để quay lại Venue
chinh no. Lần này, khóa nhóm là "$_id"
tất nhiên, với tất cả các tài sản của chính địa điểm bằng cách sử dụng $first
và "$review"
còn lại chi tiết quay trở lại một mảng với $push
. Tất nhiên là "$comments"
đầu ra từ $group
trước đó trở thành "review.comments"
đường dẫn.
Làm việc trên một tài liệu duy nhất và các mối quan hệ của nó, điều này thực sự không quá tệ. $unwind
nhà điều hành đường ống có thể nói chung là một vấn đề về hiệu suất, nhưng trong bối cảnh của việc sử dụng này, nó không thực sự gây ra nhiều tác động như vậy.
Vì dữ liệu vẫn đang được "kết hợp trên máy chủ" nên vẫn còn lưu lượng truy cập thấp hơn nhiều so với các giải pháp thay thế còn lại khác.
Thao tác JavaScript
Tất nhiên trường hợp khác ở đây là thay vì thay đổi dữ liệu trên chính máy chủ, bạn thực sự thao tác với kết quả. Trong hầu hết trường hợp tôi sẽ ủng hộ cách tiếp cận này vì mọi "bổ sung" vào dữ liệu có thể được xử lý tốt nhất trên máy khách.
Tất nhiên là vấn đề với việc sử dụng populate()
đó là trong khi nó có thể 'trông giống như' một quy trình đơn giản hơn nhiều, trên thực tế nó KHÔNG PHẢI LÀ THAM GIA theo bất kỳ cách nào. Tất cả populate()
thực sự là "ẩn" quy trình cơ bản của việc gửi nhiều truy vấn đến cơ sở dữ liệu, rồi chờ kết quả thông qua xử lý không đồng bộ.
Vì vậy, "sự xuất hiện" của một phép nối thực sự là kết quả của nhiều yêu cầu tới máy chủ và sau đó thực hiện "thao tác phía máy khách" của dữ liệu để nhúng chi tiết vào các mảng.
Vì vậy, ngoài cảnh báo rõ ràng đó rằng các đặc tính hiệu suất không ngang bằng với máy chủ $lookup
, một lưu ý khác tất nhiên là "mongoose Documents" trong kết quả không thực sự là các đối tượng JavaScript thuần túy có thể bị thao túng thêm.
Vì vậy, để thực hiện phương pháp này, bạn cần thêm .lean()
phương thức cho truy vấn trước khi thực thi, để hướng dẫn mongoose trả về "các đối tượng JavaScript thuần túy" thay vì Document
các kiểu được ép kiểu với các phương thức lược đồ được đính kèm với mô hình. Tất nhiên, lưu ý rằng dữ liệu kết quả không còn có quyền truy cập vào bất kỳ "phương thức phiên bản" nào mà nếu không sẽ được liên kết với chính các mô hình liên quan:
let venue = await Venue.findOne({ _id: id.id })
.populate({
path: 'reviews',
options: { sort: { createdAt: -1 } },
populate: [
{ path: 'comments', populate: [{ path: 'author' }] }
]
})
.lean();
Hiện tại venue
là một đối tượng đơn giản, chúng tôi có thể đơn giản xử lý và điều chỉnh khi cần:
venue.reviews = venue.reviews.map( r =>
({
...r,
comments: r.comments.map( c =>
({
...c,
author: {
...c.author,
isAuthor: c.author.followers.map( f => f.toString() ).indexOf(req.user.id) != -1
}
})
)
})
);
Vì vậy, nó thực sự chỉ là một vấn đề xoay quanh từng mảng bên trong cho đến mức bạn có thể thấy followers
mảng trong author
thông tin chi tiết. Sau đó, so sánh có thể được thực hiện với ObjectId
các giá trị được lưu trữ trong mảng đó sau lần đầu tiên sử dụng .map()
để trả về các giá trị "chuỗi" để so sánh với req.user.id
cũng là một chuỗi (nếu không, thì cũng thêm .toString()
về điều đó), vì nhìn chung dễ dàng so sánh các giá trị này theo cách này thông qua mã JavaScript.
Một lần nữa, mặc dù tôi cần nhấn mạnh rằng nó "trông có vẻ đơn giản" nhưng trên thực tế, đó là điều bạn thực sự muốn tránh đối với hiệu suất hệ thống, vì những truy vấn bổ sung đó và việc truyền giữa máy chủ và máy khách tốn rất nhiều thời gian xử lý và thậm chí do chi phí yêu cầu, điều này làm tăng thêm chi phí thực trong quá trình vận chuyển giữa các nhà cung cấp dịch vụ lưu trữ.
Tóm tắt
Về cơ bản, đó là những phương pháp tiếp cận mà bạn có thể thực hiện, ngắn gọn là "tự làm" nơi bạn thực sự thực hiện "nhiều truy vấn" vào cơ sở dữ liệu thay vì sử dụng trình trợ giúp .populate()
là.
Sử dụng đầu ra điền, sau đó bạn có thể chỉ cần thao tác dữ liệu với kết quả giống như bất kỳ cấu trúc dữ liệu nào khác, miễn là bạn áp dụng .lean()
vào truy vấn để chuyển đổi hoặc trích xuất dữ liệu đối tượng thuần túy từ các tài liệu mongoose được trả về.
Trong khi các phương pháp tiếp cận tổng hợp có vẻ liên quan hơn nhiều, có "rất nhiều" nhiều lợi thế hơn khi thực hiện công việc này trên máy chủ. Các tập hợp kết quả lớn hơn có thể được sắp xếp, các phép tính có thể được thực hiện để lọc thêm và tất nhiên bạn sẽ nhận được một "phản hồi duy nhất" đối với một "yêu cầu duy nhất" được thực hiện cho máy chủ, tất cả đều không có chi phí bổ sung.
Hoàn toàn có thể tranh luận rằng bản thân các đường ống có thể đơn giản được xây dựng dựa trên các thuộc tính đã được lưu trữ trên lược đồ. Vì vậy, việc viết phương pháp của riêng bạn để thực hiện "xây dựng" này dựa trên lược đồ đính kèm sẽ không quá khó khăn.
Tất nhiên về lâu dài hơn $lookup
là giải pháp tốt hơn, nhưng có lẽ bạn sẽ cần phải thực hiện thêm một chút công việc mã hóa ban đầu, nếu tất nhiên, bạn không chỉ đơn giản sao chép từ những gì được liệt kê ở đây;)