Docs Menu
Docs Home
/ / /
Kotlin Coroutine
/ /

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 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.

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(
@BsonId 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.

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:

  • 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.

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:

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.

data class Music(
@BsonId 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:

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.

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:

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(
@BsonId val id: Int,
val guest: String? = null,
val room: String,
val reserved: Boolean = false
)

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:

  1. Find and return an empty room for Jan.

  2. Find and return an empty room for Pat.

  3. Update the room to booked for Pat.

  4. 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.

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:

  1. Find an empty room for Jan and reserve it.

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

  3. 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.

Back

Specify a Query