【问题标题】:Play / Scala / Futures: Chained RequestsPlay / Scala / Futures:链式请求
【发布时间】:2016-02-28 11:45:40
【问题描述】:

我正在尝试执行可能很简单的操作,但遇到了困难:我有一个在 Mongo 中创建用户的 Play 控制器,但我首先要验证是否没有用户具有相同的电子邮件地址.我的用户对象上有一个函数,它通过电子邮件地址搜索用户并返回 Future[Option[User]]:

  def findByEmail(email: String): Future[Option[User]] = {
    collection.find(Json.obj("email" -> email)).one[User]
  }

我通过电子邮件搜索用户的控制器功能有效:

  def get(id: String) = Action.async {
    User.findById(id).map {
      case None => NotFound
      case user => Ok(Json.toJson(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)    
    }
  }

并且相应的控制器功能起作用:

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

但是当我修改 post 方法以首先检查具有指定电子邮件的用户时,它失败了:

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

它报告说,虽然它期望一个 Future[Result],但它找到了一个 Future[Object]。我认为这个错误意味着它最终找到了一个 Future[Future[Result]],这不是它所期望的。

我的问题是:将此类调用链接在一起的最佳做法是什么?我应该添加一个 Await.result() 调用来等待第一个操作完成后再继续吗?这会导致发生任何不需要的同步操作吗?或者有没有更好的方法来解决这个问题?

提前致谢!

【问题讨论】:

  • 当然Await 会添加同步,这是不想要的。错误是由于Future 内部的异质值造成的。确保所有情况的返回类型都相同。

标签: scala playframework


【解决方案1】:

您的代码存在两个问题。暂时只看这个块:

case None => create(user).map {
    case u => Created("")
}
case u => BadRequest

首先,create(user).map { ... }返回Future[Result],但case u => BadRequest返回Result,然后编译器转到更“宽”的类型,即Object。让我们分开这个块(改变只是为了说明我的观点):

val future: Future[Object] = findByEmail("").map {
  case Some(u) => BadRequest
  case None => create(User()).map {
    case u => Created("")
  }
}

现在,很明显两个 case 块必须返回相同的类型:

val future: Future[Future[Result]] = findByEmail("").map {
  case Some(u) => Future.successful(BadRequest)
  case None => create(User()).map {
    case u => Created("")
  }
}

注意我如何从case Some(u) => BadRequest 更改为case Some(u) => Future.successful(BadRequest),现在我们有了Future[Future[Result]],这不是我们想要的,并显示了第二个问题。让我们看看Future.map签名:

def map[S](f: T => S)(implicit executor: ExecutionContext): Future[S]

忘记隐式执行器,因为它与本次讨论无关:

def map[S](f: T => S): Future[S]

因此,我们收到一个从T 转换为S 的块,然后我们将S 包装成Future:

val futureInt: Future[Int] = Future.successful(1)
val futureString: Future[String] = futureInt.map(_.toString)

但是如果块返回另一个Future 怎么办?然后它会被包裹起来,你会得到一个Future[Future[...]]:

val futureFuture: Future[Future[String]] = futureInt.map(v => Future.successful(v.toString))

为了避免换行,我们需要使用flatMap 而不是map:

val futureInt: Future[Int] = Future.successful(1)
val futureString: Future[String] = futureInt.flatMap(v => Future.successful(v.toString))

让我们回到您的代码并改用flatMap:

val future: Future[Result] = findByEmail("").flatMap {
  case Some(u) => Future.successful(BadRequest)
  case None => create(User()).map {
    case u => Created("")
  }
}

然后,最终版本将是:

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

【讨论】:

  • 谢谢你,它工作得很好!我仍然不完全理解 map 和 flatMap 之间的区别,但是在做了足够的研究和阅读之后,我发现 flatMap 非常适合我的用例。再给你一个问题:有没有更优雅的方法来使用 for 理解来解决这个问题?我读到理解有助于使 flatMap() 调用更具可读性并避免大量嵌套。我现在正在尝试重写它,但任何指针将不胜感激!
  • 这是一个很好的参考资料,它揭示了我的地图与平面地图的问题:Play Framework: async I/O without the thread pool and callback hell
猜你喜欢
  • 2013-02-09
  • 2017-03-27
  • 1970-01-01
  • 2019-03-13
  • 2023-03-19
  • 2016-02-16
  • 1970-01-01
  • 2016-11-13
  • 1970-01-01
相关资源
最近更新 更多