Explore o novo chatbot do Developer Center! O MongoDB AI chatbot pode ser acessado na parte superior da sua navegação para responder a todas as suas perguntas sobre o MongoDB .

Junte-se a nós no Amazon Web Services re:Invent 2024! Saiba como usar o MongoDB para casos de uso de AI .
Desenvolvedor do MongoDB
Central de desenvolvedor do MongoDBchevron-right
Produtoschevron-right
MongoDBchevron-right

Como manter várias versões de um registro no MongoDB (atualizações 2024)

John Page6 min read • Published Aug 12, 2024 • Updated Aug 12, 2024
Framework de agregaçãoMongoDB
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
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
2config.set("inspectDepth", 8) // Set mongosh to print nicely
3
4// Connect to a specific database
5db = db.getSiblingDB("version_example")
6db.data.drop()
7const nFields = 5
8
9// Function to generate random field values based on a specified change percentage
10function 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
21for (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
29console.log("ORIGINAL DATA")
30console.table(db.data.find().toArray())
(index)_idcampo_1campo_2campo_3campo_4dateUpdated
00344919742024-04-15T13:30:12.788Z
111394342024-04-15T13:30:12.836Z
22513096932024-04-15T13:30:12.849Z
33294421852024-04-15T13:30:12.860Z
4441351572024-04-15T13:30:12.866Z
5508556282024-04-15T13:30:12.874Z
66855624782024-04-15T13:30:12.883Z
77272396252024-04-15T13:30:12.895Z
88704040302024-04-15T13:30:12.905Z
9969131392024-04-15T13:30:12.914Z
Em seguida, modificamos os dados que registram o histórico como parte da operação de atualização.
1const oldTime = new Date()
2//We can make changes to these without history like so
3sleep(500);
4// Making the change and recording the OLD value
5for (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
24console.log("NEW DATA")
25db.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
3const filterHistory = { $filter: { input: "$history", cond: { $lt: ["$$this.dateUpdated", oldTime] } } }
4
5//Merge them together and replace the top level document
6
7const 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
11const revertPipeline = [{ $set: { rewoundTO: oldTime } }, applyChanges]
12
13//Show results
14db.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.
Iniciar a conversa

Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Relacionado
Tutorial

Construindo com padrões: o padrão Subset


Sep 23, 2022 | 3 min read
Tutorial

Procedimento para permitir que usuários "não raiz" parem/iniciem/reiniciem o processo "mongod"


May 16, 2022 | 3 min read
exemplo de código

GroupUs


Jul 07, 2022 | 1 min read
Tutorial

Dados da primavera desbloqueados: queries avançadas com MongoDB


Nov 08, 2024 | 7 min read