0

I'm in the process of trying to combine some nested calls with reactivemongo in my play2 application. I get a list of objects returned from createObjects. I then loop over them, check if the object exist in the collection and if not insert them:

def dostuff() = Action {
    implicit request =>
      form.bindFromRequest.fold(
        errors => BadRequest(views.html.invite(errors)),
        form => {
        val objectsReadyForSave = createObjects(form.companyId, form.companyName, sms_pattern.findAllIn(form.phoneNumbers).toSet)
          Async {
            for(object <- objectsReadyForSave) {
                collection.find(BSONDocument("cId" -> object.get.cId,"userId" ->
                object.userId.get)).cursor.headOption.map { maybeFound =>
                maybeFound.map { found =>
                  Logger.info("Found record, do not insert")
                } getOrElse {
                  collection.insert(object)
                }
              }
            }
            Future(Ok(views.html.invite(form)))
          }            
          })
   }

I feel that this way is not as good as it can be and feels not "play2" and "reactivemongo". So my question is: How should I structure my nested calls to get the result I want and get the information of which objects that have been inserted?

jakob
  • 5,979
  • 7
  • 64
  • 103

2 Answers2

2

Here's how I'd rewrote it.

def dostuff() = Action { implicit request =>
  form.bindFromRequest.fold(
    errors => BadRequest(views.html.invite(errors)),
    form   => {
      createObjects(form.companyId,
        form.companyName,
        sms_pattern.findAllIn(form.phoneNumbers).toSet).map(ƒ)

      Ok(views.html.invite(form))
    }
  )
}

// ...
// In the model
// ...

def ƒ(cId: Option[String], userId: Option[String], logger: Logger) = {
  // You need to handle the case where obj.cId or obj.userId are None
  collection.find(BSONDocument("cId" -> obj.cId.get, "userId" -> obj.userId.get))
    .cursor
    .headOption
    .map { maybeFound =>
      maybeFound map { _ =>
        logger.info("Record found, do not insert")
      } getOrElse {
        collection.insert(obj)
      }
    }
}

There may be some syntax errors, but the idea is there.

Samy Dindane
  • 17,900
  • 3
  • 40
  • 50
  • Thank you for your answer! What if I instead of handling this in the model create a method def dostuff(object : MyObject) = {} and call .map(dostuff) instead? – jakob May 23 '13 at 06:53
  • That means you'll be handling forms stuff in your model, which is imo pretty messy. – Samy Dindane May 23 '13 at 08:09
  • I understand, but if you look at reactivemongo examples they usually handle form stuff in the models eg. https://github.com/sgodbillon/reactivemongo-demo-app/blob/master/app/models/articles.scala. Maybe if I could chat with you about the whole use case it would be easier to understand? – jakob May 23 '13 at 10:48
  • I personally would avoid doing that, but I guess it's a matter of taste. — Feel free to chat in here so everybody can maybe learn from it. :-) – Samy Dindane May 23 '13 at 11:20
  • Ok perfect, Thank you! So let's say we do it the reactivemongo way. How would you handle the return to the view? Collection insert will return Future[LastError]. So flow is: createObjects -> collection.find() -> getOrElse -> collection.insert() -> return inserted objects to view. – jakob May 24 '13 at 07:39
  • I suggest checking if the `Future[LastError]` is allright; if so return a `Success` containing the inserted elements, otherwise return `Failure`. Choose the right view accordingly in your controller. – Samy Dindane May 24 '13 at 08:43
2

I am not an expert in mongoDB neither in ReactiveMongo but it seems that you are trying to use a NoSQL database in the same way as you would use standard SQL databases. Note that mongoDB is asynchronous which means that operations may be executed in some future, this is why insertion/update operations do not return affected documents. Regarding your questions:

1 To insert the objects if they do not exist and get the information of which objects that have been inserted?

You should probably look at the mongoDB db.collection.update() method and call it with the upsert parameter as true. If you can afford it, this will either update documents if they already exist in database or insert them otherwise. Again, this operation does not return affected documents but you can check how many documents have been affected by accessing the last error. See reactivemongo.api.collections.GenericCollection#update which returns a Future[LastError].

2 For all the objects that are inserted, add them to a list and then return it with the Ok() call.

Once again, inserted/updated documents will not be returned. If you really need to return the complete affected document back, you will need to make another query to retrieve matching documents.

I would probably rewrite your code this way (without error/failure handling):

def dostuff() = Action {
    implicit request =>
        form.bindFromRequest.fold(
            errors => BadRequest(views.html.invite(errors)),
            form => {
                val objectsReadyForSave = createObjects(form.companyId, form.companyName, sms_pattern.findAllIn(form.phoneNumbers).toSet)
                Async {
                    val operations = for {
                        data <- objectsReadyForSave
                    } yield collection.update(BSONDocument("cId" -> data.cId.get, "userId" -> data.userId.get), data, upsert = true)

                    Future.sequence(operations).map {
                        lastErrors =>
                            Ok("Documents probably inserted/updated!")
                    }
                }
            }
        )
}

See also Scala Futures: http://docs.scala-lang.org/overviews/core/futures.html

This is really useful! ;)

almeidap
  • 256
  • 2
  • 11