Получить последний поддокумент из массива
У меня есть массив. Я хотел бы выбрать объект с самым высоким revision
номер из моего history
массивы (множественное число).
Мой документ выглядит так (часто это будет больше, чем один объект в uploaded_files
):
{
"_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" : [
{
"history" : [
{
"file" : ObjectId("59596f9fb6c89a031019bcae"),
"revision" : 0
}
],
"_id" : ObjectId("59596f9fb6c89a031019bcaf")
"display_name" : "Example filename.txt"
}
]
}
Мой код, который выбирает документ:
function getProject(req, projectId) {
let populateQuery = [
{path: 'owner'},
{path: 'uploaded_files.history.file'}
]
return new Promise(function (resolve, reject) {
Project.findOne({ project_id: projectId }).populate(populateQuery).then((project) => {
if (!project)
reject(new createError.NotFound(req.path))
resolve(project)
}).catch(function (err) {
reject(err)
})
})
}
Как я могу выбрать документ, чтобы он выводил только объект с наибольшим номером ревизии из массивов истории?
1 ответ
Вы можете решить эту проблему несколькими способами. Конечно, они отличаются друг от друга подходом и производительностью, и я думаю, что есть некоторые более важные соображения, которые необходимо учитывать при разработке. В частности, здесь есть "потребность" в "изменениях" данных в шаблоне использования вашего реального приложения.
Запрос через агрегат
Что касается главной точки получения "последнего элемента из внутреннего массива", то вам действительно следует использовать .aggregate()
Операция для этого:
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)
})
}
Поскольку это оператор агрегации, где мы также можем выполнять "соединения" на "сервере", а не делать дополнительные запросы (вот что .populate()
на самом деле здесь) с помощью $lookup
Я позволю себе немного свободы с фактическими именами коллекций, так как ваша схема не включена в вопрос. Это нормально, поскольку вы не понимали, что на самом деле можете сделать это таким образом.
Конечно, "фактические" имена коллекций требуются серверу, который не имеет понятия определенной схемы "на стороне приложения". Здесь есть вещи, которые вы можете сделать для удобства, но об этом позже.
Вы также должны отметить, что в зависимости от того, где projectId
на самом деле происходит, то в отличие от обычных методов мангуста, таких как .find()
$match
потребует на самом деле "приведение" к ObjectId
если входное значение на самом деле является "строкой". Mongoose не может применять "типы схем" в конвейере агрегации, поэтому вам может потребоваться сделать это самостоятельно, особенно если projectId
пришел из параметра запроса:
{ "$match": { "project_id": Schema.Types.ObjectId(projectId) } },
Основная часть здесь, где мы используем $map
перебирать все "uploaded_files"
записи, а затем просто извлечь "последние" из "history"
массив с $arrayElemAt
используя "последний" индекс, который -1
,
Это должно быть разумно, поскольку наиболее вероятно, что "самая последняя редакция" на самом деле является "последней" записью массива. Мы могли бы адаптировать это, чтобы искать "самый большой", применяя $max
в качестве условия для $filter
, Таким образом, эта стадия трубопровода становится:
{ "$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"
}
}
}
}},
Что более или менее то же самое, за исключением того, что мы делаем сравнение с $max
значение и возвращает только "одну" запись из массива, что делает индекс для возврата из "отфильтрованного" массива "первой" позицией, или 0
индекс.
Что касается других общих методов использования $lookup
на месте .populate()
см. мою статью "Запросы после заполнения в Mongoose", в которой немного больше говорится о вещах, которые можно оптимизировать при использовании этого подхода.
Запрос через заполнить
Также, конечно, мы можем выполнять (хотя и не так эффективно) ту же операцию, используя .populate()
вызывает и манипулирует полученными массивами:
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)
Где, конечно, вы на самом деле возвращаете "все" предметы из "history"
, но мы просто применяем .map()
призвать .slice()
на эти элементы, чтобы снова получить последний элемент массива для каждого.
Немного больше накладных расходов, так как вся история возвращается, и .populate()
вызовы являются дополнительными запросами, но они дают те же конечные результаты.
Точка дизайна
Однако главная проблема, которую я вижу здесь, заключается в том, что у вас даже есть массив "history" внутри контента. Это не очень хорошая идея, так как вам нужно сделать что-то, как указано выше, чтобы вернуть только тот элемент, который вы хотите.
Так что в качестве "точки дизайна" я бы этого не делал. Но вместо этого я бы "отделил" историю от предметов во всех случаях. Сохраняя "встроенные" документы, я бы держал "историю" в отдельном массиве и сохранял только "самую последнюю" ревизию с фактическим содержанием:
{
"_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
}
}
Вы можете сохранить это, просто установив $set
соответствующая запись и использование $push
на "историю" за одну операцию:
.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
}
}
}
)
Разделяя массив, вы можете просто запросить и всегда получить последний, и отбросить "историю" до тех пор, пока вы действительно не захотите сделать этот запрос:
Project.findOne({ "project_id": projectId })
.select('-file_history') // The '-' here removes the field from results
.populate(populateQuery)
В общем случае, хотя я просто не стал бы беспокоиться с номером "ревизии" вообще. Сохраняя большую часть той же структуры, вы не нуждаетесь в ней при "добавлении" в массив, поскольку "последний" всегда является "последним". Это также верно для изменения структуры, где снова "последний" всегда будет последней записью для данного загруженного файла.
Попытка сохранить такой "искусственный" индекс чревата проблемами и, в основном, сводит на нет любое изменение "атомарных" операций, как показано в .update()
пример здесь, так как вам нужно знать значение "счетчика" для того, чтобы предоставить номер последней редакции, и, следовательно, нужно "прочитать" это откуда-то.