Bạn có thể giải quyết vấn đề này theo một vài cách khác nhau. Tất nhiên, chúng khác nhau về cách tiếp cận và hiệu suất, và tôi nghĩ rằng có một số cân nhắc lớn hơn bạn cần thực hiện đối với thiết kế của mình. Đáng chú ý nhất ở đây là "nhu cầu" đối với dữ liệu "bản sửa đổi" trong kiểu sử dụng của ứng dụng thực tế của bạn.
Truy vấn qua tổng hợp
Đối với điểm quan trọng nhất của việc lấy "phần tử cuối cùng từ mảng bên trong", thì bạn thực sự nên sử dụng .aggregate()
hoạt động để thực hiện điều này:
function getProject(req,projectId) {
return new Promise((resolve,reject) => {
Project.aggregate([
{ "$match": { "project_id": projectId } },
{ "$addFields": {
"uploaded_files": {
"$map": {
"input": "$uploaded_files",
"as": "f",
"in": {
"latest": {
"$arrayElemAt": [
"$$f.history",
-1
]
},
"_id": "$$f._id",
"display_name": "$$f.display_name"
}
}
}
}},
{ "$lookup": {
"from": "owner_collection",
"localField": "owner",
"foreignField": "_id",
"as": "owner"
}},
{ "$unwind": "$uploaded_files" },
{ "$lookup": {
"from": "files_collection",
"localField": "uploaded_files.latest.file",
"foreignField": "_id",
"as": "uploaded_files.latest.file"
}},
{ "$group": {
"_id": "$_id",
"project_id": { "$first": "$project_id" },
"updated_at": { "$first": "$updated_at" },
"created_at": { "$first": "$created_at" },
"owner" : { "$first": { "$arrayElemAt": [ "$owner", 0 ] } },
"name": { "$first": "$name" },
"uploaded_files": {
"$push": {
"latest": { "$arrayElemAt": [ "$$uploaded_files", 0 ] },
"_id": "$$uploaded_files._id",
"display_name": "$$uploaded_files.display_name"
}
}
}}
])
.then(result => {
if (result.length === 0)
reject(new createError.NotFound(req.path));
resolve(result[0])
})
.catch(reject)
})
}
Vì đây là một câu lệnh tổng hợp trong đó chúng tôi cũng có thể thực hiện "tham gia" trên "máy chủ" thay vì đưa ra các yêu cầu bổ sung (chính là .populate()
thực sự làm ở đây) bằng cách sử dụng $lookup
, Tôi đang tự do với tên bộ sưu tập thực tế vì lược đồ của bạn không được bao gồm trong câu hỏi. Điều đó không sao, vì bạn không nhận ra rằng trên thực tế, bạn có thể làm theo cách này.
Tất nhiên, tên bộ sưu tập "thực tế" được yêu cầu bởi máy chủ, máy chủ không có khái niệm về lược đồ được xác định "phía ứng dụng". Có những điều bạn có thể làm để thuận tiện ở đây, nhưng hãy làm thêm sau.
Bạn cũng cần lưu ý rằng tùy thuộc vào nơi projectId
thực sự xuất phát từ đó, không giống như các phương thức mongoose thông thường như .find()
$match
sẽ yêu cầu thực sự "truyền" tới một ObjectId
nếu giá trị đầu vào trên thực tế là một "chuỗi". Mongoose không thể áp dụng "các loại lược đồ" trong một đường dẫn tổng hợp, vì vậy bạn có thể cần phải tự mình thực hiện việc này, đặc biệt nếu projectId
đến từ một tham số yêu cầu:
{ "$match": { "project_id": Schema.Types.ObjectId(projectId) } },
Phần cơ bản ở đây là nơi chúng tôi sử dụng $map
để lặp lại tất cả "uploaded_files"
các mục nhập, và sau đó chỉ cần trích xuất "mới nhất" từ "history"
mảng với $arrayElemAt
sử dụng chỉ mục "cuối cùng", là -1
.
Điều đó nên hợp lý vì rất có thể "bản sửa đổi gần đây nhất" trên thực tế là mục nhập mảng "cuối cùng". Chúng tôi có thể điều chỉnh điều này để tìm kiếm "lớn nhất", bằng cách áp dụng $max
như một điều kiện để $filter
. Vì vậy, giai đoạn đường ống đó trở thành:
{ "$addFields": {
"uploaded_files": {
"$map": {
"input": "$uploaded_files",
"as": "f",
"in": {
"latest": {
"$arrayElemAt": [
{ "$filter": {
"input": "$$f.history.revision",
"as": "h",
"cond": {
"$eq": [
"$$h",
{ "$max": "$$f.history.revision" }
]
}
}},
0
]
},
"_id": "$$f._id",
"display_name": "$$f.display_name"
}
}
}
}},
Điều này ít nhiều giống nhau, ngoại trừ việc chúng tôi so sánh với $max
giá trị và chỉ trả về "một" mục nhập từ mảng làm cho chỉ mục trả về từ mảng "đã lọc" ở vị trí "đầu tiên" hoặc 0
chỉ mục.
Đối với các Kỹ thuật chung khác khi sử dụng $lookup
thay cho .populate()
, hãy xem mục nhập của tôi trên "Truy vấn sau khi điền trong Mongoose"
trong đó nói thêm một chút về những thứ có thể được tối ưu hóa khi thực hiện phương pháp này.
Truy vấn qua điền
Tất nhiên, chúng ta có thể thực hiện (mặc dù không hiệu quả bằng) loại hoạt động tương tự bằng cách sử dụng .populate()
gọi và thao tác với các mảng kết quả:
Project.findOne({ "project_id": projectId })
.populate(populateQuery)
.lean()
.then(project => {
if (project === null)
reject(new createError.NotFound(req.path));
project.uploaded_files = project.uploaded_files.map( f => ({
latest: f.history.slice(-1)[0],
_id: f._id,
display_name: f.display_name
}));
resolve(project);
})
.catch(reject)
Tất nhiên, nơi bạn thực sự trả lại "tất cả" các mục từ "history"
, nhưng chúng tôi chỉ áp dụng .map()
để gọi .slice()
trên các phần tử đó để lấy lại phần tử mảng cuối cùng cho mỗi phần tử.
Chi phí nhiều hơn một chút vì tất cả lịch sử được trả về và .populate()
cuộc gọi là các yêu cầu bổ sung, nhưng nó nhận được cùng một kết quả cuối cùng.
Một điểm thiết kế
Vấn đề chính mà tôi thấy ở đây là bạn thậm chí có một mảng "lịch sử" trong nội dung. Đây thực sự không phải là một ý tưởng tuyệt vời vì bạn cần phải làm những việc như trên để chỉ trả lại mặt hàng có liên quan mà bạn muốn.
Vì vậy, là một "quan điểm của thiết kế", tôi sẽ không làm điều này. Nhưng thay vào đó, tôi sẽ "tách" lịch sử khỏi các mục trong mọi trường hợp. Giữ lại tài liệu "nhúng", tôi sẽ giữ "lịch sử" trong một mảng riêng biệt và chỉ giữ lại bản sửa đổi "mới nhất" với nội dung thực tế:
{
"_id" : ObjectId("5935a41f12f3fac949a5f925"),
"project_id" : 13,
"updated_at" : ISODate("2017-07-02T22:11:43.426Z"),
"created_at" : ISODate("2017-06-05T18:34:07.150Z"),
"owner" : ObjectId("591eea4439e1ce33b47e73c3"),
"name" : "Demo project",
"uploaded_files" : [
{
"latest" : {
{
"file" : ObjectId("59596f9fb6c89a031019bcae"),
"revision" : 1
}
},
"_id" : ObjectId("59596f9fb6c89a031019bcaf"),
"display_name" : "Example filename.txt"
}
]
"file_history": [
{
"_id": ObjectId("59596f9fb6c89a031019bcaf"),
"file": ObjectId("59596f9fb6c89a031019bcae"),
"revision": 0
},
{
"_id": ObjectId("59596f9fb6c89a031019bcaf"),
"file": ObjectId("59596f9fb6c89a031019bcae"),
"revision": 1
}
}
Bạn có thể duy trì điều này đơn giản bằng cách đặt $set
mục nhập có liên quan và sử dụng $push
trên "lịch sử" trong một thao tác:
.update(
{ "project_id": projectId, "uploaded_files._id": fileId }
{
"$set": {
"uploaded_files.$.latest": {
"file": revisionId,
"revision": revisionNum
}
},
"$push": {
"file_history": {
"_id": fileId,
"file": revisionId,
"revision": revisionNum
}
}
}
)
Với mảng được tách ra, bạn có thể chỉ cần truy vấn và luôn nhận được kết quả mới nhất, đồng thời loại bỏ "lịch sử" cho đến khi bạn thực sự muốn thực hiện yêu cầu đó:
Project.findOne({ "project_id": projectId })
.select('-file_history') // The '-' here removes the field from results
.populate(populateQuery)
Như một trường hợp chung, mặc dù tôi chỉ đơn giản là sẽ không bận tâm đến số "sửa đổi". Giữ nhiều cấu trúc giống nhau mà bạn không thực sự cần nó khi "nối" vào một mảng vì "mới nhất" luôn là "cuối cùng". Điều này cũng đúng với việc thay đổi cấu trúc, trong đó "mới nhất" sẽ luôn là mục nhập cuối cùng cho tệp đã tải lên.
Việc cố gắng duy trì một chỉ mục "nhân tạo" như vậy sẽ gặp phải nhiều vấn đề và hầu như làm hỏng mọi thay đổi của các hoạt động "nguyên tử" như được hiển thị trong .update()
ví dụ ở đây, vì bạn cần biết giá trị "bộ đếm" để cung cấp số sửa đổi mới nhất và do đó cần "đọc" giá trị đó từ đâu đó.