Você pode resolver isso de duas maneiras diferentes. Eles variam em abordagem e desempenho, é claro, e acho que há algumas considerações maiores que você precisa fazer em seu design. Mais notavelmente aqui está a "necessidade" de dados de "revisões" no padrão de uso de seu aplicativo real.
Consulta via agregação
Quanto ao ponto principal de obter o "último elemento do array interno", você realmente deveria usar um
.aggregate()
operação para fazer isso: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)
})
}
Como esta é uma declaração de agregação onde também podemos fazer as "junções" no "servidor" em vez de fazer solicitações adicionais (que é o que
.populate()
realmente faz aqui ) usando $lookup
, estou tomando alguma liberdade com os nomes reais da coleção, pois seu esquema não está incluído na pergunta. Tudo bem, já que você não percebeu que poderia de fato fazê-lo dessa maneira. É claro que os nomes de coleção "reais" são exigidos pelo servidor, que não tem nenhum conceito do esquema definido do "lado do aplicativo". Há coisas que você pode fazer por conveniência aqui, mas mais sobre isso mais tarde.
Você também deve observar que, dependendo de onde
projectId
realmente vem, ao contrário dos métodos regulares do mangusto, como .find()
o $match
exigirá realmente "casting" para um ObjectId
se o valor de entrada for de fato uma "string". O Mongoose não pode aplicar "tipos de esquema" em um pipeline de agregação, portanto, talvez seja necessário fazer isso sozinho, especialmente se projectId
veio de um parâmetro de solicitação: { "$match": { "project_id": Schema.Types.ObjectId(projectId) } },
A parte básica aqui é onde usamos
$map
para percorrer todos os "uploaded_files"
entradas e, em seguida, simplesmente extraia o "mais recente" do "history"
array com $arrayElemAt
usando o índice "last", que é -1
. Isso deve ser razoável, pois é mais provável que a "revisão mais recente" seja de fato a "última" entrada da matriz. Poderíamos adaptar isso para procurar o "maior", aplicando
$max
como condição para $filter
. Então, esse estágio de pipeline se torna: { "$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"
}
}
}
}},
O que é mais ou menos a mesma coisa, exceto que fazemos a comparação com o
$max
valor e retornar apenas "um" entrada do array fazendo com que o índice retorne do array "filtrado" a posição "primeira", ou 0
índice. Quanto a outras técnicas gerais sobre o uso de
$lookup
no lugar de .populate()
, veja minha entrada em "Consultando após preencher no Mongoose"
que fala um pouco mais sobre coisas que podem ser otimizadas ao adotar essa abordagem. Consulta por meio de preenchimento
Também é claro que podemos fazer (mesmo que não tão eficientemente) o mesmo tipo de operação usando
.populate()
chamadas e manipulando os arrays resultantes: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)
Onde, é claro, você está retornando "todos" os itens de
"history"
, mas simplesmente aplicamos um .map ()
para invocar o .slice()
nesses elementos para obter novamente o último elemento da matriz para cada um. Um pouco mais de sobrecarga, pois todo o histórico é retornado e o
.populate()
chamadas são solicitações adicionais, mas obtém os mesmos resultados finais. Um ponto de design
O principal problema que vejo aqui é que você ainda tem uma matriz "histórico" dentro do conteúdo. Esta não é realmente uma boa ideia, pois você precisa fazer as coisas acima para retornar apenas o item relevante que deseja.
Então, como um "ponto de design", eu não faria isso. Mas, em vez disso, eu "separaria" o histórico dos itens em todos os casos. Mantendo os documentos "incorporados", eu manteria o "histórico" em uma matriz separada e manteria apenas a revisão "mais recente" com o conteúdo real:
{
"_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
}
}
Você pode manter isso simplesmente configurando
$set
a entrada relevante e usando $push
no "histórico" em uma operação:.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
}
}
}
)
Com o array separado, você pode simplesmente consultar e sempre obter o mais recente e descartar o "histórico" até o momento em que você realmente deseja fazer essa solicitação:
Project.findOne({ "project_id": projectId })
.select('-file_history') // The '-' here removes the field from results
.populate(populateQuery)
Como um caso geral, eu simplesmente não me incomodaria com o número de "revisão". Mantendo a mesma estrutura, você realmente não precisa disso ao "anexar" a uma matriz, pois o "mais recente" é sempre o "último". Isso também é verdade para alterar a estrutura, onde novamente o "mais recente" sempre será a última entrada para o arquivo enviado.
Tentar manter um índice tão "artificial" é cheio de problemas, e principalmente arruina qualquer mudança de operações "atômicas" como mostrado no
.update()
exemplo aqui, já que você precisa saber um valor de "contador" para fornecer o número de revisão mais recente e, portanto, precisa "ler" isso de algum lugar.