如何在Scala/Play框架中处理多REST调用并合并资源信息?
Awesome question! When you need to fire off multiple REST calls in Scala/Play, wait for all of them to complete, then merge the results, you’ve got some perfect tools built right into the ecosystem. Let’s walk through the best approaches with practical examples.
1. Future.sequence + Play WSClient (Most Common for Same-Type Resources)
This is the go-to pattern when you’re fetching multiple instances of the same resource type. Play’s WSClient returns Future[WSResponse] for every REST call, and Future.sequence turns a list of these futures into a single Future[List[WSResponse]]—so you can wait for all calls to finish before merging.
Here’s a concrete example:
import play.api.libs.ws._ import scala.concurrent.{ExecutionContext, Future} import play.api.libs.json._ // Example data models case class Resource(id: String, details: String) object Resource { implicit val format: OFormat[Resource] = Json.format[Resource] val empty: Resource = Resource("", "Failed to fetch") } case class MergedResult(totalResources: Int, combinedDetails: String) class ResourceService @Inject()(ws: WSClient)(implicit ec: ExecutionContext) { // Fetch a single resource via REST private def fetchSingleResource(id: String): Future[Resource] = { ws.url(s"https://your-api.com/resources/$id") .get() .map(_.json.as[Resource]) .recover { case _ => Resource.empty } // Gracefully handle individual call failures } // Fetch all resources and merge results def fetchAndMerge(ids: List[String]): Future[MergedResult] = { // Create a list of futures for each resource call val resourceFutures: List[Future[Resource]] = ids.map(fetchSingleResource) // Wait for all futures to resolve, then merge Future.sequence(resourceFutures) .map(resources => { MergedResult( totalResources = resources.size, combinedDetails = resources.map(_.details).mkString(" | ") ) }) } }
2. Future.zip / Future.traverse (For Mixed-Types or Custom Per-Request Logic)
If you’re fetching different types of resources (e.g., a user and their orders), use Future.zip (for 2 requests) or Future.zip3/zipN (for more). For scenarios where you need to transform each request’s input before fetching, Future.traverse (a combination of map + sequence) works perfectly.
Example with Future.zip for mixed types:
case class User(id: String, name: String) case class Order(id: String, total: Double) // Fetch a user and their latest order in parallel def fetchUserAndOrder(userId: String, orderId: String): Future[(User, Order)] = { val userFuture = ws.url(s"https://your-api.com/users/$userId") .get().map(_.json.as[User]) val orderFuture = ws.url(s"https://your-api.com/orders/$orderId") .get().map(_.json.as[Order]) Future.zip(userFuture, orderFuture) }
3. Handling Partial Failures with Future.fold
If you want to continue processing even if some REST calls fail (instead of failing the entire batch), Future.fold lets you accumulate results incrementally, skipping or handling failures as you go.
Example:
def fetchAllWithPartialFailureHandling(ids: List[String]): Future[List[Resource]] = { // Start with an empty list, then add each successful resource to it Future.fold(ids.map(fetchSingleResource))(List.empty[Resource]) { (accumulator, resource) => if (resource != Resource.empty) accumulator :+ resource else accumulator } }
Bonus: Integrate with Play’s Async Controllers
If you’re building a Play endpoint, return a Future[Result] to keep your app non-blocking:
import play.api.mvc._ class ResourceController @Inject()(service: ResourceService, cc: ControllerComponents)(implicit ec: ExecutionContext) extends AbstractController(cc) { def getMergedResources(ids: String) = Action.async { val idList = ids.split(",").toList service.fetchAndMerge(idList) .map(mergedResult => Ok(Json.toJson(mergedResult))) .recover { case e => InternalServerError(s"Merge failed: ${e.getMessage}") } } }
Quick Recap
- Use
Future.sequencefor same-type resources in bulk - Use
Future.zip/zipNfor fixed numbers of different-type resources - Use
Future.fold+recoverwhen you need to handle partial failures - Always use Play’s injected
ExecutionContextto avoid thread pool issues
内容的提问来源于stack exchange,提问作者user9584853

