对于 AI 代理:可在 https://www.mongodb.com/zh-cn/docs/llms.txt 获取文档索引—通过在任何 URL 路径后添加 .md 可获取所有页面的 Markdown 版本。
See how MongoDB 9.0 delivers up to 2x higher throughput.
MongoDB Branding Shape
Register now >
Docs 菜单

复合运算符

在本指南中,您可以学习如何使用 Scala 驱动程序执行复合操作。

复合操作包含一个读取操作和一个写入操作,这些操作作为一个原子操作执行。原子操作是一种操作,它要么完全完成,要么根本没有完成。

原子操作可以帮助您避免代码中出现竞争条件。当代码的行为取决于不可控事件的顺序时,就会出现竞争条件。

MongoDB 支持以下复合操作:

  • 查找并更新一个文档

  • 查找并替换一个文档

  • 查找并删除一个文档

有关原子操作和原子性的更多信息,请参阅 MongoDB Server 手册中的原子性和事务。

提示

事务

如果您需要原子地执行更复杂的任务,例如读取和写入多个文档,请使用事务。事务允许您将一系列数据库命令作为原子操作执行。有关事务的更多信息,请参阅MongoDB Server手册中的事务。

本节介绍如何使用 Scala 驱动程序执行复合操作。

以下示例使用包含两个样本文档的集合:

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

这些示例使用以下 collection 引用将每个文档映射到 Document 类的实例:

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

每个复合操作都会返回 SingleObservable[Document],它会发出匹配的文档,或在没有文档匹配查询筛选条件的情况下完成而不发出值。如果查询筛选条件匹配多个文档,则该方法会对第一个匹配项执行操作,其中 first 指的是磁盘上的自然顺序,除非您在对应的选项对象中指定排序顺序。

注意

写入操作后返回文档

默认情况下,复合操作会返回匹配的文档,然后对其执行写入操作。您可以通过向对应方法传递选项对象,指示复合操作首先执行写入操作。查找和替换 示例显示了此配置。

要查找并更新一个文档,请使用 MongoCollection 类的 findOneAndUpdate() 方法。

以下示例使用 findOneAndUpdate() 方法查找 color 字段值为 "green" 的文档。然后,它会将该文档中的 food 字段更新为 "pizza"。

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

  • 指定更新或插入,如果没有与查询匹配的文档,则会插入查询筛选器指定的文档。

  • 将 MongoDB 实例上此操作的最长执行时间设置为 5 秒。如果操作耗时较长,findOneAndUpdate() 方法会通过观察者的 onError() 回调发出 MongoExecutionTimeoutException。

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

有关本节中使用的方法和类的详情,请参阅以下 API 文档:

要查找和替换一个文档,请使用 MongoCollection 类的 findOneAndReplace() 方法。

以下示例使用 findOneAndReplace() 方法查找 color 字段值为 "green" 的文档。它会将匹配的文档替换为

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

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

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

有关本节中使用的方法和类的详情,请参阅以下 API 文档:

要查找并删除一个文档,请使用 MongoCollection 类的 findOneAndDelete() 方法。

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

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

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

有关本节中使用的方法和类的详情,请参阅以下 API 文档:

通过原子地执行多个操作,复合操作可帮助您避免竞争条件。

在本节的示例中,假设您经营一家只有一个房间的酒店。您使用一个小型 Scala 程序来办理客户入住手续。

以下文档代表酒店房间:

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

以下示例使用 bookARoomUnsafe() 函数将来宾登记到房间。该函数分别执行查找和更新操作。

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

设想两位客人 Jan 和 Pat 尝试同时预订同一个房间。

Jan 会看到以下输出:

You got the Blue Room, Jan

Pat 看到以下输出:

You got the Blue Room, Pat

查看数据库时,您会看到以下文档:

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

您的应用程序告诉 Pat 和 Jan,他们都预约了房间,但数据库仅显示 Jan 的预约。以下是 MongoDB 执行的操作序列:

  1. 查找并返回 Jan 的空房间

  2. 查找并返回 Pat 的空房间

  3. 使用 Pat 的预订更新房间

  4. 使用 Jan 的预订更新房间

尽管 Pat 暂时预留了房间,但 Jan 的更新操作覆盖了 Pat 的预约,因为它是最后运行的。

以下示例使用 bookARoomSafe() 函数将来宾登记到房间。该函数使用 findOneAndUpdate() 作为单个原子操作来执行查找和更新操作。

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

设想两位客人 Jan 和 Pat 尝试同时预订同一个房间。

Jan 会看到以下输出:

You got the Blue Room, Jan

Pat 看到以下输出:

Sorry, we are booked, Pat

查看数据库时,您会看到以下文档:

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

Jan 预留了房间,Pat 收到了正确的消息,即没有可用的房间。以下是 MongoDB 执行的操作序列:

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

  2. 尝试为 Pat 查找并预留一间空房

  3. 找不到空房间,完成时不发出值

由于 findOneAndUpdate() 原子地执行查找和更新,因此 Pat 的操作无法在 Jan 的预订完成后将房间读取为可用。

重要

写锁

在复合操作期间,MongoDB 实例会在您正在修改的文档上放置写锁(write lock)。

有关本节中使用的方法和类的详情,请参阅以下 API 文档: