For AI agents: a documentation index is available at https://www.mongodb.com/docs/llms.txt — markdown versions of all pages are available by appending .md to any URL path.
Docs Menu

Compound Operations

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

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

Atomic operations can help you avoid race conditions in your code. A race condition occurs when your code's behavior depends 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

For more information about atomic operations and atomicity, see Atomicity and Transactions in the MongoDB Server manual.

Tip

Transactions

If you need to perform more complex tasks atomically, such as reading and writing to more than one document, use transactions. Transactions let you execute a sequence of database commands as an atomic operation. For more information about transactions, see Transacations in the MongoDB Server manual.

This section shows how to use the Scala driver to perform compound operations.

The following examples use a collection containing two sample documents:

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

The examples use the following collection reference to map each document to an instance of the Document class:

val database: MongoDatabase = mongoClient.getDatabase("compound_operations")
val collection: MongoCollection[Document] = database.getCollection("example")
val hotelCollection: MongoCollection[Document] = database.getCollection("rooms")

Each compound operation returns a SingleObservable[Document] that emits the matched document, or completes without emitting a value if no documents match the query filter. If the query filter matches multiple documents, the method acts on the first match, where first refers to natural order on disk unless you specify a sort order in the corresponding options object.

Note

Return Documents after Write Operation

By default, a compound operation returns the matching document, and then performs the write operation on it. You can direct the compound operation to perform the write operation first by passing an options object to the corresponding method. The Find and Replace example shows this configuration.

To find and update one document, use the findOneAndUpdate() method of the MongoCollection class.

The following example uses the findOneAndUpdate() method to find a document where the color field has the value "green". Then, it updates the food field in that document to "pizza".

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

  • 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 emits a MongoExecutionTimeoutException through the observer's onError() callback.

val filter = equal("color", "green")
val update = set("food", "pizza")
val options = new FindOneAndUpdateOptions()
.upsert(true)
.maxTime(5, TimeUnit.SECONDS)
val observable: SingleObservable[Document] =
collection.findOneAndUpdate(filter, update, options)
observable.subscribe(new Observer[Document] {
override def onNext(doc: Document): Unit = println(doc.toJson())
override def onError(e: Throwable): Unit = println(s"Failed: ${e.getMessage}")
override def onComplete(): Unit = println("Completed")
})
{"_id": {"$oid": "1"}, "color": "green", "food": "pizza"}
Completed

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

To find and replace one document, use the findOneAndReplace() method of the MongoCollection class.

The following example uses the findOneAndReplace() method to find a document where the color field has the value "green". It replaces the matching document with

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

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

val replaceFilter = equal("color", "green")
val replacement = Document(
"music" -> "classical",
"color" -> "green"
)
val replaceOptions = new FindOneAndReplaceOptions()
.returnDocument(ReturnDocument.AFTER)
val replaceObservable: SingleObservable[Document] =
collection.findOneAndReplace(replaceFilter, replacement, replaceOptions)
replaceObservable.subscribe(new Observer[Document] {
override def onNext(doc: Document): Unit = println(doc.toJson())
override def onError(e: Throwable): Unit = println(s"Failed: ${e.getMessage}")
override def onComplete(): Unit = println("Completed")
})
{"_id": {"$oid": "1"}, "music": "classical", "color": "green"}
Completed

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

To find and delete one document, use the findOneAndDelete() method of the MongoCollection class.

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

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

val deleteOptions = new FindOneAndDeleteOptions()
.sort(descending("_id"))
val deleteObservable: SingleObservable[Document] =
collection.findOneAndDelete(empty(), deleteOptions)
deleteObservable.subscribe(new Observer[Document] {
override def onNext(doc: Document): Unit = println(doc.toJson())
override def onError(e: Throwable): Unit = println(s"Failed: ${e.getMessage}")
override def onComplete(): Unit = println("Completed")
})
{"_id": {"$oid": "2"}, "food": "pear", "color": "yellow"}
Completed

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

By performing multiple operations atomically, compound operations help you avoid race conditions.

For the examples in this section, imagine that you run a hotel with one room. You use a small Scala program to check guests into the room.

The following document represents the hotel room:

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

The following example uses the bookARoomUnsafe() function to check a guest into a room. The function performs the find and update operations separately.

def bookARoomUnsafe(guestName: String): Unit = {
val availableFilter = equal("reserved", false)
val findResults = Await.result(
hotelCollection.find(availableFilter).first().toFuture(),
Duration(10, TimeUnit.SECONDS)
)
if (Option(findResults).isEmpty) {
println(s"Sorry, we are booked, $guestName")
return
}
val room = findResults
val roomName = room.getString("room")
println(s"You got the $roomName, $guestName")
val reserveUpdate = combine(
set("reserved", true),
set("guest", guestName)
)
val roomFilter = equal("_id", room.get("_id").get)
Await.result(
hotelCollection.updateOne(roomFilter, reserveUpdate).toFuture(),
Duration(10, TimeUnit.SECONDS)
)
}

Imagine that two guests, Jan and Pat, try to book the same room at the same time.

Jan sees this output:

You got the Blue Room, Jan

Pat sees this output:

You got the Blue Room, Pat

When you look at the database, you see the following document:

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

Your application told both Pat and Jan that they had reserved the room, but the database shows a reservation for Jan only. Here is the sequence of operations that MongoDB performed:

  1. Find and return the empty room for Jan

  2. Find and return the empty room for Pat

  3. Update the room with Pat's reservation

  4. Update the room with Jan's reservation

Although Pat reserved the room for a moment, Jan's update operation overwrote Pat's reservation because it ran last.

The following example uses the bookARoomSafe() function to check a guest into a room. The function uses findOneAndUpdate() to perform the find and update operations as a single atomic operation.

def bookARoomSafe(guestName: String): Unit = {
val reserveUpdate = combine(
set("reserved", true),
set("guest", guestName)
)
val availableFilter = equal("reserved", false)
val room: Document = Await.result(
hotelCollection.findOneAndUpdate(availableFilter, reserveUpdate).toFuture(),
Duration(10, TimeUnit.SECONDS)
)
if (Option(room).isEmpty) {
println(s"Sorry, we are booked, $guestName")
return
}
val roomName = room.getString("room")
println(s"You got the $roomName, $guestName")
}

Imagine that two guests, Jan and Pat, try to book the same room at the same time.

Jan sees this output:

You got the Blue Room, Jan

Pat sees this output:

Sorry, we are booked, Pat

When you look at the database, you see the following document:

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

Jan reserved the room, and Pat received the correct message that no rooms are available. Here is the sequence of operations that MongoDB performed:

  1. Find an empty room for Jan and reserve it

  2. Try to find and reserve an empty room for Pat

  3. Find no empty rooms and complete without emitting a value

Because findOneAndUpdate() performs the find and update atomically, Pat's operation cannot read the room as available after Jan's reservation completes.

Important

Write Lock

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

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