Docs Menu

Docs HomeDevelop ApplicationsMongoDB DriversJava Sync

Compound Operations

On this page

  • Overview
  • How to Use Compound Operations
  • Find and Update
  • Find and Replace
  • Find and Delete
  • Avoiding a Race Condition
  • Example With Race Condition
  • Example Without Race Condition

In this guide, you can learn how to perform compound operations with the MongoDB Java driver.

Compound operations consist of a read and write operation performed as one atomic operation. An atomic operation is an operation which either completes entirely, or does not complete at all. Atomic operations cannot partially complete.

Atomic operations can help you avoid race conditions in your code. A race condition occurs when your code's behavior is dependent on the order of uncontrollable events.

MongoDB supports the following compound operations:

  • Find and update one document

  • Find and replace one document

  • Find and delete one document

If you need to perform more complex tasks atomically, such as reading and writing to more than one document, use transactions. Transactions are a feature of MongoDB and other databases that lets you define an arbitrary sequence of database commands as an atomic operation.

For more information on atomic operations and atomicity, see the MongoDB manual entry for atomicity and transactions.

For more information on transactions, see the MongoDB manual entry for transactions.

This section shows how to use each compound operation with the MongoDB Java Driver.

The following examples use a collection containing these two sample documents.

{"_id": 1, "food": "donut", "color": "green"}
{"_id": 2, "food": "pear", "color": "yellow"}

The full code for the following examples is available on Github here.

Note

Before or After the Write?

By default, each compound operation returns your found document in the state before your write operation. You can retrieve your found document in the state after your write operation by using the options class corresponding to your compound operation. You can see an example of this configuration in the Find and Replace example below.

To find and update one document, use the findOneAndUpdate() method of the MongoCollection class. The findOneAndUpdate() method returns your found document or null if no documents match your query.

The following example uses the findOneAndUpdate() method to find a document with the color field set to "green" and update the food field in that document to "pizza".

The example also uses a FindOneAndUpdateOptions instance to specify the following options:

  • Exclude the _id field from the found document with a projection.

  • Specify an upsert, which inserts the document specified by the query filter if no documents match the query.

  • Set a maximum execution time of 5 seconds for this operation on the MongoDB instance. If the operation takes longer, the findOneAndUpdate() method will throw a MongoExecutionTimeoutException.

// <MongoCollection set up code here>
// Creates a projection to exclude the "_id" field from the retrieved documents
Bson projection = Projections.excludeId();
// Creates a filter to match documents with a "color" value of "green"
Bson filter = Filters.eq("color", "green");
// Creates an update document to set the value of "food" to "pizza"
Bson update = Updates.set("food", "pizza");
// Defines options that specify projected fields, permit an upsert and limit execution time
FindOneAndUpdateOptions options = new FindOneAndUpdateOptions().
projection(projection).
upsert(true).
maxTime(5, TimeUnit.SECONDS);
// Updates the first matching document with the content of the update document, applying the specified options
Document result = collection.findOneAndUpdate(filter, update, options);
// Prints the matched document in its state before the operation
System.out.println(result.toJson());

The output of the preceding code should look like this:

{"food": "pizza", "color": "green"}

For more information on the Projections class, see our guide on the Projections builder.

For more information on the upsert operation, see our guide on upserts.

For more information about the methods and classes mentioned in this section, see the following API Documentation:

To find and replace one document, use the findOneAndReplace() method of the MongoCollection class. The findOneAndReplace() method returns your found document or null if no documents match your query.

The following example uses the findOneAndReplace() method to find a document with the color field set to "green" and replace it with the following document:

{"music": "classical", "color": "green"}

The example also uses a FindOneAndReplaceOptions instance to specify that the returned document should be in the state after our replace operation.

// <MongoCollection set up code here>
// Creates instructions to replace the matching document with a new document
Bson filter = Filters.eq("color", "green");
Document replace = new Document("music", "classical").append("color", "green");
// Defines options specifying that the operation should return a document in its post-operation state
FindOneAndReplaceOptions options = new FindOneAndReplaceOptions().
returnDocument(ReturnDocument.AFTER);
// Atomically finds and replaces the matching document and prints the replacement document
Document result = collection.findOneAndReplace(filter, replace, options);
System.out.println(result.toJson());

The output of the preceding code should look like this:

{"_id": 1, "music": "classical", "color": "green"}

For more information about the methods and classes mentioned in this section, see the following API Documentation:

To find and delete one document, use the findOneAndDelete() method of the MongoCollection class. The findOneAndDelete() method returns your found document or null if no documents match your query.

The following example uses the findOneAndDelete() method to find and delete the document with the largest value in the _id field.

