Play Scala 2.5.x - Play with MongoDB 开发指南

欢迎来访PlayScala社区(http://www.playscala.cn/)

在开始阅读本文之前,请确保你熟悉Play-Json的相关开发,或是已经阅读过Play Scala 2.5.x - Play JSON开发指南。

1 为什么要Play with MongoDB?

在Reactive越来越流行的今天,传统阻塞式的数据库驱动已经无法满足Reactive应用的需要,为此我们将目光转向新诞生的数据库新星MongoDB。MongoDB从诞生以来就争议不断,总结一下主要有一下几点:

  • Schemaless
  • 不支持事务
  • 默认忽略错误
  • 默认关闭认证
  • 会导致数据丢失

其实Schemaless不支持事务是技术选型时的决定,不应该受到吐槽,主要看是否满足业务需求以及团队的喜好,没什么可争议的。至于默认忽略错误也是无稽之谈,对于那些非关键数据,MongoDB为你提供了一个Fire and Forget模式,可以显著提高系统性能,并且几乎所有的MongoDB驱动都默认关闭了这个模式,如果需要你可以手动打开。默认关闭认证并不是不支持认证,只是为了方便快速原型,如果你敢在线上裸奔MongoDB,我只能默默地为你点根蜡烛...。数据丢失问题已经成为历史,曾经在网上广为流传的两篇关于MongoDB数据丢失问题(1, 2), 经过分布式系统安全性测试组织JEPSEN最新的测试分析表明,MongoDB 3.4.0已经解决了这些问题。

聊完争议,我们来看看MongoDB有哪些优点:

  • 简单易用
  • BSON格式数据统一前后台
  • 异步数据库驱动
  • 没有事务,所以高并发时仍能保持很好的读写性能
  • Schemaless,方便快速原型
  • 支持集群,MapReduce
  • 支持GridFS,易用的分布式文件系统
  • 通过oplog可以实现实时应用

其中异步数据库驱动最为吸引人,也是本文关注的重点。其它的一些优点并非是MongoDB独有的,例如oplog,其它数据库也有相似的技术,例如mysql的binlog。

2 如何Play with MongoDB?

Reactive-Mongo是一个基于Scala编写的异步非阻塞MongoDB驱动,该项目同时提供了Play框架的集成插件Play-ReactiveMongo。本文将基于Play-ReactiveMongo插件介绍MongoDB的开发技巧。

2.1 配置Play-ReactiveMongo插件

打开Play项目,修改build.sbt添加Play-ReactiveMongo依赖:

libraryDependencies ++= Seq(
  "org.reactivemongo" %% "play2-reactivemongo" % "0.11.14"
)

修改application.conf,添加如下内容:

# 启用ReactiveMongoModule
play.modules.enabled += "play.modules.reactivemongo.ReactiveMongoModule"

# 配置数据库连接
mongodb.uri = "mongodb://someuser:somepasswd@localhost:27017/your_db_name"

OK,此时在命令行执行sbt compile,sbt会自动下载Play-ReactiveMongo依赖,并完成编译过程。

2.2 开发示例

2.2.1 定义Model和Controller

在定义Model时最好显式声明_id属性,因为该属性为MongoDB的默认主键,如果没有,在插入时会自动生成。下面代码定义了一个Person类,以及用于完成PersonJsObject之间相互转换的隐式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]
}

只要导入models.JsonFormats.personFormat这个隐式对象,我们便可以在PersonJsObject实现双向转换:

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内可以直接使用MongoController定义的方法和属性,例如database

import play.api.mvc.{ Action, Controller }
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.json._

//导入ReactiveMongo插件
import play.modules.reactivemongo.{ MongoController, ReactiveMongoApi, ReactiveMongoComponents }

//导入BSON-JSON conversions/collection
import reactivemongo.play.json._
import reactivemongo.play.json.collection._

//导入隐式的format对象,用于JsObject <-> Person之间相互转换
import models.JsonFormats._

class Application @Inject() (val reactiveMongoApi: ReactiveMongoApi) extends Controller 
    with MongoController with ReactiveMongoComponents {
    def personColFuture = database.map(_.collection[JSONCollection]("persons"))
    
    ...    
}

请注意,personColFuturedef而不是val,这样做的原因是为了适应Play框架的热加载功能。

2.2.2 插入操作

不同的修改操作会返回不同类型的WriteResult,通过该类型的WriteResult可以判断当前操作是否成功。JSONCollection.insert()方法返回类型为Future[WriteResult]类型,判断当前操作成功的条件是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")
  } 
}

所有的操作都是异步的,即返回结果类型为Future[T],你需要熟悉这种开发模式。

WriteResult.ok为true仅仅表明成功的读取了WriteResult响应,并不表示当前的操作一定执行成功了。

2.2.3 更新操作

JSONCollection.update()方法返回Future[UpdateWriteResult]UpdateWriteResult.n表示匹配条件的记录数量,UpdateWriteResult.nModified表示真实被修改的记录数量(不包含更新值和原值相同的记录,因为这些记录其实并没有被修改),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操作符,则意味着是用当前的文档替换原文档,例如:

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

上面的代码将会把符合条件的文档更新为只剩一个name属性的文档片段。

在使用update方法时,千万别忘记$set操作符,否则会造成数据丢失。

2.2.4 查询操作

JSONCollection.find()方法返回结果为GenericQueryBuilder类型,该类型用于构建查询语句,调用其cursor方法会触发查询请求并返回一个Cursor[T]类型,通过迭代该Cursor[T]我们可以收集查询结果。GenericQueryBuilder.one[T]方法等价于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()方法返回结果为Future[WriteResult]类型,WriteResult.n表示删除的记录数量。

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()方法设置分页信息,然后使用Cursor[T].collect[List]()方法收集前15条查询结果。利用JSONCollection.count()方法可以查询满足条件的记录总数。

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]转换成Documents,返回类型为MultiBulkWriteResultMultiBulkWriteResult.n表示成功插入的条数。

def testBulkInsert = Action.async {
  val list = List(Person("0", "p0", 30), Person("1", "p1", 30))
  personColFuture.flatMap{ personCol =>
    //将List[Person]转换成待插入的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方法,可以实现一个简单的消息队列或是任务领取功能,

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公司开发的MongoDB管理工具,非商业用途可以免费使用,如果是公司还是建议购买商业Licence。该工具基于Java开发,支持跨平台并且功能非常全面,例如在查询结果列表上可以直接进行编辑,Collections的复制粘贴和导入导出,用户角色和权限管理,是客户端管理的首选工具。

3.2 Robomongo

Robomongo前身是由Dmitry Schetnikovichk开发并维护的个人项目,目前已经被Studio 3T收购,并对外承诺永久免费使用。该工具基于Qt开发,支持跨平台,目前已经正式发布1.0版本。

4 小结

MongoDB自2009发布以来,产品和社区都已经非常成熟,已经有商业公司在云上提供MongoDB服务。除此之外,MongoDB不仅方便开发,而且容易维护,普通的开发人员利用自带的mongodumpmongorestore命令便可进行备份、恢复操作。当然最重要的是利用MongoDB的异步驱动和oplog可以开发高性能的实时应用,同时统一了前后端的数据结构,开发体验非常不错!最后再补充一句,如果对事务性要求较高,还是建议选择RDBMS。转载请注明作者joymufeng,欢迎来访PlayScala社区(http://www.playscala.cn/)。

你可能感兴趣的:(Play Scala 2.5.x - Play with MongoDB 开发指南)