Como manter várias versões de um registro no MongoDB (atualizações 2024)
Avalie esse Tutorial
Ao longo dos anos, houve vários métodos propostos para o controle de versão de dados no MongoDB. O controle de versão de dados significa ser capaz de obter facilmente não apenas a versão mais recente de um documento ou documentos, mas também visualizar e consultar a forma como os documentos eram em um determinado ponto no tempo.
Houve a publicação do blog de Asya kamski escrita aproximadamente 10 anos atrás, uma atualização de Paulo feito (autor de Practical MongoDB Aggregations) e também informações no site da MongoDB sobre o padrão de versão de 2019.
Eles mantêm duas coleções distintas de dados – uma com a versão mais recente e outra com versões ou atualizações anteriores, permitindo reconstruí-las.
No entanto, desde então, houve mudanças sísmicas de baixo nível nos recursos de atualização e agregação do MongoDB. Aqui, mostrarei uma maneira relativamente simples de manter um histórico de documentos ao atualizar sem manter coleções adicionais.
Para fazer isso, usamos atualizações expressivas, também chamadas de atualizações do pipeline de agregação. Em vez de passar um objeto com operadores de atualização como segundo argumento para atualizar, coisas como $push e $set, expressamos nossa atualização como um pipeline de agregação, com um conjunto ordenado de alterações. Ao fazer isso, podemos não apenas fazer alterações, mas também pegar os valores anteriores de todos os campos que alteramos e registrá-los em um campo diferente como histórico.
O exemplo mais simples disso seria usar o seguinte como o parâmetro de atualização para uma operação updateOne.
1 [ { $set : { a: 5 , previous_a: "$a" } }]
Isso definiria explicitamente
a
como 5 , mas também definiria previous_a
como qualquer que fosse o a
antes da atualização. No entanto, isso só nos daria uma retrospectiva do histórico de uma única mudança.Antes de:
1 { 2 a: 3 3 }
Depois:
1 { 2 a: 5, 3 previous_a: 3 4 }
O que queremos fazer é pegar todos os campos que mudamos e construir um objeto com esses valores anteriores e, em seguida, empurrá-lo para uma array — teoricamente, assim:
1 [ { $set : { a: 5 , b: 8 } , 2 $push : { history : { a:"$a",b:"$b"} } ]
O acima não funciona porque a parte $push em negrito é um operador de atualização, não uma sintaxe de agregação, portanto, dá um erro de sintaxe. Em vez disso, o que precisamos fazer é reescrever o push como uma operação de array, da seguinte forma:
1 {"$set":{"history": 2 {"$concatArrays":[[{ _updateTime: "$$NOW", a:"$a",b:"$b"}}], 3 {"$ifNull":["$history",[]]}]}}}
Para explicar o que está acontecendo aqui, quero adicionar um objeto,
{ _updateTime: "$$NOW", a:"$a",b:"$b"}
, à matriz no início. Não posso usar $push, pois essa é uma sintaxe de atualização, e a sintaxe expressiva trata da geração de um documento com novas versões para os campos, ou seja, apenas $set. Portanto, preciso definir a matriz como a matriz anterior com um novo valor anexado.Usamos $concatArrays para unir duas arrays, então preparo meu único documento contendo os valores antigos para campos em uma array. Em seguida, a nova array é a minha array concatenada com a array antiga.
Eu uso $ifNUll para dizer se o valor anteriormente era nulo ou ausente, trate-o como uma array vazia, então da primeira vez, ele realmente faz
history = [{ _updateTime: "$$NOW", a:"$a",b:"$b"}] + []
.Antes de:
1 { 2 a: 3, 3 b: 1 4 }
Depois:
1 { 2 a: 5, 3 b: 8, 4 history: [ 5 { 6 _updateTime: Date(...), 7 a: 3, 8 b: 1 9 } 10 ] 11 }
É um pouco difícil de escrever, mas se escrevermos o código para demonstrar isso e declará-lo como objetos separados, ele ficará muito mais claro. Veja a seguir um script que você pode executar no shell do MongoDB colando-o ou carregando -o com
load("versioning.js")
.Este código primeiro gera alguns registros simples:
1 // Configure the inspection depth for better readability in output 2 config.set("inspectDepth", 8) // Set mongosh to print nicely 3 4 // Connect to a specific database 5 db = db.getSiblingDB("version_example") 6 db.data.drop() 7 const nFields = 5 8 9 // Function to generate random field values based on a specified change percentage 10 function randomFieldValues(percentageToChange) { 11 const fieldVals = new Object(); 12 for (let fldNo = 1; fldNo < nFields; fldNo++) { 13 if (Math.random() < (percentageToChange / 100)) { 14 fieldVals[`field_${fldNo}`] = Math.floor(Math.random() * 100) 15 } 16 } 17 return fieldVals 18 } 19 20 // Loop to create and insert 10 records with random data into the 'data' collection 21 for (let id = 0; id < 10; id++) { 22 const record = randomFieldValues(100) 23 record._id = id 24 record.dateUpdated = new Date() 25 db.data.insertOne(record) 26 } 27 28 // Log the message indicating the data that will be printed next 29 console.log("ORIGINAL DATA") 30 console.table(db.data.find().toArray())
(index) | _id | campo_1 | campo_2 | campo_3 | campo_4 | dateUpdated |
---|---|---|---|---|---|---|
0 | 0 | 34 | 49 | 19 | 74 | 2024-04-15T13:30:12.788Z |
1 | 1 | 13 | 9 | 43 | 4 | 2024-04-15T13:30:12.836Z |
2 | 2 | 51 | 30 | 96 | 93 | 2024-04-15T13:30:12.849Z |
3 | 3 | 29 | 44 | 21 | 85 | 2024-04-15T13:30:12.860Z |
4 | 4 | 41 | 35 | 15 | 7 | 2024-04-15T13:30:12.866Z |
5 | 5 | 0 | 85 | 56 | 28 | 2024-04-15T13:30:12.874Z |
6 | 6 | 85 | 56 | 24 | 78 | 2024-04-15T13:30:12.883Z |
7 | 7 | 27 | 23 | 96 | 25 | 2024-04-15T13:30:12.895Z |
8 | 8 | 70 | 40 | 40 | 30 | 2024-04-15T13:30:12.905Z |
9 | 9 | 69 | 13 | 13 | 9 | 2024-04-15T13:30:12.914Z |
Em seguida, modificamos os dados que registram o histórico como parte da operação de atualização.
1 const oldTime = new Date() 2 //We can make changes to these without history like so 3 sleep(500); 4 // Making the change and recording the OLD value 5 for (let id = 0; id < 10; id++) { 6 const newValues = randomFieldValues(30) 7 //Check if any changes 8 if (Object.keys(newValues).length) { 9 newValues.dateUpdated = new Date() 10 11 const previousValues = new Object() 12 for (let fieldName in newValues) { 13 previousValues[fieldName] = `$${fieldName}` 14 } 15 16 const existingHistory = { $ifNull: ["$history", []] } 17 const history = { $concatArrays: [[previousValues], existingHistory] } 18 newValues.history = history 19 20 db.data.updateOne({ _id: id }, [{ $set: newValues }]) 21 } 22 } 23 24 console.log("NEW DATA") 25 db.data.find().toArray()
Agora temos registros que se parecem com isso — com os valores atuais, mas também uma matriz refletindo quaisquer alterações.
1 { 2 _id: 6, 3 field_1: 85, 4 field_2: 3, 5 field_3: 71, 6 field_4: 71, 7 dateUpdated: ISODate('2024-04-15T13:34:31.915Z'), 8 history: [ 9 { 10 field_2: 56, 11 field_3: 24, 12 field_4: 78, 13 dateUpdated: ISODate('2024-04-15T13:30:12.883Z') 14 } 15 ] 16 }
Agora podemos usar um pipeline de agregação para recuperar qualquer versão anterior de cada documento. Para fazer isso, primeiro filtramos o histórico para incluir apenas as alterações até o ponto no tempo que queremos. Em seguida, os mesclamos em ordem:
1 //Get only history until point required 2 3 const filterHistory = { $filter: { input: "$history", cond: { $lt: ["$$this.dateUpdated", oldTime] } } } 4 5 //Merge them together and replace the top level document 6 7 const applyChanges = { $replaceRoot: { newRoot: { $mergeObjects: { $concatArrays: [["$$ROOT"], { $ifNull: [filterHistory, []] }] } } } } 8 9 // You can optionally add a $match here but you would normally be better to 10 // $match on the history fields at the start of the pipeline 11 const revertPipeline = [{ $set: { rewoundTO: oldTime } }, applyChanges] 12 13 //Show results 14 db.data.aggregate(revertPipeline).toArray()
1 { 2 _id: 6, 3 field_1: 85, 4 field_2: 56, 5 field_3: 24, 6 field_4: 78, 7 dateUpdated: ISODate('2024-04-15T13:30:12.883Z'), 8 history: [ 9 { 10 field_2: 56, 11 field_3: 24, 12 field_4: 78, 13 dateUpdated: ISODate('2024-04-15T13:30:12.883Z') 14 } 15 ], 16 rewoundTO: ISODate('2024-04-15T13:34:31.262Z') 17 },
Essa técnica surgiu através da discussão das necessidades de um cliente MongoDB. Eles tinham exatamente esse caso de uso para manter o histórico e o atual e para poder consultar e recuperar qualquer um deles sem precisar manter uma cópia completa do documento. É uma escolha ideal se as mudanças forem relativamente pequenas. Ele também pode ser adaptado para registrar apenas uma entrada de histórico se o valor do campo for diferente, o que permite computar deltas mesmo ao substituir todo o registro.
Como nota de advertência, o controle de versão dentro de um documento como este tornará os documentos maiores. Isso também significa uma variedade cada vez maior de edições. Se você acredita que pode haver centenas ou milhares de alterações, essa técnica não é adequada e o histórico deve ser gravado em um segundo documento usando uma transação. Para fazer isso, execute a atualização com findOneAndUpdate e retorne os campos que você está alterando dessa chamada para inserir em uma coleção de histórico.
Este não pretende ser um tutorial passo a passo, embora você possa tentar os exemplos acima e ver como funciona. É uma das muitas modelagem de dados sofisticada
técnicas que você pode usar para construir serviços de alto desempenho no MongoDB e MongoDB Atlas. Se você precisar de controle de versão de registro, poderá usar isso. Se não, então talvez passe um pouco mais de tempo vendo o que você pode criar com o aggregation pipeline, um mecanismo de processamento de dados completo para Turing que é executado junto com seus dados, economizando tempo e custo de fechá-los para o cliente processar. Saiba mais sobre aggregation.
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.