Question

I've a MongoDB collection where I store User documents like this:

{
    "_id" : ObjectId("52d14842ed0000ed0017cceb"),
    "email": "joe@gmail.com",
    "firstName": "Joe"
    ...
}

Users must be unique by email address, so I added an index for the email field:

collection.indexesManager.ensure(
  Index(List("email" -> IndexType.Ascending), unique = true)
)

And here is how I insert a new document:

def insert(user: User): Future[User] = {
  val json = user.asJson.transform(generateId andThen copyKey(publicIdPath, privateIdPath) andThen publicIdPath.json.prune).get
  collection.insert(json).map { lastError =>
    User(json.transform(copyKey(privateIdPath, publicIdPath) andThen privateIdPath.json.prune).get).get
  }.recover {
    throw new IllegalArgumentException(s"an user with email ${user.email} already exists")
  }
} 

In case of error, the code above throws an IllegalArgumentException and the caller is able to handle it accordingly. BUT if I modify the recover section like this...

def insert(user: User): Future[User] = {
  val json = user.asJson.transform(generateId andThen copyKey(publicIdPath, privateIdPath) andThen publicIdPath.json.prune).get
  collection.insert(json).map { lastError =>
    User(json.transform(copyKey(privateIdPath, publicIdPath) andThen privateIdPath.json.prune).get).get
  }.recover {
    case e: Throwable => throw new IllegalArgumentException(s"an user with email ${user.email} already exists")
  }
}

... I no longer get an IllegalArgumentException, but I get something like this:

play.api.Application$$anon$1: Execution exception[[IllegalArgumentException: DatabaseException['E11000 duplicate key error index: gokillo.users.$email_1  dup key: { : "giuseppe.greco@agamura.com" }' (code = 11000)]]]

... and the caller is no longer able to handle the exception as it should. Now the real questions are:

  1. How do I handle the diverse error types (i.e. the ones provided by LastError) in the recover section?
  2. How do I ensure the caller gets the expected exceptions (e.g. IllegalArgumentException)?
Was it helpful?

Solution

Finally I was able to manage things correctly. Here below is how to insert an user and handle possible exceptions with ReactiveMongo:

val idPath = __ \ 'id
val oidPath = __ \ '_id

/**
  * Generates a BSON object id.
  */
protected val generateId = __.json.update(
  (oidPath \ '$oid).json.put(JsString(BSONObjectID.generate.stringify))
)

/**
  * Converts the current JSON into an internal representation to be used
  * to interact with Mongo.
  */
protected val toInternal = (__.json.update((oidPath \ '$oid).json.copyFrom(idPath.json.pick))
  andThen idPath.json.prune
)

/**
  * Converts the current JSON into an external representation to be used
  * to interact with the rest of the world.
  */
protected val toExternal = (__.json.update(idPath.json.copyFrom((oidPath \ '$oid).json.pick))
  andThen oidPath.json.prune
)

...

def insert(user: User): Future[User] = {
  val json = user.asJson.transform(idPath.json.prune andThen generateId).get
  collection.insert(json).transform(
    success => User(json.transform(toExternal).get).get,
    failure => DaoServiceException(failure.getMessage)
  )
}

The user parameter is a POJO-like instance with an internal representation in JSON – User instances always contain valid JSON since it is generated and validated in the constructor and I no longer need to check whether user.asJson.transform fails.

The first transform ensures there is no id already in the user and then generates a brand new Mongo ObjectID. Then, the new object is inserted in the database, and finally the result converted back to the external representation (i.e. _id => id). In case of failure, I just create a custom exception with the current error message. I hope that helps.

OTHER TIPS

My experience is more with the pure java driver, so I can only comment on your strategy for working with mongo in general -

It seems to me that all you're accomplishing by doing the query beforehand is duplicating mongos uniqueness check. Even with that, you still have to percolate an exception upwards because of possible failure. Not only is this slower, but it's vulnerable to a race condition because the combination of your query + insert is not atomic. In that case you'd have

  • request 1: try to insert. email exists? false - Proceed with insert
  • request 2: try to insert. email exists? false - Proceed with insert
  • request 1: succeed
  • request 2: mongo will throw the database exception.

Wouldn't it be simpler to just let mongo throw the db exception and throw your own illegal argument if that happens?

Also, pretty sure the id will be generated for you if you omit it, and that there's a simpler query for doing your uniqueness check, if that's still the way you want to code it. At least in the java driver you can just do

collection.findOne(new BasicDBObject("email",someemailAddress))

Take a look at upsert mode of the update method (section "Insert a New Document if No Match Exists (Upsert)"): http://docs.mongodb.org/manual/reference/method/db.collection.update/#insert-a-new-document-if-no-match-exists-upsert

I asked a similar question a while back on reactivemongo's google group. You can have another case inside the recovery block to match a LastError object, query its error code, and handle the error appropriately. Here's the original question:

https://groups.google.com/forum/#!searchin/reactivemongo/alvaro$20naranjo/reactivemongo/FYUm9x8AMVo/LKyK01e9VEMJ

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