Docs Menu

Docs HomeDevelop ApplicationsMongoDB DriversJava

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>
Bson projection = Projections.excludeId();
Bson filter = Filters.eq("color", "green");
Bson update = Updates.set("food", "pizza");
FindOneAndUpdateOptions options = new FindOneAndUpdateOptions().
projection(projection).
upsert(true).
maxTime(5, TimeUnit.SECONDS);
/* The result variable contains your document in the
state before your update operation is performed. */
Document result = collection.findOneAndUpdate(filter, update, options);
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>
Bson filter = Filters.eq("color", "green");
Document replace = new Document("music", "classical").append("color", "green");
FindOneAndReplaceOptions options = new FindOneAndReplaceOptions().
returnDocument(ReturnDocument.AFTER);
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");
Bson filter = Filters.empty();
FindOneAndDeleteOptions options = new FindOneAndDeleteOptions().
sort(sort);
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() {
Bson filter = Filters.eq("reserved", false);
Document myRoom = this.collection.find(filter).first();
if (myRoom == null){
System.out.println("Sorry, we are booked " + this.guest);
return;
}
String myRoomName = myRoom.getString("room");
System.out.println("You got the " + myRoomName + " " + this.guest);
Bson update = Updates.combine(Updates.set("reserved", true), Updates.set("guest", guest));
Bson roomFilter = Filters.eq("_id", myRoom.get("_id", Integer.class));
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(){
Bson update = Updates.combine(Updates.set("reserved", true), Updates.set("guest", guest));
Bson filter = Filters.eq("reserved", false);
Document myRoom = this.collection.findOneAndUpdate(filter, update);
if (myRoom == null){
System.out.println("Sorry, we are booked " + this.guest);
return;
}
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 →