shuang
shuang

Reputation: 246

Nested document with reactive mongo and Scala

I'm trying to store a nested document in MongoDB through Scala. The document looks like:

Project {
    "_id": ObjectId("528547370cf6e41449003512"),
    "highLevelCode": NumberLong(3),
    "description": [
        {"_id": ObjectId("528547370cf6e41449003521"),
        "lang": "en",
        "desc": "desc in English"}, 
        {"_id ": ObjectId("528547370cf6e41449003522"), 
        "lang": "fr", 
        "desc": "desc en francais"}],
    "budget": NumberLong(12345)
}

Basically I want to store nested descriptions, which could be of multiple languages in the Project document.

The code I wrote is:

import reactivemongo.bson._
import reactivemongo.bson.handlers.{BSONWriter, BSONReader}
import reactivemongo.bson.BSONLong
import reactivemongo.bson.BSONString

case class LocaleText(
  id: Option[BSONObjectID],
  lang: String,
  textDesc: String
)

object LocaleText {
  implicit object LocaleTextBSONReader extends BSONReader[LocaleText] {
    def fromBSON(document: BSONDocument): LocaleText = {
      val doc = document.toTraversable

      LocaleText(
        doc.getAs[BSONObjectID]("_id"),
        doc.getAs[BSONString]("lang").map(_.value).get,
        doc.getAs[BSONString]("textDesc").map(_.value).get
      )
    }
  }

  implicit object LocaleTextBSONWriter extends BSONWriter[LocaleText] {
    def toBSON(localText: LocaleText) = {
      BSONDocument(
        "_id" -> localText.id.getOrElse(BSONObjectID.generate),
        "lang" -> BSONString(localText.lang),
        "textDesc" -> BSONString(localText.textDesc)
      )
    }
  }    
}

case class Project(
  id:                Option[BSONObjectID],
  description:          List[LocaleText],
  budget:               Option[Long]
  )

object Project {

  implicit object ProjectReader extends BSONReader[Project]{
    def fromBSON(doc: BSONDocument): Project = {
      val document = doc.toTraversable

      Project(
        document.getAs[BSONObjectID]("_id"),
        document.getAs[BSONArray]("description").map { values =>
            values.values.toList.flatMap { case value =>
              value match {
                case v: LocaleText => Some(v.asInstanceOf[LocaleText])
                case _ => None
              }
            }
        }.getOrElse(List.empty),
        document.getAs[BSONLong]("budget").map(_.value)
      )
    }
  }

  implicit object ProjectWriter extends BSONWriter[Project]{
    def toBSON(project: Project): BSONDocument = {
      BSONDocument(
        "_id"                   -> project.id.getOrElse(BSONObjectID.generate),
    "description"           -> BSONArray(project.description)
      ).append(Seq(
        project.budget.map(b => "budget" -> BSONLong(b))
      ).flatten:_*)
    }
  }
}

However, it gave me compilation error like

overloaded method value apply with alternatives: [error] (producer: reactivemongo.bson.Implicits.Producer[(String, reactivemongo.bson.BSONValue)],producers: reactivemongo.bson.Implicits.Producer[(String, reactivemongo.bson.BSONValue)])reactivemongo.bson.AppendableBSONDocument [error] (els: (String, reactivemongo.bson.BSONValue))reactivemongo.bson.AppendableBSONDocument [error] cannot be applied to ((String, reactivemongo.bson.BSONObjectID), List[LocaleText])...

Basically Scala doesn't like the line "description" -> BSONArray(project.description)

However, the following alternative works although I cannot use a List/Array to allow more than two languages:

case class LocaleText(
  enDesc: String,
  frDesc: String)

case class Project(
  id:                   Option[BSONObjectID],
  description:          LocaleText)

object Project {
implicit object LocaleTextBSONReader extends BSONReader[LocaleText] {
    def fromBSON(document: BSONDocument): LocaleText = {
      val doc = document.toTraversable

      LocaleText(
        doc.getAs[BSONString]("enDesc").map(_.value).get,
        doc.getAs[BSONString]("frDesc").map(_.value).get
      )
    }
  }

