Reputation: 31
I've really been struggling with some of these concepts in Scala and Play. I want to update my database, and I think I need to wrap my database update function in a Future, but I don't know how to return the required play.api.mvc.Result
.
I have a controller in Scala that returns some response:
def somePath = MyCustomAction.async(parse.tolerantJson) { implicit request =>
request.body.validate[MyObject].map { myObject =>
for {
getSomething <- getSomethingFuture
getSomethingElse <- getSomethingElseFuture
} yield {
if (getSomethingElse) {
if (getSomething)
updateMyDatabase(myObject)//perform database request
else
BadRequest("Invalid request")
}
} else {
// do some other things
}
}
}
private [this] def updateMyDatabase(myObject: MyObject) {
// do things to update the database
}
Should updateMyDatabase be returning a Result
? Do I want to wrap it in a Future and check if it completed successfully? And if I check, do I then return the correct Result
on the Success
method?
Right now, I'm not understanding how to structure these things or how to actually implement a Future
based solution.
Upvotes: 3
Views: 435
Reputation: 6114
@soote's answer works, but one reason to have updateMyDatabase
return a future is so you can unify error handling using Future#recover
. Another reason is so you can reuse the method in other places, since Play kind of expects blocking operations to be done in Future
s
If updateMyDatabase
returns a Future[_]
you can do something like this:
def somePath = MyCustomAction.async(parse.tolerantJson) { implicit request =>
request.body.validate[MyObject].map { myObject =>
val futResult = for {
getSomething <- getSomethingFuture
getSomethingElse <- getSomethingElseFuture
if getSomethingElse
if getSomething
_ <- updateMyDatabase(myObject)
} yield Ok("")
futResult.recover {
case e: Exception1 => BadRequest("bad request")
case e: Exception2 => BadRequest("blah")
case e => InternalServerError(e.getMessage)
}
}
}
private [this] def updateMyDatabase(myObject: MyObject): Future[Unit] = ???
You may be wondering how to more granularly handle errors. Here's one way:
def somePath = MyCustomAction.async(parse.tolerantJson) { implicit request =>
request.body.validate[MyObject].map { myObject =>
val futResult = for {
getSomething <- getSomethingFuture
getSomethingElse <- getSomethingElseFuture
_ <- if(getSomethingElse && getSomething) {
updateMyDatabase(myObject)
} else {
Future.failed(new CustomException("Couldn't get something else"))
}
} yield Ok("")
futResult.recover {
case e: CustomException => BadRequest("failed to get something else")
case e: Exception2 => BadRequest("blah")
case e => InternalServerError(e.getMessage)
}
}
}
Here's an alternate way:
def somePath = MyCustomAction.async(parse.tolerantJson) { implicit request =>
request.body.validate[MyObject].map { myObject =>
val futResult = for {
getSomething <- getSomethingFuture
getSomethingElse <- getSomethingElseFuture
result <- if(getSomethingElse && getSomething) {
updateMyDatabase(myObject).map(_ => Ok(""))
} else {
Future.successful(BadRequest("failed to get something else"))
}
} yield result
futResult.recover {
case e: Exception2 => BadRequest("blah")
case e => InternalServerError(e.getMessage)
}
}
}
Upvotes: 0
Reputation: 3260
Your updateMyDatabase function should return some none Unit value in order to tell if it succeeded or not. There are multiple responses a database action can return:
So a Try[Boolean] would be a good type to handle all of these scenarios.
private [this] def updateMyDatabase(myObject: MyObject): Try[Boolean] = {
// do things to update the database
}
We can now match on the response, and return the correct Result type.
updateMyDatabase(myObject) match {
case Failure(exception) => BadRequest
case Success(b) => if (b) Ok else BadRequest
}
Since getSomethingFuture and getSomethingElseFutures are both returning Futures, you are already working within the context of a Future, and do not need to wrap any of your Results in a Future. The yield keyword will make sure that it rewraps anything in the yield body back into a Future.
Now you still need to handle the situation where getSomethingFuture or getSomethingElseFuture fail. To do this, you can use the recover function. So your final code will look something like this:
(for {
getSomething <- getSomethingFuture
getSomethingElse <- getSomethingElseFuture
} yield {
// this code only executes if both futures are successful.
updateMyDatabase(myObject) match {
case Failure(exception) => BadRequest
case Success(b) => if (b) Ok else BadRequest
}
}) recover {
// Here you can match on different exception types and handle them accordingly.
// So throw a specific exception for each task if you need to handle their failures differently.
case e: GetSomethingFutureFailed => BadRequest
case e: GetSomethingElseFutureFailed => BadRequest
case _ => BadRequest
}
From the play documentation: Note that you may be tempted to therefore wrap your blocking code in Futures. This does not make it non-blocking, it just means the blocking will happen in a different thread. You still need to make sure that the thread pool that you are using has enough threads to handle the blocking.
Also make sure you instruct your controller to inject an execution context like so:
import scala.concurrent.ExecutionContext
class AsyncController @Inject() (...)(implicit exec: ExecutionContext)
Upvotes: 1