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
Idiomaschevron-right
JavaScriptchevron-right

Como usar transações MongoDB em Node.js

Lauren Schaefer10 min read • Published Feb 04, 2022 • Updated Aug 24, 2023
Node.jsMongoDBJavaScript
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Início rápido
star-empty
star-empty
star-empty
star-empty
star-empty
Logotipo do Início rápido em Node.js
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!
Você está usando ACID?
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.

Criando uma reserva Airbnb

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çãolistingsAndReviews. 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 collectionusers 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.

Configurar

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çãolistingsAndReviews 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 emailexclusivo.

Criar uma transação no Node.js

Agora que estamos configurados, vamos implementar a funcionalidade para armazenar as reservas do Airbnb.

Obter uma cópia do modelo Node.js

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.
  1. Baixe uma cópia de template.js.
  2. Abra template.js no seu editor de código favorito.
  3. 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.
  4. 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.

Criar uma função de auxiliar

Vamos criar uma função auxiliar. Esta função gerará um documento de reserva que utilizaremos mais tarde.
  1. Cole a seguinte função em transaction.js:
1function 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():
1createReservationDocument("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}

Crie uma função para a transação

Vamos criar uma função cuja tarefa seja criar a reserva no banco de dados.
  1. Continuando a trabalhar em transaction.js, crie uma função assíncrona chamada createReservation. 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.
    1async function createReservation(client, userEmail, nameOfListing, reservationDates, reservationDetails) {
    2}
  2. Agora precisamos acessar as coleções que atualizaremos nesta função. Adicione o seguinte código a createReservation().
    1const usersCollection = client.db("sample_airbnb").collection("users");
    2const listingsAndReviewsCollection = client.db("sample_airbnb").collection("listingsAndReviews");
  3. Vamos criar nosso documento de reserva chamando a função auxiliar que criamos na seção anterior. Cole o seguinte código em createReservation().
    1const reservation = createReservationDocument(nameOfListing, reservationDates, reservationDetails);
  4. 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.
    1const session = client.startSession();
  5. 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().
    1const transactionOptions = {
    2 readPreference: 'primary',
    3 readConcern: { level: 'local' },
    4 writeConcern: { w: 'majority' }
    5};
  6. 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 { }.
    1try {
    2
    3} catch(e){
    4
    5} finally {
    6
    7}
  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 para withTransaction() dentro de try { } . Vamos começar passando uma função assíncrona anônima para withTransaction().
    1const transactionResults = await session.withTransaction(async () => {}, transactionOptions);
  8. A função de retorno de chamada anônima que estamos passando parawithTransaction() 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 à matrizreservationsdentro do documentouserapropriado . Cole o seguinte dentro da função anônima que está sendo passada para withTransaction().
    1const usersUpdateResults = await usersCollection.updateOne(
    2 { email: userEmail },
    3 { $addToSet: { reservations: reservation } },
    4 { session });
    5console.log(`${usersUpdateResults.matchedCount} document(s) found in the users collection with the email address ${userEmail}.`);
    6console.log(`${usersUpdateResults.modifiedCount} document(s) was/were updated to include the reservation.`);
  9. 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 arraydatesReserveddo 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.
    1const isListingReservedResults = await listingsAndReviewsCollection.findOne(
    2 { name: nameOfListing, datesReserved: { $in: reservationDates } },
    3 { session });
    4if (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}
  10. A última coisa que queremos fazer dentro de nossa transação é adicionar as datas de reserva à array datesReservedna collectionlistingsAndReviews. Cole o seguinte abaixo do código existente na função anônima.
    1const listingsAndReviewsUpdateResults = await listingsAndReviewsCollection.updateOne(
    2 { name: nameOfListing },
    3 { $addToSet: { datesReserved: { $each: reservationDates } } },
    4 { session });
    5console.log(`${listingsAndReviewsUpdateResults.matchedCount} document(s) found in the listingsAndReviews collection with the name ${nameOfListing}.`);
    6console.log(`${listingsAndReviewsUpdateResults.modifiedCount} document(s) was/were updated to include the reservation dates.`);
  11. Queremos saber se a transação foi bem-sucedida. Se transactionResults estiver definido, saberemos que a transação foi bem-sucedida. Se transactionResults estiver indefinido, saberemos que o abortamos intencionalmente em nosso código. Abaixo da definição da constantetransactionResults, cole o seguinte código.
    1if (transactionResults) {
    2 console.log("The reservation was successfully created.");
    3} else {
    4 console.log("The transaction was intentionally aborted.");
    5}
  12. Vamos registrar todos os erros gerados. Cole o seguinte dentro de catch(e){ }:
    1console.log("The transaction was aborted due to an unexpected error: " + e);
  13. Independentemente do que aconteça, precisamos encerrar nossa sessão. Cole o seguinte dentro de finally { }:
    1await session.endSession();
    Neste ponto, sua função deve ter a seguinte aparência:
    1async 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}

Chamar a função

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.
  1. Dentro de main() abaixo do comentário que diz Make the appropriate DB calls, chame sua função createReservation():
    1await 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 });
  2. Salve seu arquivo.
  3. Execute seu script executando node transaction.js em seu shell.
  4. A seguinte saída será exibida em seu shell.
    11 document(s) found in the users collection with the email address leslie@example.com.
    21 document(s) was/were updated to include the reservation.
    31 document(s) found in the listingsAndReviews collection with the name Infinite Views.
    41 document(s) was/were updated to include the reservation dates.
    5The 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" na listingsAndReviews 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}

Encerrando

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.

Recursos adicionais


Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Início rápido
star-empty
star-empty
star-empty
star-empty
star-empty
Relacionado
Tutorial

Consulta flexível com Atlas Search


Jul 12, 2024 | 3 min read
Tutorial

Múltiplas conexões MongoDB em um único aplicativo


Apr 02, 2024 | 10 min read
Tutorial

Criar um backend de gerenciamento de mídia escalável: integrando Node.js, Armazenamento de blobs Azure e MongoDB


Nov 05, 2024 | 10 min read
Tutorial

Crie um blog moderno com Gatsby e MongoDB


Apr 02, 2024 | 16 min read
Sumário