在開始閱讀本文之前卵渴,請確保你熟悉Play-Json的相關(guān)開發(fā)阅茶,或是已經(jīng)閱讀過Play Scala 2.5.x - Play JSON開發(fā)指南倒堕。
1 為什么要Play with MongoDB?
在Reactive越來越流行的今天缸匪,傳統(tǒng)阻塞式的數(shù)據(jù)庫驅(qū)動(dòng)已經(jīng)無法滿足Reactive應(yīng)用的需要珍手,為此我們將目光轉(zhuǎn)向新誕生的數(shù)據(jù)庫新星MongoDB。MongoDB從誕生以來就爭議不斷双抽,總結(jié)一下主要有一下幾點(diǎn):
- Schemaless
- 不支持事務(wù)
- 默認(rèn)忽略錯(cuò)誤
- 默認(rèn)關(guān)閉認(rèn)證
- 會(huì)導(dǎo)致數(shù)據(jù)丟失
其實(shí)Schemaless
和不支持事務(wù)
是技術(shù)選型時(shí)的決定百框,不應(yīng)該受到吐槽,主要看是否滿足業(yè)務(wù)需求以及團(tuán)隊(duì)的喜好牍汹,沒什么可爭議的铐维。至于默認(rèn)忽略錯(cuò)誤
也是無稽之談,對于那些非關(guān)鍵數(shù)據(jù)慎菲,MongoDB為你提供了一個(gè)Fire and Forget
模式嫁蛇,可以顯著提高系統(tǒng)性能,并且?guī)缀跛械腗ongoDB驅(qū)動(dòng)都默認(rèn)關(guān)閉了這個(gè)模式露该,如果需要你可以手動(dòng)打開睬棚。默認(rèn)關(guān)閉認(rèn)證
并不是不支持認(rèn)證
,只是為了方便快速原型,如果你敢在線上裸奔MongoDB抑党,我只能默默地為你點(diǎn)根蠟燭...包警。數(shù)據(jù)丟失
問題已經(jīng)成為歷史,曾經(jīng)在網(wǎng)上廣為流傳的兩篇關(guān)于MongoDB數(shù)據(jù)丟失問題(1, 2), 經(jīng)過分布式系統(tǒng)安全性測試組織JEPSEN最新的測試分析表明新荤,MongoDB 3.4.0已經(jīng)解決了這些問題揽趾。
聊完?duì)幾h,我們來看看MongoDB有哪些優(yōu)點(diǎn):
- 簡單易用
- BSON格式數(shù)據(jù)統(tǒng)一前后臺(tái)
- 異步數(shù)據(jù)庫驅(qū)動(dòng)
- 沒有事務(wù)苛骨,所以高并發(fā)時(shí)仍能保持很好的讀寫性能
- Schemaless篱瞎,方便快速原型
- 支持集群,MapReduce
- 支持GridFS痒芝,易用的分布式文件系統(tǒng)
- 通過oplog可以實(shí)現(xiàn)實(shí)時(shí)應(yīng)用
其中異步數(shù)據(jù)庫驅(qū)動(dòng)
最為吸引人俐筋,也是本文關(guān)注的重點(diǎn)。其它的一些優(yōu)點(diǎn)并非是MongoDB獨(dú)有的严衬,例如oplog澄者,其它數(shù)據(jù)庫也有相似的技術(shù),例如mysql的binlog请琳。
2 如何Play with MongoDB粱挡?
Reactive-Mongo
是一個(gè)基于Scala編寫的異步非阻塞MongoDB驅(qū)動(dòng),該項(xiàng)目同時(shí)提供了Play框架的集成插件Play-ReactiveMongo俄精。本文將基于Play-ReactiveMongo插件介紹MongoDB的開發(fā)技巧询筏。
2.1 配置Play-ReactiveMongo插件
打開Play項(xiàng)目,修改build.sbt
添加Play-ReactiveMongo依賴:
libraryDependencies ++= Seq(
"org.reactivemongo" %% "play2-reactivemongo" % "0.11.14"
)
修改application.conf
竖慧,添加如下內(nèi)容:
# 啟用ReactiveMongoModule
play.modules.enabled += "play.modules.reactivemongo.ReactiveMongoModule"
# 配置數(shù)據(jù)庫連接
mongodb.uri = "mongodb://someuser:somepasswd@localhost:27017/your_db_name"
OK嫌套,此時(shí)在命令行執(zhí)行sbt compile
,sbt會(huì)自動(dòng)下載Play-ReactiveMongo依賴圾旨,并完成編譯過程踱讨。
2.2 開發(fā)示例
2.2.1 定義Model和Controller
在定義Model時(shí)最好顯式聲明_id
屬性,因?yàn)樵搶傩詾镸ongoDB的默認(rèn)主鍵砍的,如果沒有痹筛,在插入時(shí)會(huì)自動(dòng)生成。下面代碼定義了一個(gè)Person
類挨约,以及用于完成Person
和JsObject
之間相互轉(zhuǎn)換的隱式OFormat[Person]
對象personFormat
味混。
package models
case class Person(_id: String, name: String, age: Int)
object JsonFormats {
import play.api.libs.json.Json
// Generates Writes and Reads for Person, thanks to Json Macros
implicit val personFormat = Json.format[Person]
}
只要導(dǎo)入models.JsonFormats.personFormat
這個(gè)隱式對象,我們便可以在Person
和JsObject
實(shí)現(xiàn)雙向轉(zhuǎn)換:
import models.JsonFormats.personFormat
//JsObject -> Person
val jsObj = Json.obj("name" -> "joymufeng", "age" -> 31)
val p = jsObj.as[Person]
//Person -> JsObject
val newJsObj = Json.toJson(p)
Application
Controller混入了MongoController
诫惭,所以在Application
內(nèi)可以直接使用MongoController
定義的方法和屬性,例如database
蔓挖。
import play.api.mvc.{ Action, Controller }
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.json._
//導(dǎo)入ReactiveMongo插件
import play.modules.reactivemongo.{ MongoController, ReactiveMongoApi, ReactiveMongoComponents }
//導(dǎo)入BSON-JSON conversions/collection
import reactivemongo.play.json._
import reactivemongo.play.json.collection._
//導(dǎo)入隱式的format對象夕土,用于JsObject <-> Person之間相互轉(zhuǎn)換
import models.JsonFormats._
class Application @Inject() (val reactiveMongoApi: ReactiveMongoApi) extends Controller
with MongoController with ReactiveMongoComponents {
def personColFuture = database.map(_.collection[JSONCollection]("persons"))
...
}
請注意,personColFuture
是def
而不是val
,這樣做的原因是為了適應(yīng)Play框架的熱加載功能怨绣。
2.2.2 插入操作
不同的修改操作會(huì)返回不同類型的WriteResult
角溃,通過該類型的WriteResult
可以判斷當(dāng)前操作是否成功。JSONCollection.insert()
方法返回類型為Future[WriteResult]
類型篮撑,判斷當(dāng)前操作成功的條件是wr.ok && wr.n == 1
减细。
def testInsert(name: String, age: Int) = Action.async {
personColFuture.flatMap(_.insert(Person(name, name, age))).map{ wr: WriteResult =>
if (wr.ok && wr.n == 1) {
Ok("success")
} else {
Ok("fail")
}
}.recover{ case t: Throwable =>
Ok("error")
}
}
所有的操作都是異步的,即返回結(jié)果類型為Future[T]赢笨,你需要熟悉這種開發(fā)模式未蝌。
WriteResult.ok
為true僅僅表明成功的讀取了WriteResult響應(yīng),并不表示當(dāng)前的操作一定執(zhí)行成功了茧妒。
2.2.3 更新操作
JSONCollection.update()
方法返回Future[UpdateWriteResult]
萧吠,UpdateWriteResult.n
表示匹配條件的記錄數(shù)量,UpdateWriteResult.nModified
表示真實(shí)被修改的記錄數(shù)量(不包含更新值和原值相同的記錄桐筏,因?yàn)檫@些記錄其實(shí)并沒有被修改)纸型,UpdateWriteResult.upserted
返回被upserted的記錄_id列表。
def testUpdate(_id: String, newName: String) = Action.async {
personColFuture.flatMap(_.update(Json.obj("_id" -> _id), Json.obj("$set" -> Json.obj("name" -> newName)))).map{ uwr =>
if (uwr.ok && uwr.n == 1) {
Ok("success")
} else {
Ok("fail")
}
}.recover{ case t: Throwable =>
Ok("error")
}
}
MongoDB的update
操作支持更新文檔或替換文檔梅忌,如果更新文檔的部分屬性使用$set
操作符狰腌,例如上面的示例代碼僅更新了name
屬性。如果沒有$set
操作符牧氮,則意味著是用當(dāng)前的文檔替換原文檔琼腔,例如:
def update(_id: String, newName: String) = Action.async {
personColFuture.flatMap(_.update(Json.obj("_id" -> _id), Json.obj("name" -> newName))).map{ uwr =>
if (uwr.ok && uwr.n == 1) {
Ok("success")
} else {
Ok("fail")
}
}.recover{ case t: Throwable =>
Ok("error")
}
}
上面的代碼將會(huì)把符合條件的文檔更新為只剩一個(gè)name
屬性的文檔片段。
在使用
update
方法時(shí)蹋笼,千萬別忘記$set
操作符展姐,否則會(huì)造成數(shù)據(jù)丟失。
2.2.4 查詢操作
JSONCollection.find()
方法返回結(jié)果為GenericQueryBuilder
類型剖毯,該類型用于構(gòu)建查詢語句圾笨,調(diào)用其cursor
方法會(huì)觸發(fā)查詢請求并返回一個(gè)Cursor[T]
類型,通過迭代該Cursor[T]
我們可以收集查詢結(jié)果逊谋。GenericQueryBuilder.one[T]
方法等價(jià)于GenericQueryBuilder.cursor[T]().headOption
擂达。
def testRead(_id: String) = Action.async {
personColFuture.flatMap(_.find(Json.obj("_id" -> _id)).one[Person]).map{
case Some(p) => Ok("Find Person " + p.name)
case None => Ok("Person Not Found.")
}.recover{ case t: Throwable =>
Ok("error")
}
}
2.2.5 刪除操作
JSONCollection.remove()
方法返回結(jié)果為Future[WriteResult]
類型,WriteResult.n
表示刪除的記錄數(shù)量胶滋。
def testDelete(_id: String) = Action.async {
personColFuture.flatMap(_.remove(Json.obj("_id" -> _id))).map{ wr =>
if (uwr.ok && uwr.n == 1) {
Ok("success")
} else {
Ok("fail")
}
}.recover{ case t: Throwable =>
Ok("error")
}
}
2.2.6 分頁操作
這里使用GenericQueryBuilder.options()
方法設(shè)置分頁信息板鬓,然后使用Cursor[T].collect[List]()
方法收集前15條查詢結(jié)果。利用JSONCollection.count()
方法可以查詢滿足條件的記錄總數(shù)究恤。
def testPaging(page: Int) = Action.async {
for{
personCol <- personColFuture
list <- personCol.find(Json.obj())
.options(QueryOpts(skipN = page * 15, batchSizeN = 15))
.cursor[Person]()
.collect[List](15)
total <- personCol.count(Some(Json.obj()))
} yield {
Ok(s"Total: ${total}\r\n${list.map(_.name).mkString("\r\n")}")
}
}
2.2.7 批量插入
批量插入可以直接使用JSONCollection.bulkInsert
俭令, 插入前需將List[Person]
轉(zhuǎn)換成Documents
,返回類型為MultiBulkWriteResult
部宿。MultiBulkWriteResult.n
表示成功插入的條數(shù)抄腔。
def testBulkInsert = Action.async {
val list = List(Person("0", "p0", 30), Person("1", "p1", 30))
personColFuture.flatMap{ personCol =>
//將List[Person]轉(zhuǎn)換成待插入的Documents
val docs = list.map(implicitly[personCol.ImplicitlyDocumentProducer](_))
personCol.bulkInsert(false)(docs: _*).map{ mbwr: MultiBulkWriteResult =>
if(mbwr.ok && mbwr.n > 0){
Ok(s"成功插入${mbwr.n}條記錄")
} else {
Ok(mbwr.toString)
}
}
}
}
2.2.8 FindAndModify
借助MongoDB提供的FindAndModify
方法瓢湃,可以實(shí)現(xiàn)一個(gè)簡單的消息隊(duì)列或是任務(wù)領(lǐng)取功能,
def testFindAndModify = Action.async {
personColFuture.flatMap{ personCol =>
val selector = Json.obj()
val modifier = personCol.updateModifier(Json.obj("$set" -> Json.obj("age" -> 30)))
personCol.findAndModify(selector, modifier)
.map(_.result[Person]).map{
case Some(personBeforeUpdate) =>
Ok(s"Fetch Person ${personBeforeUpdate.name}")
case None =>
Ok("No Person Found.")
}
}
}
3 客戶端工具選擇
3.1 Studio 3T
Studio 3T是由3T Software Labs公司開發(fā)的MongoDB管理工具赫蛇,非商業(yè)用途可以免費(fèi)使用绵患,如果是公司還是建議購買商業(yè)Licence。該工具基于Java開發(fā)悟耘,支持跨平臺(tái)并且功能非常全面落蝙,例如在查詢結(jié)果列表上可以直接進(jìn)行編輯,Collections的復(fù)制粘貼和導(dǎo)入導(dǎo)出暂幼,用戶角色和權(quán)限管理筏勒,是客戶端管理的首選工具。
3.2 Robomongo
Robomongo前身是由Dmitry Schetnikovichk開發(fā)并維護(hù)的個(gè)人項(xiàng)目粟誓,目前已經(jīng)被Studio 3T收購奏寨,并對外承諾永久免費(fèi)使用。該工具基于Qt開發(fā)鹰服,支持跨平臺(tái)病瞳,目前已經(jīng)正式發(fā)布1.0版本。
4 小結(jié)
MongoDB自2009發(fā)布以來悲酷,產(chǎn)品和社區(qū)都已經(jīng)非常成熟套菜,已經(jīng)有商業(yè)公司在云上提供MongoDB服務(wù)。除此之外设易,MongoDB不僅方便開發(fā)逗柴,而且容易維護(hù),普通的開發(fā)人員利用自帶的mongodump
和mongorestore
命令便可進(jìn)行備份顿肺、恢復(fù)操作戏溺。當(dāng)然最重要的是利用MongoDB的異步驅(qū)動(dòng)和oplog可以開發(fā)高性能的實(shí)時(shí)應(yīng)用,同時(shí)統(tǒng)一了前后端的數(shù)據(jù)結(jié)構(gòu)屠尊,開發(fā)體驗(yàn)非常不錯(cuò)旷祸!最后再補(bǔ)充一句,如果對事務(wù)性要求較高讼昆,還是建議選擇RDBMS托享。轉(zhuǎn)載請注明作者joymufeng,歡迎來訪PlayScala社區(qū)(http://www.playscala.cn/)浸赫。