Play Scala 2.5.x - Play with MongoDB 開發(fā)指南

歡迎來訪PlayScala社區(qū)(http://www.playscala.cn/)

在開始閱讀本文之前卵渴,請確保你熟悉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類挨约,以及用于完成PersonJsObject之間相互轉(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è)隱式對象,我們便可以在PersonJsObject實(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)

ApplicationController混入了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"))
    
    ...    
}

請注意,personColFuturedef而不是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ā)人員利用自帶的mongodumpmongorestore命令便可進(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/)浸赫。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末闰围,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子既峡,更是在濱河造成了極大的恐慌羡榴,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件运敢,死亡現(xiàn)場離奇詭異炕矮,居然都是意外死亡么夫,警方通過查閱死者的電腦和手機(jī)者冤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門肤视,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人涉枫,你說我怎么就攤上這事邢滑。” “怎么了愿汰?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵困后,是天一觀的道長。 經(jīng)常有香客問我衬廷,道長摇予,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任吗跋,我火速辦了婚禮侧戴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘跌宛。我一直安慰自己酗宋,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布疆拘。 她就那樣靜靜地躺著蜕猫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪哎迄。 梳的紋絲不亂的頭發(fā)上回右,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音漱挚,去河邊找鬼翔烁。 笑死,一個(gè)胖子當(dāng)著我的面吹牛棱烂,可吹牛的內(nèi)容都是我干的租漂。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼颊糜,長吁一口氣:“原來是場噩夢啊……” “哼哩治!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起衬鱼,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬榮一對情侶失蹤业筏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后鸟赫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蒜胖,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡消别,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了台谢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寻狂。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖朋沮,靈堂內(nèi)的尸體忽然破棺而出蛇券,到底是詐尸還是另有隱情,我是刑警寧澤樊拓,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布纠亚,位于F島的核電站,受9級(jí)特大地震影響筋夏,放射性物質(zhì)發(fā)生泄漏蒂胞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一条篷、第九天 我趴在偏房一處隱蔽的房頂上張望骗随。 院中可真熱鬧,春花似錦拥娄、人聲如沸蚊锹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽牡昆。三九已至,卻和暖如春摊欠,著一層夾襖步出監(jiān)牢的瞬間丢烘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來泰國打工些椒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留播瞳,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓免糕,卻偏偏與公主長得像赢乓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子石窑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容