Compound Operations
On this page
Overview
In this guide, you can learn how to perform compound operations with the MongoDB Kotlin 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.
How to Use Compound Operations
This section shows how to use each compound operation with the MongoDB Kotlin Driver.
The following examples use a collection containing these two sample documents.
{"_id": 1, "food": "donut", "color": "green"} {"_id": 2, "food": "pear", "color": "yellow"}
This data is modeled with the following Kotlin data class:
data class FoodOrder( val id: Int, val food: String, val color: String )
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.
Find and Update
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.
Example
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:
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 aMongoExecutionTimeoutException
.
val filter = Filters.eq(FoodOrder::color.name, "green") val update = Updates.set(FoodOrder::food.name, "pizza") val options = FindOneAndUpdateOptions() .upsert(true) .maxTime(5, TimeUnit.SECONDS) /* The result variable contains your document in the state before your update operation is performed or null if the document was inserted due to upsert being true */ val result = collection.findOneAndUpdate(filter, update, options) println(result)
FoodOrder(id=1, food=donut, 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:
Find and Replace
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.
Example
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.
data class Music( val id: Int, val music: String, val color: String ) val filter = Filters.eq(FoodOrder::color.name, "green") val replace = Music(1, "classical", "green") val options = FindOneAndReplaceOptions() .returnDocument(ReturnDocument.AFTER) val result = collection.withDocumentClass<Music>().findOneAndReplace(filter, replace, options) println(result)
Music(id=1, music=classical, color=green)
For more information about the methods and classes mentioned 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. The findOneAndDelete()
method returns your found
document or null
if no documents match your query.
Example
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.
val sort = Sorts.descending("_id") val filter = Filters.empty() val options = FindOneAndDeleteOptions().sort(sort) val result = collection.findOneAndDelete(filter, options) println(result)
FoodOrder(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:
Avoiding a Race Condition
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 Kotlin 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}
This data is modeled with the following Kotlin data class:
data class HotelRoom( val id: Int, val guest: String? = null, val room: String, val reserved: Boolean = false )
Example With Race Condition
Let's say our app uses this bookARoomUnsafe
method to checkout our room to
a guest:
suspend fun bookARoomUnsafe(guestName: String) { val filter = Filters.eq("reserved", false) val myRoom = hotelCollection.find(filter).firstOrNull() if (myRoom == null) { println("Sorry, we are booked, $guestName") return } val myRoomName = myRoom.room println("You got the $myRoomName, $guestName") val update = Updates.combine(Updates.set("reserved", true), Updates.set("guest", guestName)) val roomFilter = Filters.eq("_id", myRoom.id) hotelCollection.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": false}
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.
Example Without Race Condition
Let's use a compound operation to avoid the race condition and always give our users the correct message.
suspend fun bookARoomSafe(guestName: String) { val update = Updates.combine( Updates.set(HotelRoom::reserved.name, true), Updates.set(HotelRoom::guest.name, guestName) ) val filter = Filters.eq("reserved", false) val myRoom = hotelCollection.findOneAndUpdate(filter, update) if (myRoom == null) { println("Sorry, we are booked, $guestName") return } val myRoomName = myRoom.room println("You got the $myRoomName, $guestName") }
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": false}
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.
When 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.