Overview
在本指南中,您可以学习如何使用 Scala 驱动程序执行复合操作。
复合操作包含一个读取操作和一个写入操作,这些操作作为一个原子操作执行。原子操作是一种操作,它要么完全完成,要么根本没有完成。
原子操作可以帮助您避免代码中出现竞争条件。当代码的行为取决于不可控事件的顺序时,就会出现竞争条件。
MongoDB 支持以下复合操作:
查找并更新一个文档
查找并替换一个文档
查找并删除一个文档
有关原子操作和原子性的更多信息,请参阅 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 执行的操作序列:
查找并返回 Jan 的空房间
查找并返回 Pat 的空房间
使用 Pat 的预订更新房间
使用 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 执行的操作序列:
为 Jan 找到一个空房间并预订
尝试为 Pat 查找并预留一间空房
找不到空房间,完成时不发出值
由于 findOneAndUpdate() 原子地执行查找和更新,因此 Pat 的操作无法在 Jan 的预订完成后将房间读取为可用。
重要
写锁
在复合操作期间,MongoDB 实例会在您正在修改的文档上放置写锁(write lock)。
有关本节中使用的方法和类的详情,请参阅以下 API 文档: