文档菜单
文档首页
/ / /
Kotlin 协程

复合操作

本页内容

  • 概述
  • 如何使用复合操作
  • 查找并更新
  • 查找并替换
  • 查找并删除
  • 避免竞争条件
  • 有竞争条件的示例
  • 无竞争条件的示例

在本指南中,您可以了解如何使用MongoDB Kotlin驱动程序执行复合操作。

复合操作包括作为一个原子操作执行的读和写操作。原子操作要么完全完成,要么完全不完成。原子操作不能部分完成。

原子操作可以帮助您避免代码中的竞争条件。竞争条件是指代码的行为依赖于不可控事件的顺序。

MongoDB支持以下复合操作

  • 查找并更新一个文档

  • 查找并替换一个文档

  • 查找并删除一个文档

如果您需要原子地执行更复杂的任务,例如对多个文档进行读写,请使用事务。事务是MongoDB和其他数据库的功能,允许您将任意序列的数据库命令定义为一个原子操作。

有关原子操作和原子性的更多信息,请参阅有关原子性和事务的MongoDB手册条目.

有关事务的更多信息,请参阅有关事务的MongoDB手册条目。

本节展示了如何使用MongoDB Kotlin驱动程序使用每个复合操作。

以下示例使用包含以下两个示例文档的集合。

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

这些数据使用以下Kotlin数据类建模

data class FoodOrder(
@BsonId val id: Int,
val food: String,
val color: String
)

注意

写入之前或之后?

默认情况下,每个复合操作都会返回在您的写入操作之前的状态中的找到的文档。您可以通过使用与您的复合操作对应的选项类来检索在您的写入操作之后的状态中的找到的文档。您可以在下面的查找并替换示例中看到此配置的一个示例。

要查找和更新一个文档,请使用findOneAndUpdate() 方法,它属于 MongoCollection 类。如果查询没有匹配到文档,findOneAndUpdate() 方法返回找到的文档或 null

以下示例使用 findOneAndUpdate() 方法查找具有 color 字段设置为 "green" 的文档,并将该文档中的 food 字段更新为 "pizza"

示例还使用 FindOneAndUpdateOptions 实例来指定以下选项

  • 指定一个 upsert,如果查询没有匹配到文档,则插入指定的文档。

  • 为 MongoDB 实例上的此操作设置最大执行时间为 5 秒。如果操作耗时更长,findOneAndUpdate() 方法将抛出 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)

有关 Projections 类的更多信息,请参阅我们的Projections 构建器指南.

有关 upsert 操作的更多信息,请参阅我们的 upsert 指南。

有关本节中提到的方法和类的更多信息,请参阅以下 API 文档

要查找并替换一个文档,请使用 MongoCollection 类的 findOneAndReplace() 方法。该方法返回找到的文档或 null(如果查询没有匹配的文档)。

以下示例使用 findOneAndReplace() 方法查找具有 color 字段设置为 "green" 的文档,并用以下文档替换它

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

该示例还使用一个 FindOneAndReplaceOptions 实例来指定返回的文档应该是替换操作后的状态。

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)

有关本节中提到的方法和类的更多信息,请参阅以下 API 文档

要查找并删除一个文档,请使用 MongoCollection 类的 findOneAndDelete() 方法。该方法返回找到的文档或 null(如果查询没有匹配的文档)。

以下示例使用 findOneAndDelete() 方法查找并删除 _id 字段中具有最大值的文档。

示例使用 FindOneAndDeleteOptions 实例指定对 _id 字段的降序排序。

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)

有关 Sorts 类的更多信息,请参阅我们的 Sorts 构建器指南。

有关本节中提到的方法和类的更多信息,请参阅以下 API 文档

在本节中,我们将探讨两个示例。第一个示例包含一个竞争条件,第二个示例使用复合操作来避免第一个示例中存在的竞争条件。

对于这两个示例,让我们假设我们经营着一家只有一个房间的酒店,并且我们有一个小的 Kotlin 程序来帮助我们向客人结账。

以下 MongoDB 中的文档表示房间

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

这些数据使用以下Kotlin数据类建模

data class HotelRoom(
@BsonId val id: Int,
val guest: String? = null,
val room: String,
val reserved: Boolean = false
)

假设我们的应用程序使用此 bookARoomUnsafe 方法来向客人结账房间

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)
}

想象有两个不同的客人,Jan 和 Pat,同时使用此方法尝试预订房间。

Jan 看到以下输出

You got the Blue Room, Jan

Pat看到以下输出

You got the Blue Room, Pat

当我们查看我们的数据库时,我们看到以下内容

{"_id": 1, "guest": "Jan", "room": "Blue Room", "reserved": false}

Pat会不高兴。当Pat来到我们的酒店时,Jan已经占用了她的房间。发生了什么问题?

以下是我们的MongoDB实例视角下发生的事件序列

  1. 为Jan找到并返回一个空房间。

  2. 为Pat找到并返回一个空房间。

  3. 将房间更新为Pat已预订。

  4. 将房间更新为Jan已预订。

注意,Pat曾短暂地预订了房间,但由于Jan的更新操作是最后一个执行的,我们的文档显示"Jan"是客人。

让我们使用复合操作来避免竞争条件,并始终给我们的用户提供正确的信息。

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")
}

想象有两个不同的客人,Jan 和 Pat,同时使用此方法尝试预订房间。

Jan 看到以下输出

You got the Blue Room, Jan

Pat看到以下输出

Sorry, we are booked, Pat

当我们查看我们的数据库时,我们看到以下内容

{"_id": 1, "guest": "Jan", "room": "Blue Room", "reserved": false}

Pat得到了正确的信息。虽然她可能很伤心没有获得预订,但她至少知道不要来我们的酒店。

以下是我们的MongoDB实例视角下发生的事件序列

  1. 为Jan找到一个空房间并预订。

  2. 尝试为Pat找到一个空房间并预订。

  3. 当没有房间可用时,返回null

重要

写锁

您的MongoDB实例在复合操作期间会对您正在修改的文档放置写锁。

有关Updates类的信息,请参阅我们的Updates构建器指南。

有关Filters类的更多信息,请参阅我们的Filters构建器指南。

有关 findOneAndUpdate() 方法的更多信息,请参阅 MongoCollection 类的 API 文档。

下一页

MongoDB Kotlin 驱动