Reputation: 21
I am trying to perform what is probably a simple operation, but running into difficulties: I have a Play controller that creates a user in Mongo, but I first want to verify that there is not already a user with the same email address. I have a function on my User object that searches for a User by email address and returns a Future[Option[User]]:
def findByEmail(email: String): Future[Option[User]] = {
collection.find(Json.obj("email" -> email)).one[User]
}
My controller function that searches for a User by email works:
def get(id: String) = Action.async {
User.findById(id).map {
case None => NotFound
case user => Ok(Json.toJson(user))
}
}
I have a function that creates a user:
def create(user:User): Future[User] = {
// Generate a new id
val id = java.util.UUID.randomUUID.toString
// Create a JSON representation of the user
val json = Json.obj(
"id" -> id,
"email" -> user.email,
"password" -> user.password,
"firstName" -> user.firstName,
"lastName" -> user.lastName)
// Insert it into MongoDB
collection.insert(json).map {
case writeResult if writeResult.ok == true => User(Some(id), user.email, user.password, user.firstName, user.lastName)
case writeResult => throw new Exception(writeResult.message)
}
}
And the corresponding controller function works:
def post = Action.async(parse.json) {
implicit request =>
request.body.validate[User].map {
user => User.create(user).map {
case u => Created(Json.toJson(u))
}
}.getOrElse(Future.successful(BadRequest))
}
But when I modify the post method to first check for a User with the specified email it fails:
def post = Action.async(parse.json) {
implicit request =>
request.body.validate[User].map {
user => User.findByEmail(user.email).map {
case None => User.create(user).map {
case u => Created(Json.toJson(u))
}
case u => BadRequest
}
}.getOrElse(Future.successful(BadRequest))
}
It reports that while it expects a Future[Result], it found a Future[Object]. I think the error means that it ultimately found a Future[Future[Result]], which is not what it expects.
My question is: what is the best practice for chaining such calls together? Should I add an Await.result() call to wait for the first operation to complete before proceeding? Will that cause any unwanted synchronous operations to occur? Or is there a better way to approach this problem?
Thanks in advance!
Upvotes: 2
Views: 829
Reputation: 12214
There are two problems with your code. Looking just to this block for awhile:
case None => create(user).map {
case u => Created("")
}
case u => BadRequest
First, create(user).map { ... }
returns a Future[Result]
, but case u => BadRequest
returns a Result
, then the compiler goes to a more "wide" type, which is Object
. Let's separate this block (changes just to illustrate my point):
val future: Future[Object] = findByEmail("").map {
case Some(u) => BadRequest
case None => create(User()).map {
case u => Created("")
}
}
Now, it is clear that both case blocks must return the same type:
val future: Future[Future[Result]] = findByEmail("").map {
case Some(u) => Future.successful(BadRequest)
case None => create(User()).map {
case u => Created("")
}
}
Notice how I've changed from case Some(u) => BadRequest
to case Some(u) => Future.successful(BadRequest)
and now we have Future[Future[Result]]
, which is not what we want and shows the second problem. Let's see the Future.map
signature:
def map[S](f: T => S)(implicit executor: ExecutionContext): Future[S]
Forget about the implicit executor, because it is irrelevant for this discussion:
def map[S](f: T => S): Future[S]
So, we receive a block that transforms from T
to S
and we then wrap S
into a Future
:
val futureInt: Future[Int] = Future.successful(1)
val futureString: Future[String] = futureInt.map(_.toString)
But what if the block returns another Future
? Then it will be wrapped and you will get a Future[Future[...]]
:
val futureFuture: Future[Future[String]] = futureInt.map(v => Future.successful(v.toString))
To avoid the wrap, we need to use flatMap
instead of map
:
val futureInt: Future[Int] = Future.successful(1)
val futureString: Future[String] = futureInt.flatMap(v => Future.successful(v.toString))
Let's go back to your code and use a flatMap
instead:
val future: Future[Result] = findByEmail("").flatMap {
case Some(u) => Future.successful(BadRequest)
case None => create(User()).map {
case u => Created("")
}
}
And then, the final version will be:
def post = Action.async(parse.json) { implicit request =>
request.body.validate[User].map { user =>
findByEmail(user.email) flatMap { // flatMap instead of map
case Some(u) => Future.successful(BadRequest) // wrapping into a future
case None => create(user).map {
case u => Created(Json.toJson(u))
}
}
}.getOrElse(Future.successful(BadRequest))
}
Upvotes: 4