Search code examples
mongodbscalaplayframework-2.0reactivemongoplay-reactivemongo

Using case class for json validation and MongoDB persistense (Reactivemongo), what about id?


So, I have a case class as well as readers and writers for both JSON and BSONDocument format.

Problem is, when inserting to MongoDB, I want to be able to specify the BSONObjectID, so I can return it upon creation. However, if I add a id: BSONObjectID in the case class, I cannot find a way to get the JSON validation/transformation to work.

This is my code:

case class Mini(username: String, email: String, quizAnswer1: List[String] )

implicit object MiniWriter extends BSONDocumentWriter[Mini] {
  def write(mini: Mini): BSONDocument = BSONDocument(
    "username" -> mini.username,
    "email" -> mini.email,
    "quizAnswer1" -> mini.quizAnswer1
  )
}

implicit object MiniReader extends BSONDocumentReader[Mini] {
  def read(doc: BSONDocument): Mini = Mini(
    doc.getAs[String]("username").get,
    doc.getAs[String]("email").get,
    doc.getAs[List[String]]("quizAnswer1").toList.flatten
  )
}

implicit val miniReads: Reads[Mini] = (
  (JsPath \ "username").read[String] and
  (JsPath \ "email").read[String] and
  (JsPath \ "quizAnswer1").read[List[String]]
)(Mini.apply _)

implicit val miniWrites: Writes[Mini] = (
  (JsPath \ "username").write[String] and
  (JsPath \ "email").write[String] and
  (JsPath \ "quizAnswer1").write[List[String]]
)(unlift(Mini.unapply))

I really want to avoid working with duplicate model representations of the same model. Any ideas?


Solution

  • If you don't need the id in the model itself you could just use it temporarly while your operations. On a PUT you could use a following base implementation:

    def insert(t: T)(implicit ctx: ExecutionContext): Future[BSONObjectID] = {
        val id = BSONObjectID.generate
        val obj = format.writes(t).as[JsObject]
        obj \ "_id" match {
          case _: JsUndefined =>
            coll.insert(obj ++ Json.obj("_id" -> id)).map { _ => id }
    
         case JsObject(Seq((_, JsString(oid)))) =>
            coll.insert(obj).map { _ => BSONObjectID(oid) }
    
         case JsString(oid) =>
            coll.insert(obj).map { _ => BSONObjectID(oid) }
    
         case f => sys.error(s"Could not parse _id field: $f")
    }
    

    }

    On an update the id is provided by the POST request.

    When you query the db you could use the following base implementation to get the ids of the resultset temporary

    def find(sel: JsObject, limit: Int = 0, skip: Int = 0, sort: JsObject = Json.obj(), projection: JsObject = Json.obj())(implicit ctx: ExecutionContext): Future[Traversable[(T, BSONObjectID)]] = {
        val cursor = coll.find(sel).projection(projection).sort(sort).options(QueryOpts().skip(skip).batchSize(limit)).cursor[JsObject]
        val l = if (limit != 0) cursor.collect[Traversable](limit) else cursor.collect[Traversable]()
        l.map(_.map(js => (js.as[T], (js \ "_id").as[BSONObjectID])))
    }