Como usar transações MongoDB em Node.js
Avalie esse Início rápido
Desenvolvedores que migram de bancos de dados relacionais para o MongoDB geralmente perguntam: " O MongoDB suporta transações ACID? Em caso afirmativo, como você cria uma transação? " A resposta para a primeira pergunta é " Sim!"
Começando em 4.0, o MongoDB adicionou suporte para transações ACID de vários documentose, começando em 4.2, O MongoDB adicionou suporte para transações ACID distribuídas. Se você não estiver familiarizado com o que são transações ACID ou se deve usá-las no MongoDB, confira minha postagem anterior sobre o assunto.
Esta publicação usa o MongoDB 4.0, Driver MongoDB Node.js 3.3.2 e Node.js 10.16.3.
Estamos na metade da série Início rápido com MongoDB e Node.js. Começamos explicando como se conectar ao MongoDB e executar cada uma das operações CRUD (Create, Read, Update e Delete). Em seguida , abordamos tópicos mais avançados , como a estrutura de agregação.
O código que escrevemos hoje usará a mesma estrutura do código que criamos na primeira publicação da série; então, se você tiver alguma dúvida sobre como começar ou como o código está estruturado, volte a essa primeira postagem.
Agora, vamos analisar a segunda pergunta que os desenvolvedores fazem — vamos descobrir como criar uma transação!
Quer ver transações em ação? Confira o vídeo abaixo! Ele abrange os mesmos tópicos sobre os quais você lerá neste artigo.
Comece hoje mesmo com um cluster M0 no Atlas. É gratuito para sempre e é a maneira mais fácil de experimentar as etapas desta série de blogs.
Como você já deve ter percebido ao trabalhar com o MongoDB, a maioria dos casos de uso não exige que você use transações com vários documentos. Quando você modela seus dados usando nossa regra prática Dados que são acessados juntos devem ser armazenados juntos, você descobrirá que raramente precisa usar uma transação de vários documentos. De fato, tive um pouco de dificuldade para pensar em um caso de uso para o conjunto de dados do Airbnb que exigisse uma transação com vários documentos.
Depois de refletir um pouco, criei um exemplo verossímil. Digamos que queremos permitir que os usuários criem reservas no
sample_airbnb database
.Poderemos começar criando uma collection chamada
users
. Queremos que os usuários possam visualizar facilmente suas reservas quando estiverem visualizando seus perfis, portanto, armazenaremos as reservas como documentos incorporados na coleçãousers
. Por exemplo, digamos que um usuário chamado leslie crie duas reservas. O documento dela na collectionusers
seria assim:1 { 2 "_id": {"$oid":"5dd589544f549efc1b0320a5"}, 3 "email": "leslie@example.com", 4 "name": "Leslie Yepp", 5 "reservations": [ 6 { 7 "name": "Infinite Views", 8 "dates": [ 9 {"$date": {"$numberLong":"1577750400000"}}, 10 {"$date": {"$numberLong":"1577836800000"}} 11 ], 12 "pricePerNight": {"$numberInt":"180"}, 13 "specialRequests": "Late checkout", 14 "breakfastIncluded": true 15 }, 16 { 17 "name": "Lovely Loft", 18 "dates": [ 19 {"$date": {"$numberLong": "1585958400000"}} 20 ], 21 "pricePerNight": {"$numberInt":"210"}, 22 "breakfastIncluded": false 23 } 24 ] 25 }
Ao pesquisar os anúncios do Airbnb, os usuários precisam saber se o anúncio já está reservado para as datas da viagem. Como resultado, queremos armazenar as datas em que o anúncio está reservado na coleção
listingsAndReviews
. Por exemplo, o anúncio "Infinite Views" que Leslie reservou deve ser atualizado para listar suas datas de reserva.1 { 2 "_id": {"$oid":"5dbc20f942073d6d4dabd730"}, 3 "name": "Infinite Views", 4 "summary": "Modern home with infinite views from the infinity pool", 5 "property_type": "House", 6 "bedrooms": {"$numberInt": "6"}, 7 "bathrooms": {"$numberDouble":"4.5"}, 8 "beds": {"$numberInt":"8"}, 9 "datesReserved": [ 10 {"$date": {"$numberLong": "1577750400000"}}, 11 {"$date": {"$numberLong": "1577836800000"}} 12 ] 13 }
Manter esses dois registros sincronizados é fundamental. Se tivéssemos que criar uma reserva em um documento da collection
users
sem atualizar o documento associado na collectionlistingsAndReviews
, nossos dados seriam inconsistentes. Podemos usar uma transação de vários documentos para garantir que ambas as atualizações sejam bem-sucedidas ou falhem juntas.Como em todas as publicações nesta série de Início Rápido do MongoDB e Node.js, você precisa garantir que concluiu as etapas de pré-requisitos descritas na seção Configuração da primeira publicação da série.
Observação: para utilizar transações, oMongoDB deve ser configurado como um conjunto de réplicas ou um cluster fragmentado. As transações não são suportadas em sistemas standalone. Se você estiver utilizando um banco de dados hospedado no Atlas, não precisará se preocupar com isso, pois cada Atlas cluster é um conjunto de réplicas ou um cluster fragmentado. Se você estiver hospedando sua própria implantação independente, siga estas instruções para converter sua instância em um conjunto de réplicas.
Estaremos usando o anúncio do Airbnb “Visualizações infinitas” que criamos em uma postagem anterior desta série. Volte para a postagem sobre Criação de documentos se seu banco de dados não tiver atualmente a listagem "Visualizações infinitas".
O conjunto de dados de amostra do Airbnb tem apenas a coleção
listingsAndReviews
por padrão. Para ajudá-lo a criar rapidamente a coleção e os dados necessários, escrevi usersCollection.js. Baixe uma cópia do arquivo, atualize a constanteuri
para refletir suas informações de conexão do Atlas e execute o script executando node usersCollection.js
. O script criará três novos usuários na collectionusers
:LeslieYepp,April Ludfence eTom Haverdodge. Se a coleçãousers
ainda não existir, o MongoDB a criará automaticamente para você quando você inserir os novos usuários. O script também cria um índice no campo email
na coleçãousers
. O índice exige que cada documento na coleçãousers
tenha um email
exclusivo.Agora que estamos configurados, vamos implementar a funcionalidade para armazenar as reservas do Airbnb.
Para facilitar o acompanhamento desta publicação no blog, criei um modelo inicial para um script do Node.js que acessa um cluster do Atlas.
- Abra
template.js
no seu editor de código favorito. - Atualize o URI de conexão para apontar para seu cluster do Atlas. Se não tiver certeza de como fazer isso, consulte a primeira publicação desta série.
- Salve o arquivo como
transaction.js
.
Você pode executar esse arquivo executando
node transaction.js
em seu shell. Nesse ponto, o arquivo simplesmente abre e fecha uma conexão com o cluster do Atlas, portanto, nenhuma saída é esperada. Se você vir DeprecationWarnings, poderá ignorá-los para os fins deste post.Vamos criar uma função auxiliar. Esta função gerará um documento de reserva que utilizaremos mais tarde.
- Cole a seguinte função em
transaction.js
:
1 function createReservationDocument(nameOfListing, reservationDates, reservationDetails) { 2 // Create the reservation 3 let reservation = { 4 name: nameOfListing, 5 dates: reservationDates, 6 } 7 8 // Add additional properties from reservationDetails to the reservation 9 for (let detail in reservationDetails) { 10 reservation[detail] = reservationDetails[detail]; 11 } 12 13 return reservation; 14 }
Para ter uma ideia do que essa função está fazendo, deixe-me mostrar um exemplo. Poderemos chamar essa função de dentro de
main()
:1 createReservationDocument("Infinite Views", 2 [new Date("2019-12-31"), new Date("2020-01-01")], 3 { pricePerNight: 180, specialRequests: "Late checkout", breakfastIncluded: true });
A função retornaria o seguinte:
1 { 2 name: 'Infinite Views', 3 dates: [ 2019-12-31T00:00:00.000Z, 2020-01-01T00:00:00.000Z ], 4 pricePerNight: 180, 5 specialRequests: 'Late checkout', 6 breakfastIncluded: true 7 }
Vamos criar uma função cuja tarefa seja criar a reserva no banco de dados.
- Continuando a trabalhar em
transaction.js
, crie uma função assíncrona chamadacreateReservation
. A função deve aceitar umMongoClient
, o endereço de e-mail do usuário, o nome do anúncio do Airbnb, as datas da reserva e quaisquer outros detalhes da reserva como parâmetros.1 async function createReservation(client, userEmail, nameOfListing, reservationDates, reservationDetails) { 2 } - Agora precisamos acessar as coleções que atualizaremos nesta função. Adicione o seguinte código a
createReservation()
.1 const usersCollection = client.db("sample_airbnb").collection("users"); 2 const listingsAndReviewsCollection = client.db("sample_airbnb").collection("listingsAndReviews"); - Vamos criar nosso documento de reserva chamando a função auxiliar que criamos na seção anterior. Cole o seguinte código em
createReservation()
.1 const reservation = createReservationDocument(nameOfListing, reservationDates, reservationDetails); - Cada transação e suas operações devem estar associadas a uma sessão. Abaixo do código existente em
createReservation()
, inicie uma sessão.1 const session = client.startSession(); - Podemos optar por definir opções para a transação. Não vamos entrar em detalhes sobre eles aqui. Você pode saber mais sobre essas opções na documentação do driver. Cole o seguinte abaixo do código existente em
createReservation()
.1 const transactionOptions = { 2 readPreference: 'primary', 3 readConcern: { level: 'local' }, 4 writeConcern: { w: 'majority' } 5 }; - Agora estamos prontos para começar a trabalhar com nossa transação. Abaixo do código existente em
createReservation()
, abra um blocotry { }
, siga-o com um blococatch { }
e termine com um blocofinally { }
.1 try { 2 3 } catch(e){ 4 5 } finally { 6 7 } - Podemos usar o withTransaction()do ClientSession para iniciar uma transação, executar uma função de retorno de chamada e confirmar (ou abortar em caso de erro) a transação.
withTransaction()
exige que passemos uma função que será executada dentro da transação. Adicione uma chamada parawithTransaction()
dentro detry { }
. Vamos começar passando uma função assíncrona anônima parawithTransaction()
.1 const transactionResults = await session.withTransaction(async () => {}, transactionOptions); - A função de retorno de chamada anônima que estamos passando para
withTransaction()
não faz nada no momento. Vamos começar a criar incrementalmente as operações de banco de dados que queremos chamar de dentro dessa função. Podemos começar adicionando uma reserva à matrizreservations
dentro do documentouser
apropriado . Cole o seguinte dentro da função anônima que está sendo passada parawithTransaction()
.1 const usersUpdateResults = await usersCollection.updateOne( 2 { email: userEmail }, 3 { $addToSet: { reservations: reservation } }, 4 { session }); 5 console.log(`${usersUpdateResults.matchedCount} document(s) found in the users collection with the email address ${userEmail}.`); 6 console.log(`${usersUpdateResults.modifiedCount} document(s) was/were updated to include the reservation.`); - Como queremos ter certeza de que um anúncio do Airbnb não está com reserva dupla em uma determinada data, devemos verificar se a data da reserva já está listada no array
datesReserved
do anúncio . Em caso afirmativo, devemos abortar a transação. Abortar a transação reverterá a atualização do documento do usuário que fizemos na etapa anterior. Cole o seguinte abaixo do código existente na função anônima.1 const isListingReservedResults = await listingsAndReviewsCollection.findOne( 2 { name: nameOfListing, datesReserved: { $in: reservationDates } }, 3 { session }); 4 if (isListingReservedResults) { 5 await session.abortTransaction(); 6 console.error("This listing is already reserved for at least one of the given dates. The reservation could not be created."); 7 console.error("Any operations that already occurred as part of this transaction will be rolled back."); 8 return; 9 } - A última coisa que queremos fazer dentro de nossa transação é adicionar as datas de reserva à array
datesReserved
na collectionlistingsAndReviews
. Cole o seguinte abaixo do código existente na função anônima.1 const listingsAndReviewsUpdateResults = await listingsAndReviewsCollection.updateOne( 2 { name: nameOfListing }, 3 { $addToSet: { datesReserved: { $each: reservationDates } } }, 4 { session }); 5 console.log(`${listingsAndReviewsUpdateResults.matchedCount} document(s) found in the listingsAndReviews collection with the name ${nameOfListing}.`); 6 console.log(`${listingsAndReviewsUpdateResults.modifiedCount} document(s) was/were updated to include the reservation dates.`); - Queremos saber se a transação foi bem-sucedida. Se
transactionResults
estiver definido, saberemos que a transação foi bem-sucedida. SetransactionResults
estiver indefinido, saberemos que o abortamos intencionalmente em nosso código. Abaixo da definição da constantetransactionResults
, cole o seguinte código.1 if (transactionResults) { 2 console.log("The reservation was successfully created."); 3 } else { 4 console.log("The transaction was intentionally aborted."); 5 } - Vamos registrar todos os erros gerados. Cole o seguinte dentro de
catch(e){ }
:1 console.log("The transaction was aborted due to an unexpected error: " + e); - Independentemente do que aconteça, precisamos encerrar nossa sessão. Cole o seguinte dentro de
finally { }
:1 await session.endSession(); Neste ponto, sua função deve ter a seguinte aparência:1 async function createReservation(client, userEmail, nameOfListing, reservationDates, reservationDetails) { 2 3 const usersCollection = client.db("sample_airbnb").collection("users"); 4 const listingsAndReviewsCollection = client.db("sample_airbnb").collection("listingsAndReviews"); 5 6 const reservation = createReservationDocument(nameOfListing, reservationDates, reservationDetails); 7 8 const session = client.startSession(); 9 10 const transactionOptions = { 11 readPreference: 'primary', 12 readConcern: { level: 'local' }, 13 writeConcern: { w: 'majority' } 14 }; 15 16 try { 17 const transactionResults = await session.withTransaction(async () => { 18 19 const usersUpdateResults = await usersCollection.updateOne( 20 { email: userEmail }, 21 { $addToSet: { reservations: reservation } }, 22 { session }); 23 console.log(`${usersUpdateResults.matchedCount} document(s) found in the users collection with the email address ${userEmail}.`); 24 console.log(`${usersUpdateResults.modifiedCount} document(s) was/were updated to include the reservation.`); 25 26 27 const isListingReservedResults = await listingsAndReviewsCollection.findOne( 28 { name: nameOfListing, datesReserved: { $in: reservationDates } }, 29 { session }); 30 if (isListingReservedResults) { 31 await session.abortTransaction(); 32 console.error("This listing is already reserved for at least one of the given dates. The reservation could not be created."); 33 console.error("Any operations that already occurred as part of this transaction will be rolled back."); 34 return; 35 } 36 37 const listingsAndReviewsUpdateResults = await listingsAndReviewsCollection.updateOne( 38 { name: nameOfListing }, 39 { $addToSet: { datesReserved: { $each: reservationDates } } }, 40 { session }); 41 console.log(`${listingsAndReviewsUpdateResults.matchedCount} document(s) found in the listingsAndReviews collection with the name ${nameOfListing}.`); 42 console.log(`${listingsAndReviewsUpdateResults.modifiedCount} document(s) was/were updated to include the reservation dates.`); 43 44 }, transactionOptions); 45 46 if (transactionResults) { 47 console.log("The reservation was successfully created."); 48 } else { 49 console.log("The transaction was intentionally aborted."); 50 } 51 } catch(e){ 52 console.log("The transaction was aborted due to an unexpected error: " + e); 53 } finally { 54 await session.endSession(); 55 } 56 57 }
Agora que escrevemos uma função que cria uma reserva usando uma transação, vamos testá-la! Vamos criar uma reserva para leslie na lista "infinitas visualizações" para as noite de 31 de dezembro de 2019 e 1 janeiro de 2020.
- Dentro de
main()
abaixo do comentário que dizMake the appropriate DB calls
, chame sua funçãocreateReservation()
:1 await createReservation(client, 2 "leslie@example.com", 3 "Infinite Views", 4 [new Date("2019-12-31"), new Date("2020-01-01")], 5 { pricePerNight: 180, specialRequests: "Late checkout", breakfastIncluded: true }); - Salve seu arquivo.
- Execute seu script executando
node transaction.js
em seu shell. - A seguinte saída será exibida em seu shell.
1 1 document(s) found in the users collection with the email address leslie@example.com. 2 1 document(s) was/were updated to include the reservation. 3 1 document(s) found in the listingsAndReviews collection with the name Infinite Views. 4 1 document(s) was/were updated to include the reservation dates. 5 The reservation was successfully created. O documento de leslie na collectionusers
agora contém a reserva.1 { 2 "_id": {"$oid":"5dd68bd03712fe11bebfab0c"}, 3 "email": "leslie@example.com", 4 "name": "Leslie Yepp", 5 "reservations": [ 6 { 7 "name": "Infinite Views", 8 "dates": [ 9 {"$date": {"$numberLong":"1577750400000"}}, 10 {"$date": {"$numberLong":"1577836800000"}} 11 ], 12 "pricePerNight": {"$numberInt":"180"}, 13 "specialRequests": "Late checkout", 14 "breakfastIncluded": true 15 } 16 ] 17 } A listagem "Infinite Views" nalistingsAndReviews
collectionagora contém as datas de reserva.1 { 2 "_id": {"$oid": "5dbc20f942073d6d4dabd730"}, 3 "name": "Infinite Views", 4 "summary": "Modern home with infinite views from the infinity pool", 5 "property_type": "House", 6 "bedrooms": {"$numberInt":"6"}, 7 "bathrooms": {"$numberDouble":"4.5"}, 8 "beds": {"$numberInt":"8"}, 9 "datesReserved": [ 10 {"$date": {"$numberLong": "1577750400000"}}, 11 {"$date": {"$numberLong": "1577836800000"}} 12 ] 13 }
Hoje, implementamos uma transação multidocumento. As transações são realmente úteis quando você precisa fazer alterações em mais de um documento como uma operação do tipo "tudo ou nada".
Certifique-se de usar as write concerns corretas ao criar uma transação. Consulte a documentação do MongoDB para obter mais informações.
Quando você usa bancos de dados relacionais, os dados relacionados geralmente são divididos entre tabelas diferentes em um esforço para normalizar os dados. Como resultado, o uso de transações é bastante comum.
Quando você usa o MongoDB, os dados acessados juntos devem ser armazenados juntos. Ao modelar seus dados dessa maneira, você provavelmente descobrirá que raramente precisa usar transações.
Esta postagem incluiu muitos trechos de código que se basearam no código gravado na primeira postagem desta série de Início Rápido do MongoDB e do Node.js. Para obter uma cópia completa do código usado na postagem de hoje, visite o Repositório GitHub do Node.js Quick Start.
Agora você está pronto para tentar change stream e gatilhos. Confira o próximo post desta série para saber mais!
Questões? Comentários? Queremos muito nos conectar com você. Participe da conversa nos fóruns da MongoDB Community.