Question

When I update a document in MongoDB, I usually proceed like this:

  1. Find the current document and get the version number
  2. Increase the version number and save the new document
  3. Save the old document in a vermongo collection

Here is my solution – I'm sorry for the amount of code... but it is necessary to let you understand:

trait MyDaoComponent {

  private val toObjectId = Writes[String] {
    id => Json.obj("_id" -> Json.obj("$oid" -> id))
  }

  private val toUpdateObject = (__ \ '$set).json.copyFrom(__.json.pick)

  private def setVersion(version: Option[Int]) = __.json.update(
    version match {
      case Some(v) => (__ \ '_version).json.put(JsNumber(v))
      case None => (__ \ '_version).json.prune
    }
  )

  private def toVersioned(deleted: Boolean) = (__.json.update(
    ( __ \ '_id ).json.copyFrom((__ \ '_id \ '$oid).json.pickBranch) and
    ( __ \ '_id \ '_version).json.copyFrom((__ \ '_version).json.pick) and
    (__ \ '_version).json.update(of[JsValue].map {
      case JsNumber(oldVersion) => if (deleted) JsString(f"deleted:$oldVersion%.0f") else JsNumber(oldVersion)
      case _ => JsNull
    }) and
    (__ \ '_timestamp).json.put(Json.toJson(LocalDateTime.now)) reduce) andThen
    ( __ \ '_id \ '$oid ).json.prune
  )

  ...

  def update(entity: A): Future[Boolean] = {
    entity.id match {
      case Some(id) =>
        val objectId = toObjectId.writes(id)
        collection.find(objectId).cursor[JsValue].headOption.flatMap { _ match {
          case Some(json) =>
            collection.update(
              objectId,
              entity.asJson.transform(
                (__ \ '_id).json.prune andThen 
                setVersion(Some((json as (__ \ '_version).read[Int]) + 1)) andThen
                toUpdateObject
              ).get
            ).flatMap { updateLastError => if (updateLastError.ok) {
              version(json, false).flatMap { versionLastError =>
                if (versionLastError.ok) Future.successful(updateLastError)
                else Future.failed(versionLastError)
              }}
              else Future.failed(updateLastError)
            }
          case None => Future.successful(LastError(true, None, None, None, None, 0, false))
        }}.transform(
          success => if (success.n > 0) true else false,
          failure => failure match { case e: LastError => e.code match { 
            case Some(11001) => DaoServiceException(e.message, Some(DUPLICATE_KEY_ON_UPDATE))
            case _ => DaoServiceException(e.message, Some(DATABASE_ERROR))
          }}
        )
      case None => Future.failed(DaoServiceException("'id' not defined", Some(MISSING_FIELD)))
    } 
  }

  private def version(document: JsValue, deleted: Boolean): Future[LastError] = {
    shadowCollection.insert(document.transform(toVersioned(deleted)).get)
  }
}

The code above works... but I'd like to replace the calls to find and update with an atomic command. I've tried with FindAndModify... but it requires BSONDocuments while I'm working with pure JSON (the update method takes entity as an argument, which privide a JSON representation of the data).

Is there a way to convert JSON to BSON so that I can use FindAndModify?

Was it helpful?

Solution

Just looked at Play-ReactiveMongo and figured out they provide play.modules.reactivemongo.json.BSONFormats to convert BSON to/from JSON. I hope that helps.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top