最近在做 Play framework for Scala 项目,数据库用的是 mongodb ,使用 ReactiveMongodb 模块来连接数据库,这套技术体系是第一次使用,遇到最频繁的问题就是如何将case class object 转换成JsObject然后再通过ReactiveMongodb转成BSONObject保存在mongodb数据库中,case class object 和 JsObject 如何相互转换。
傻方法
最傻的方法,手动给每一个case class 实现互转的read和write函数 比如:
import models.BaseModel import play.api.libs.json.{JsObject, JsResult, JsSuccess, JsValue, Json, OWrites, Reads} case class Test(name: String, value1: Int, value2: Double, value3: String) extends BaseModel[Test] object Test { implicit object reads extends Reads[Test] { def reads(json: JsValue): JsResult[Test] = json match { case obj: JsObject => { val name = (obj \ "name").asOpt[String].getOrElse("") val value1 = (obj \ "value1").asOpt[Int].getOrElse(0) val value2 = (obj \ "value2").asOpt[Double].getOrElse(0.0) val value3 = (obj \ "value3").asOpt[String].getOrElse("") JsSuccess(Test(name, value1, value2, value3)) } } } implicit object writes extends OWrites[Test] { def writes(test: Test): JsObject = Json.obj( "name" -> test.name, "value1" -> test.value1, "value2" -> test.value2, "value3" -> test.value3, ) } }
懒方法
Play framework 提供了 json-macro 来很轻松的实现这个功能,参考官方文档 ,只需要一行代码就可以代替上面的read和write函数。其原理是在编译的时候自动生成上面read和write函数,有了json-macro,只需简单一句就可搞定,比如:
import models.BaseModel import play.api.libs.json._ case class Test(name: String, value1: Int, value2: Double, value3: String) extends BaseModel[Test] object Test { // Generates Writes and Reads for App thanks to Json Macros implicit val format = Json.format[Test] }
自以为是的方法
还可以通过反射,在运行时动态转换,在BaseModel中利用scala反射动态获取case class object的字段先转成map,再转成JsObject, 例如:
package models import java.util.Date import play.api.libs.json.{JsBoolean, JsNull, JsNumber, JsObject, JsString, Json} import reactivemongo.bson.BSONObjectID import utils.JsonUtils import scala.reflect.runtime.universe._ class BaseModel[T: TypeTag] { def tag: TypeTag[T] = typeTag[T] /** * Convert model object to JsObject. * @return */ def toJsObject: JsObject = { val map = fieldsMap() val jsObj = JsObject(map.map { item => item._1 -> ( item._2 match { case value: String => JsString(value) case value: Int => JsNumber(value) case value: Double => JsNumber(value) case value: Boolean => JsBoolean(value) case value: Date => JsNumber(value.getTime) case value: BSONObjectID => Json.obj("$oid" -> JsString(value.stringify)) case _ => JsNull }) }) jsObj } /** * All field without null => value Map * @return */ def fieldsMap() : Map[String, Any] = { val tpe = this.tag.tpe val allAccessors = tpe.decls.collect { case meth: MethodSymbol if meth.isCaseAccessor => meth } val m = runtimeMirror(getClass.getClassLoader) val im = m.reflect(this) var map = Map[String, Any]() allAccessors.foreach { symbol => val fieldName = symbol.name.toString val fieldMirror = im.reflectField(symbol) val fieldValue = fieldMirror.get if (fieldValue != null) { map = map.updated(fieldName, fieldValue) } } map } }
比较
第一种方法比较简单,而且也很灵活,但是需要手写大量代码。第二种方法最简单,但是没办法修改转换的逻辑,不够灵活。第三种方法写起来比较难,可以自己定义转换逻辑,比较灵活,但是又一个最大的问题,运行很慢,大约比其他两种方法慢三倍,所以高并发的场景慎用,另外利用Scala反射将Model object转成JsObject相对容易实现,反过来要难很多,目前博主还没有实现这个功能。。。
为了比较三者的性能,设计了一个循环100w次,不断的创建对象并转换成JsObject,看三种方法所需的时间,测试代码:
object Main { def main(args: Array[String]): Unit = { var count_succ = 0 val json = Json.obj("name" -> "zapya", "value1" -> 11, "value2" -> 33.234, "value3" -> "zapyaValue") val startTime = System.currentTimeMillis() for (a <- 1 to 1000000) { val obj = Test("zapya", 11, 33.234, "zapyaValue") // val jsonConvert = Test.writes.writes(obj) // 方法一 val jsonConvert = Test.format.writes(obj) // 方法二 // val jsonConvert = obj.toJsObject //方法三 if (jsonConvert.equals(json)) { count_succ += 1 } } val time = System.currentTimeMillis() - startTime println(s"take $time, success count $count_succ") } }
结果:
方法一:take 4173, success count 1000000
方法二:take 5951, success count 1000000
方法三:take 18873, success count 1000000
Comments | NOTHING