简体   繁体   中英

Play / Scala / Futures: Chained Requests

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!

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))
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM