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

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

在開(kāi)始閱讀本文之前,請(qǐng)確保你熟悉Play-Json的相關(guān)開(kāi)發(fā),或是已經(jīng)閱讀過(guò)Play Scala 2.5.x - Play JSON開(kāi)發(fā)指南

1 為什么要Play with MongoDB?

Reactive越來(lái)越流行的今天,傳統(tǒng)阻塞式的數(shù)據(jù)庫(kù)驅(qū)動(dòng)已經(jīng)無(wú)法滿足Reactive應(yīng)用的需要,為此我們將目光轉(zhuǎn)向新誕生的數(shù)據(jù)庫(kù)新星MongoDB。MongoDB從誕生以來(lái)就爭(zhēng)議不斷,總結(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ì)的喜好,沒(méi)什么可爭(zhēng)議的。至于默認(rèn)忽略錯(cuò)誤也是無(wú)稽之談,對(duì)于那些非關(guān)鍵數(shù)據(jù),MongoDB為你提供了一個(gè)Fire and Forget模式,可以顯著提高系統(tǒng)性能,并且?guī)缀跛械腗ongoDB驅(qū)動(dòng)都默認(rèn)關(guān)閉了這個(gè)模式,如果需要你可以手動(dòng)打開(kāi)。默認(rèn)關(guān)閉認(rèn)證并不是不支持認(rèn)證,只是為了方便快速原型,如果你敢在線上裸奔MongoDB,我只能默默地為你點(diǎn)根蠟燭...。數(shù)據(jù)丟失問(wèn)題已經(jīng)成為歷史,曾經(jīng)在網(wǎng)上廣為流傳的兩篇關(guān)于MongoDB數(shù)據(jù)丟失問(wèn)題(1, 2), 經(jīng)過(guò)分布式系統(tǒng)安全性測(cè)試組織JEPSEN最新的測(cè)試分析表明,MongoDB 3.4.0已經(jīng)解決了這些問(wèn)題。

聊完?duì)幾h,我們來(lái)看看MongoDB有哪些優(yōu)點(diǎn):

  • 簡(jiǎn)單易用
  • BSON格式數(shù)據(jù)統(tǒng)一前后臺(tái)
  • 異步數(shù)據(jù)庫(kù)驅(qū)動(dòng)
  • 沒(méi)有事務(wù),所以高并發(fā)時(shí)仍能保持很好的讀寫(xiě)性能
  • Schemaless,方便快速原型
  • 支持集群,MapReduce
  • 支持GridFS,易用的分布式文件系統(tǒng)
  • 通過(guò)oplog可以實(shí)現(xiàn)實(shí)時(shí)應(yīng)用

其中異步數(shù)據(jù)庫(kù)驅(qū)動(dòng)最為吸引人,也是本文關(guān)注的重點(diǎn)。其它的一些優(yōu)點(diǎn)并非是MongoDB獨(dú)有的,例如oplog,其它數(shù)據(jù)庫(kù)也有相似的技術(shù),例如mysql的binlog。

2 如何Play with MongoDB?

Reactive-Mongo是一個(gè)基于Scala編寫(xiě)的異步非阻塞MongoDB驅(qū)動(dòng),該項(xiàng)目同時(shí)提供了Play框架的集成插件Play-ReactiveMongo。本文將基于Play-ReactiveMongo插件介紹MongoDB的開(kāi)發(fā)技巧。

2.1 配置Play-ReactiveMongo插件

打開(kāi)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ù)庫(kù)連接
mongodb.uri = "mongodb://someuser:somepasswd@localhost:27017/your_db_name"

OK,此時(shí)在命令行執(zhí)行sbt compile,sbt會(huì)自動(dòng)下載Play-ReactiveMongo依賴,并完成編譯過(guò)程。

2.2 開(kāi)發(fā)示例

2.2.1 定義Model和Controller

在定義Model時(shí)最好顯式聲明_id屬性,因?yàn)樵搶傩詾镸ongoDB的默認(rèn)主鍵,如果沒(méi)有,在插入時(shí)會(huì)自動(dòng)生成。下面代碼定義了一個(gè)Person類,以及用于完成PersonJsObject之間相互轉(zhuǎn)換的隱式OFormat[Person]對(duì)象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è)隱式對(duì)象,我們便可以在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對(duì)象,用于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"))
    
    ...    
}

請(qǐng)注意,personColFuturedef而不是val,這樣做的原因是為了適應(yīng)Play框架的熱加載功能。

2.2.2 插入操作

不同的修改操作會(huì)返回不同類型的WriteResult,通過(guò)該類型的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],你需要熟悉這種開(kāi)發(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í)并沒(méi)有被修改),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屬性。如果沒(méi)有$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í),千萬(wàn)別忘記$set操作符,否則會(huì)造成數(shù)據(jù)丟失。

2.2.4 查詢操作

JSONCollection.find()方法返回結(jié)果為GenericQueryBuilder類型,該類型用于構(gòu)建查詢語(yǔ)句,調(diào)用其cursor方法會(huì)觸發(fā)查詢請(qǐng)求并返回一個(gè)Cursor[T]類型,通過(guò)迭代該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 分頁(yè)操作

這里使用GenericQueryBuilder.options()方法設(shè)置分頁(yè)信息,然后使用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è)簡(jiǎn)單的消息隊(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公司開(kāi)發(fā)的MongoDB管理工具,非商業(yè)用途可以免費(fèi)使用,如果是公司還是建議購(gòu)買商業(yè)Licence。該工具基于Java開(kāi)發(fā),支持跨平臺(tái)并且功能非常全面,例如在查詢結(jié)果列表上可以直接進(jìn)行編輯,Collections的復(fù)制粘貼和導(dǎo)入導(dǎo)出,用戶角色和權(quán)限管理,是客戶端管理的首選工具。

3.2 Robomongo

Robomongo前身是由Dmitry Schetnikovichk開(kāi)發(fā)并維護(hù)的個(gè)人項(xiàng)目,目前已經(jīng)被Studio 3T收購(gòu),并對(duì)外承諾永久免費(fèi)使用。該工具基于Qt開(kāi)發(fā),支持跨平臺(tái),目前已經(jīng)正式發(fā)布1.0版本。

4 小結(jié)

MongoDB自2009發(fā)布以來(lái),產(chǎn)品和社區(qū)都已經(jīng)非常成熟,已經(jīng)有商業(yè)公司在云上提供MongoDB服務(wù)。除此之外,MongoDB不僅方便開(kāi)發(fā),而且容易維護(hù),普通的開(kāi)發(fā)人員利用自帶的mongodumpmongorestore命令便可進(jìn)行備份、恢復(fù)操作。當(dāng)然最重要的是利用MongoDB的異步驅(qū)動(dòng)和oplog可以開(kāi)發(fā)高性能的實(shí)時(shí)應(yīng)用,同時(shí)統(tǒng)一了前后端的數(shù)據(jù)結(jié)構(gòu),開(kāi)發(fā)體驗(yàn)非常不錯(cuò)!最后再補(bǔ)充一句,如果對(duì)事務(wù)性要求較高,還是建議選擇RDBMS。轉(zhuǎn)載請(qǐng)注明作者joymufeng,歡迎來(lái)訪PlayScala社區(qū)(http://www.playscala.cn/)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容