The example uses a FindOneAndDeleteOptions instance to specify a descending sort on the _id field.

// <MongoCollection set up code here>
Bson sort = Sorts.descending("_id");
// Creates an empty filter to match all documents in the collection
Bson filter = Filters.empty();
// Defines options that specify a descending sort on the "_id" field
FindOneAndDeleteOptions options = new FindOneAndDeleteOptions().
sort(sort);
// Deletes the document containing the highest "_id" value and prints the deleted document
Document result = collection.findOneAndDelete(filter, options);
System.out.println(result.toJson());

The output of the preceding code should look like this:

{"_id": 2, "food": "pear", "color": "yellow"}

For more information on the Sorts class, see our guide on the Sorts builder.

For more information about the methods and classes mentioned in this section, see the following API Documentation:

In this section we explore two examples. The first example contains a race condition, the second example uses a compound operation to avoid the race condition present in the first example.

For both examples, let's imagine that we run a hotel with one room and that we have a small Java program to help us checkout this room to a guest.

The following document in MongoDB represents the room:

{"_id": 1, "guest": null, "room": "Blue Room", "reserved": false}

The full code for this example is available on Github here.

Let's say our app uses this bookARoom method to checkout our room to a guest:

public void bookARoom() {
// Creates a filter to match documents representing available rooms
Bson filter = Filters.eq("reserved", false);
// Retrieves a document that represents the first available room
Document myRoom = this.collection.find(filter).first();
// Prints a message if no documents representing available rooms are found
if (myRoom == null){
System.out.println("Sorry, we are booked " + this.guest);
return;
}
String myRoomName = myRoom.getString("room");
// Prints a message that guest that successfully booked the room
System.out.println("You got the " + myRoomName + " " + this.guest);
// Creates an update document to mark a room as reserved
Bson update = Updates.combine(Updates.set("reserved", true), Updates.set("guest", guest));
// Creates a filter that matches the "_id" field of the first available room
Bson roomFilter = Filters.eq("_id", myRoom.get("_id", Integer.class));
// Updates the first matching document to mark it as reserved
this.collection.updateOne(roomFilter, update);
}

Imagine two separate guests, Jan and Pat, try to book the room with this method at the same time.

Jan sees this output:

You got the Blue Room Jan

And Pat sees this output:

You got the Blue Room Pat

When we look at our database, we see the following:

{"_id": 1, "guest": "Jan", "room": "Blue Room", "reserved": true}

Pat will be unhappy. When Pat shows up to our hotel, Jan will be occupying her room. What went wrong?

Here is the sequence of events that happened from the perspective of our MongoDB instance:

  • Find and return an empty room for Jan

  • Find and return an empty room for Pat

  • Update the room to booked for Pat

  • Update the room to booked for Jan

Notice that for a brief moment Pat had reserved the room, but as Jan's update operation was the last to execute our document has "Jan" as the guest.

Let's use a compound operation to avoid the race condition and always give our users the correct message.

public void bookARoom(){
// Creates an update document to mark a room as reserved
Bson update = Updates.combine(Updates.set("reserved", true), Updates.set("guest", guest));
// Creates a filter to match a document representing an available room
Bson filter = Filters.eq("reserved", false);
// Updates the first document that matches the filter to mark it as reserved
Document myRoom = this.collection.findOneAndUpdate(filter, update);
// Prints a message when there are no available rooms
if (myRoom == null){
System.out.println("Sorry, we are booked " + this.guest);
return;
}
// Prints the name of the guest that successfully booked the room
String myRoomName = myRoom.getString("room");
System.out.println("You got the " + myRoomName + " " + this.guest);
}

Imagine two separate guests, Jan and Pat, try to book the room with this method at the same time.

Jan sees this output:

You got the Blue Room Jan

And Pat sees this output:

Sorry, we are booked Pat

When we look at our database, we see the following:

{"_id":1, "guest":"Jan", "room":"Blue Room", "reserved":true}

Pat got the correct message. While she might be sad she didn't get the reservation, at least she knows not to travel to our hotel.

Here is the sequence of events that happened from the perspective of our MongoDB instance:

  • Find an empty room for Jan and reserve it.

  • Try to find an empty room for Pat and reserve it. As there are not any rooms left, return null.

Important

Write Lock

Your MongoDB instance places a write lock on the document you are modifying for the duration of your compound operation.

For information on the Updates class, see our guide on the Updates builder.

For more information of the Filters class, see our guide on the Filters builder.

For more information on the findOneAndUpdate() method, see the API Documentation for the MongoCollection class.

←  Specify a QueryBuilders →