Navigation

Transactions

Overview

For situations that require atomicity of reads and writes to multiple documents (in one or more collections), MongoDB supports multi-document transactions. The code snippets on this page demonstrate how to execute multiple MongoDB queries in a single transaction that will undo changes in the event of an error.

Snippet Setup

To use a code snippet in a function, you must first instantiate a MongoDB collection handle:

exports = function() {
  const mongodb = context.services.get("mongodb-atlas");
  const itemsCollection = mongodb.db("store").collection("items");
  const purchasesCollection = mongodb.db("store").collection("purchases");
  // ... paste snippet here ...
}

Example

To perform a transaction:

  1. Obtain and start a client session with client.startSession().
  2. (Optional) Define a read and write settings object for the transaction. Pass this object to the method called in the next step.
  3. Use the session.withTransaction() method to start a transaction.
  4. Execute the MongoDB queries that you would like to include in your transaction in the callback for this function, which needs to either be async or return a Promise. Be sure to pass the session to each query to ensure that it is included in the transaction.
  5. If the callback encounters an error, you should abort the transaction with session.abortTransaction() in a Promise rejection handler with Promise.catch() or a try ... catch exception handler.
  6. When the transaction is complete, call session.endSession() to end the session and free resources.

The following example creates two users, “henry” and “michelle”, and a uses a transaction to move “browniePoints” between those users atomically:

exports = function () {
  const client = context.services.get("mongodb-atlas");

  db = client.db("exampleDatabase");

  accounts = db.collection("accounts");
  browniePointsTrades = db.collection("browniePointsTrades");

  // create user accounts with initial balances
  accounts.insertOne({ name: "henry", browniePoints: 42 });
  accounts.insertOne({ name: "michelle", browniePoints: 144 });

  // trade points between user accounts in a transaction
  tradeBrowniePoints(
    client,
    accounts,
    browniePointsTrades,
    "michelle",
    "henry",
    5
  );

  return "Successfully traded brownie points.";
};

async function tradeBrowniePoints(
  client,
  accounts,
  browniePointsTrades,
  userAddPoints,
  userSubtractPoints,
  pointVolume
) {
  // Step 1: Start a Client Session
  const session = client.startSession();

  // Step 2: Optional. Define options to use for the transaction
  const transactionOptions = {
    readPreference: "primary",
    readConcern: { level: "local" },
    writeConcern: { w: "majority" },
  };

  // Step 3: Use withTransaction to start a transaction, execute the callback, and commit (or abort on error)
  // Note: The callback for withTransaction MUST be async and/or return a Promise.
  try {
    await session.withTransaction(async () => {
      // Step 4: Execute the queries you would like to include in one atomic transaction
      // Important:: You must pass the session to the operations
      await accounts.updateOne(
        { name: userSubtractPoints },
        { $inc: { browniePoints: -1 * pointVolume } },
        { session }
      );
      await accounts.updateOne(
        { name: userAddPoints },
        { $inc: { browniePoints: pointVolume } },
        { session }
      );
      await browniePointsTrades.insertOne(
        {
          userAddPoints: userAddPoints,
          userSubtractPoints: userSubtractPoints,
          pointVolume: pointVolume,
        },
        { session }
      );
    }, transactionOptions);
  } catch (err) {
    // Step 5: Handle errors with a transaction abort
    await session.abortTransaction();
  } finally {
    // Step 6: End the session when you complete the transaction
    await session.endSession();
  }
}