  implicit object LocaleTextBSONWriter extends BSONWriter[LocaleText] {
    def toBSON(localText: LocaleText) = {
      BSONDocument(
        "enDesc" -> BSONString(localText.enDesc),
        "frDesc" -> BSONString(localText.frDesc)
      )
    }
  }    

implicit object ProjectReader extends BSONReader[Project]{
def fromBSON(doc: BSONDocument): Project = {
  val document = doc.toTraversable

  Project(
    document.getAs[BSONObjectID]("_id"),
    document.getAs[BSONString]("iatiId").map(_.value).get,
    LocaleTextBSONReader.fromBSON(document.getAs[BSONDocument]("description").get)
  }
}

implicit object ProjectWriter extends BSONWriter[Project]{
def toBSON(project: Project): BSONDocument = {
  BSONDocument(
    "_id"                   -> project.id.getOrElse(BSONObjectID.generate),
    "iatiId"                -> BSONString(project.iatiId),
    "description"           -> LocaleTextBSONWriter.toBSON(project.description) 
 }
}

How can I convert project.description, which a List of LocaleText to BSONArray for Mongo? I appreciate if you can shed some light on my problem. Thank you very much for your help.

Upvotes: 1

Views: 4596

Answers (2)

shuang
shuang

Reputation: 246

Finally I found the solution to my own question, hope this will help some others who struggle with ReactiveMongo 0.8 as well:

case class LocaleText(
    lang: String,
    desc: String)

case class Project(
    id:                 Option[BSONObjectID],
    descriptions:         List[LocaleText])

object Project {
  implicit object LocaleTextBSONReader extends BSONReader[LocaleText] {
        def fromBSON(document: BSONDocument): LocaleText = {
          val doc = document.toTraversable

          LocaleText(
            doc.getAs[BSONString]("lang").get.value,
            doc.getAs[BSONString]("desc").get.value
          )
        }
      }

      implicit object LocaleTextBSONWriter extends BSONWriter[LocaleText] {
        def toBSON(localText: LocaleText) = {
          BSONDocument(
            "lang" -> BSONString(localText.lang),
            "desc" -> BSONString(localText.desc)
          )
        }
      }    

  implicit object ProjectReader extends BSONReader[Project]{
    def fromBSON(doc: BSONDocument): Project = {
      val document = doc.toTraversable

      Project(
        document.getAs[BSONObjectID]("_id"),
        document.getAs[BSONArray]("descriptions").get.toTraversable.toList.map { descText =>
            LocaleTextBSONReader.fromBSON(descText.asInstanceOf[TraversableBSONDocument]
        }
      )
    }
  }

  implicit object ProjectWriter extends BSONWriter[Project]{
    def toBSON(project: Project): BSONDocument = {
      BSONDocument(
        "_id"                   -> project.id.getOrElse(BSONObjectID.generate),
        "descriptions"          -> BSONArray(project.descriptions.map {
        description => LocaleTextBSONWriter.toBSON(description)
      }: _*)
    }
  }

Upvotes: 3

tmbo
tmbo

Reputation: 1317

It might be an issue in the library. I tested your code using the latest version of reactivemongo and it compiled just fine (I needed to adapt your code to fit the new syntax for BSONReaders and BSONWriters but that shouldn't have any influence on the error).

Using reactivemongo 0.10.0 you can even use the newly provided macros:

import reactivemongo.bson._

case class LocaleText(id: Option[BSONObjectID], lang: String, textDesc: String)

object LocaleText {
  implicit val localeTextBSONHandler = Macros.handler[LocaleText]
}

case class Project(id: Option[BSONObjectID], description: List[LocaleText], budget: Option[Long])

object Project {
  implicit val projectBSONHandler = Macros.handler[Project]
}

Upvotes: 2

Related Questions