Multiple WS call in one action, how to handle Prom

2019-03-09 04:59发布

问题:

I develop a little server in PlayFramework2/Scala which has to retrieve data from multiple WS (REST/JSON), manipulate the data from theses WS, then compose and return a result.

I know how to call one WS, manipulate the data and return an Async response. But I don't know how how to call successively several web-services, handle the data between every call and generate an aggregated answer.

Exemple :

  • Fetch the list of my prefered songs from WebService A
  • then, for each song, fetch the artist detail from WS B (one call by song)
  • then, generate and return something (aggregated list for example) using the A and B responses
  • then, return the result

I am blocked by the asynchronous processings of WS API (WS.url(url).get => Promise[Response]). Do I have to lean on Akka to solve this problem?

Thank you.

回答1:

flatMap and map are your friends! These two methods of the Promise type allow to transform the result of a Promise[A] into another Promise[B].

Here is a simple example of them in action (I intentionally wrote explicitly more type annotations than needed, just to help to understand where transformations happen):

def preferredSongsAndArtist = Action {
  // Fetch the list of your preferred songs from Web Service “A”
  val songsP: Promise[Response] = WS.url(WS_A).get
  val resultP: Promise[List[Something]] = songsP.flatMap { respA =>
    val songs: List[Song] = Json.fromJson(respA.json)
    // Then, for each song, fetch the artist detail from Web Service “B”
    val result: List[Promise[Something]] = songs.map { song =>
      val artistP = WS.url(WS_B(song)).get
      artistP.map { respB =>
        val artist: Artist = Json.fromJson(respB.json)
        // Then, generate and return something using the song and artist
        val something: Something = generate(song, artist)
        something
      }
    }
    Promise.sequence(result) // Transform the List[Promise[Something]] into a Promise[List[Something]]
  }
  // Then return the result
  Async {
    resultP.map { things: List[Something] =>
      Ok(Json.toJson(things))
    }
  }
}

Without the unnecessary type annotations and using the “for comprehension” notation, you can write the following more expressive code:

def preferredSongsAndArtist = Action {
  Async {
    for {
      // Fetch the list of your preferred songs from Web Service “A”
      respA <- WS.url(WS_A).get
      songs = Json.fromJson[List[Song]](respA.json)
      // Then, for each song, fetch the artist detail from Web Service “B”
      result <- Promise.sequence(songs.map { song =>
        for {
          respB <- WS.url(WS_B(song)).get
          artist = Json.fromJson[Artist](respB.json)
        } yield {
          // Then, generate and return something using the song and artist
          generate(song, artist)
        }
      })
    // Then return the result
    } yield {
      Ok(Json.toJson(result))
    }
  }
}