play-mongo是一个专门为 PlayFramework 开发的Mongodb模块,旨在为PlayFramework提供一种简洁的Mongodb访问方式。该项目基于Mongodb官方的 MongodbScalaDriver 开发,并且提供了更多实用功能,例如,
更简洁多样的数据库交方式
自动识别模型类(Model),自动编解码
自动完成JsValue和BsonValue互转
更方便的GridFS交互
ChangeStream转AkkaStream.
支持关联查询(RelationshipQuery)
GettingStarted打开build.sbt,添加如下依赖,
libraryDependencies += "cn.playscala" % "play-mongo_2.12" % "0.1.0"打开 conf/application.conf,添加数据库连接,
mongodb.uri = "mongodb://user:password@host:port/play-community?authMode=scram-sha1"然后需要配置 Model 位置,配置代码需要在应用启动之前执行,
Mongo.setModelsPackage("models")建议将上述代码放置在顶层包路径下的默认 Module 类中,
class Module extends AbstractModule { override def configure() = { Mongo.setModelsPackage("models") bind(classOf[InitializeService]).asEagerSingleton }}至此便可以将 Mongo 实例注入到任意需要的地方,
@Singletonclass Application @Inject()(cc: ControllerComponents, mongo: Mongo) extends AbstractController(cc) {}ModelandCollectionModel类使用 @Entity 注解标注,一个model实例表示mongodbcollection中的一个文档,一个mongodbcollection在概念上类似于关系数据库的一张表。
@Entity("common-user")case class User(_id: String, name: String, password: String, addTime: Instant)@Entity 注解参数用于指定关联的mongodbcollection名称,如果未指定,则默认为Model类名称。作为约定,Model类使用 _id 字段作为唯一标识,该字段同时也是mongodbcollection的默认主键。
我们可以通过两种方式访问mongodbcollection,第一种方式是使用model类,
mongo.find[User]().list().map{ users => ... }这里的参数类型 User 不仅用于指定关联的mongodbcollection,而且用于指明返回的结果类型。这意味着查询操作将会在 common-user collection上执行,并且返回的结果类型是 User。需要注意的是,在该方式下无法改变返回的结果类型。
第二种方式是使用 mongo.getCollection 方法,
mongo.collection("common-user").find[User]().list().map{ user => }在这里, find 方法上的参数类型 User 仅仅用于指定返回的结果类型,我们可以通过更改该参数类型设置不同的返回结果类型,
mongo.collection("common-user").find[JsObject]().list().map{ jsObjList => }mongo.collection("common-user").find[CommonUser](Json.obj("userType" -> "common")).list().map{ commonUsers => }当然,我们也可以使用model类指定关联的mongodbcollection,
mongo.collection[User].find[User]().list().map{ user => }第1个参数类型 User 用于指定关联的mongodbcollection,第2个参数类型 User 用于指定返回的结果类型。我们仍然可以通过改变第2个参数类型从而改变返回的结果类型。
常用操作以下示例代码默认执行了 importplay.api.libs.json.Json._ 导入,所以 Json.obj() 可以被简写为 obj() 。
Create// 插入 Modelmongo.insert[User](User("0", "joymufeng", "123456", Instant.now))// 插入 Jsonval jsObj = obj("_id" -> "0", "name" -> "joymufeng", "password" -> "123456", "addTime" -> Instant.now)mongo.collection[User].insert(jsObj)mongo.collection("common-user").insert(jsObj)Updatemongo.updateById[User]("0", obj("$set" -> obj("password" -> "123321")))mongo.updateOne[User](obj("_id" -> "0"), obj("$set" -> obj("password" -> "123321")))mongo.collection[User].updateById("0", obj("$set" -> obj("password" -> "123321")))mongo.collection[User].updateOne(obj("_id" -> "0"), obj("$set" -> obj("password" -> "123321")))mongo.collection("common-user").updateById("0", obj("$set" -> obj("password" -> "123321")))mongo.collection("common-user").updateOne(obj("_id" -> "0"), obj("$set" -> obj("password" -> "123321")))Querymongo.findById[User]("0") // Future[Option[User]]mongo.find[User](obj("_id" -> "0")).first // Future[Option[User]]mongo.collection[User].findById[User]("0") // Future[Option[User]]mongo.collection[User].find[User](obj("_id" -> "0")).first // Future[Option[User]]mongo.collection[User].findById[JsObject]("0") // Future[Option[JsObject]]mongo.collection[User].find[JsObject](obj("_id" -> "0")).first // Future[Option[JsObject]]mongo.collection("common-user").findById[User]("0") // Future[Option[User]]mongo.collection("common-user").find[User](obj("_id" -> "0")).first // Future[Option[User]]mongo.collection("common-user").findById[JsObject]("0") // Future[Option[JsObject]]mongo.collection("common-user").find[JsObject](obj("_id" -> "0")).first // Future[Option[JsObject]]Deletemongo.deleteById[User]("0")mongo.deleteOne[User](obj("_id" -> "0"))mongo.collection[User].deleteById("0")mongo.collection[User].deleteOne(obj("_id" -> "0"))mongo.collection("common-user").deleteById("0")mongo.collection("common-user").deleteOne(obj("_id" -> "0"))UploadandDownloadFiles// Upload and get the fileIdmongo.gridFSBucket.uploadFromFile("image.jpg", "image/jpg", new File("./image.jpg")).map{ fileId => Ok(fileId)}// Download file by fileIdmongo.gridFSBucket.findById("5b1183fed3ba643a3826325f").map{ case Some(file) => Ok.chunked(file.stream.toSource) .as(file.getContentType) case None => NotFound}ChangeStream我们可以通过 toSource 方法将ChangeStream转换成AkkaSource,之后便会有趣很多。例如下面的代码拥有如下几个功能:
将从ChangeStream接收到的元素进行缓冲,以方便批处理,当满足其中一个条件时便结束缓冲向后传递:
缓冲满10个元素
缓冲时间超过了1000毫秒
对缓冲后的元素进行流控,每秒只允许通过1个元素
mongo .collection[User] .watch() .fullDocument .toSource .groupedWithin(10, 1000.millis) .throttle(elements = 1, per = 1.second, maximumBurst = 1, ThrottleMode.shaping) .runForeach{ seq => // ... }RelationshipQuery@Entity("common-article")case class Article(_id: String, title: String, content: String, authorId: String)@Entity("common-author")case class Author(_id: String, name: String)mongo.find[Article].fetch[Author]("authorId").list().map{ _.map{ t => val (article, author) = t }}对于满足查询条件的每一个article,将会根据匹配条件 article.authorId==author._id 拉取关联的author。
Class,Json和Bson在处理Json时要格外小心,因为Json使用 JsNumber 表示所有数值类型,但是Bson拥有更加丰富的数值类型,这导致了Json和Bson之间的转换过程是不可逆的,因为双方的类型信息并不对称。下面我们仔细分析常见的几个场景。在讨论中将会用到如下的Model定义:
@Entity("common-user")case class User(_id: String, name: String, setting: UserSetting)case class UserSetting(gender: String, age: Int)Class->Bson我们经常使用下面代码将一个Model类实例插入mongodb,
mongo.insert[User].insert(User("0", "joymufeng", UserSetting("male", 32)))在调用底层驱动的插入操作之前,需要先将 User 转换成 Bson 。这个转换过程是可逆的,当从mongodb读取数据时,可以成功地将 Bson 转换回 User。
Json->Bson当使用JsonDSL构建一个 JsObject 对象时,所有的数值类型(例如Byte,Short,Int,Long,Float和Double)均会被转换成 JsNumber 类型(内部使用BigDecimal存储数据),数值的具体类型在这个转换过程中丢失了。在调用底层驱动程序前,Json 将会被转换为 Bson,JsNumber 将会被转换为 BsonDecimal128。当从数据库读取写入的数据时,我们没办法恢复已经丢失的数值类型信息。例如我们通常会执行如下更新操作,
mongo.update[User](Json.obj("_id" -> "0"), Json.obj("$set" -> UserSetting("male", 18)))// Ormongo.update[User](Json.obj("_id" -> "0"), Json.obj("$set" -> Json.obj("setting" -> Json.obj("gender" -> "male", "age" -> 18))))不管是 UserSetting("male",32),还是 Json.obj("gender"->"male","age"->18) 最终都会被转换为 Json.obj("gender"->JsString("male"),"age"->JsNumber(BigDecimal(18))。所以,在更新操作执行完成后, user.setting.age 字段在数据库中的类型为 NumberDecimal,当执行读取操作时便会发生类型转换错误,
mongo.findById[User]("0")// [BsonInvalidOperationException: Invalid numeric type, found: DECIMAL128]当试图将 BigDecimal 转换为 Int 时出错了.因为在这个转换过程中会导致数值精度丢失。为了解决这个问题,我们在转换 JsNumber 时尽量将其转换为较窄的数值类型,以保证其可以被安全地转换回来。例如 Json.obj("age"->JsNumber(18.0))会被转换为 BsonDocument("age",BsonInt32(18))。
评论