Overview
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.
How to Use Compound Operations
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.
Find and Update
To find and update one document, use the findOneAndUpdate() method of the MongoCollection class.
Example
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 aMongoExecutionTimeoutExceptionthrough the observer'sonError()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:
Find and Replace
To find and replace one document, use the findOneAndReplace() method of the MongoCollection class.
Example
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:
Find and Delete
To find and delete one document, use the findOneAndDelete() method of the MongoCollection class.
Example
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:
Avoiding a Race Condition
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}
Example With Race Condition
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:
Find and return the empty room for Jan
Find and return the empty room for Pat
Update the room with Pat's reservation
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.
Example Without Race Condition
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:
Find an empty room for Jan and reserve it
Try to find and reserve an empty room for Pat
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: