How to get Flux or List from two Flux(Object) in Spring Reactive Programming?

ace picture ace · Apr 5, 2018 · Viewed 11.4k times · Source

I am new to Spring Reactive Project. In my Spring Boot Controller class I have

Flux<House> ( list of all houses in the database) and Flux<Image>

coming from service layer where given Flux<Image> are images for a given House like a list of houses each of which has its own collection of images. That means that I have to iterate over Flux<House> to get id of the home and then call findByHouseId on Image to get Flux<Image> , each Image has houseId property. Each image object has a property "cover" with string value "yes" or "no" and another property "type" having various string values like "bedroom". I want to collect a

list of Flux<Image> like List<Flux<Image>> or List<List<Image>>

containing only those Images for which "cover" == "yes" OR "type" == "bedroom". How to achieve this? Programming code will answer this question.

Below was attempted as part of the solution but even this partial solution does not work:

List<Flux<Image>> imagesList= new ArrayList<Flux<Image>>();
        Flux<House> houses = houseService.findAllHouses();

        houses.map(house ->  house.getId()).subscribe(id -> imageService.findByHouseId(id))
        .map(imageFlux ->imagesList.add(imageFlux));

Please note that I am looking for solution that does not involve invoking block() ( negating the benefit of being reactive) unless this is the only way.

In non-reactive situation this is what I want:

List<House> houses  --> List of all houses
Map<String,List<Image>> mapOfImages  --> where key is houseId 

so for each house I can easily get images for that house while iterating over houses in view template.

These (houses and mapOfImages) will be passed to model like

model.addAttribute("houses",houses);
model.addAttribute("mapOfImages",mapOfImages);

So now within thyemleaf view template I can:

<div data-th-each="house : ${houses}">
        <h3 data-th-text="${house.title}"></h3>
        <div data-th-each="image : ${mapOfImages.get(house.houseId)}">
          <h5 data-th-text="${image.fileName}"></h5>
          <h5 data-th-text="${image.type}"></h5>
        </div>
 </div>   

The database being used is Mongodb which has two independent collections: house and image i.e. there are no embedded collections and the only thing that tie up an image to a house is the houseId property on image.

Answer

MuratOzkan picture MuratOzkan · Apr 9, 2018

You basically want to transform a stream of House objects into a stream of Image objects.

So, use flatMap() which converts Flux<Flux<T>> (or Flux<Mono<T>>) into Flux<T>.

Mono<List<Image>> images = houseService.findAllHouses() // returns Flux<House>
  .flatMap(house -> imageService.findByHouseId(id)      // returns Flux<Image>
                        .filter(image -> "yes".equals(image.getCover()) && ... )) 
  .collectList();                                       //returns Mono<List<Image>>

Its OK to use collectList() here, because you most possibly want to convert images into a single Mono<ServerResponse> object anyway:

return images.map(imageList -> ServerResponse.ok()
                                       .body(fromObject(imageList)));

Also, I recommend reading Project Reactor reference about when to use which operator.


EDIT:

So, you want a Mono<Map<String, Collection<Image>> instead. My answer would depend whether the Image class has a reference back to the House. It's still OK if it doesn't, the answer would be more verbose though.

I'll describe the hard way, where Image doesn't contain the House id. We need sth., which links the list of images belonging to a specific house. You can define a class for this purpose or just use reactor.util.function.Tuple2. Having that, we can just use collectMap():

Mono<Map<String, Collection<Image>>> images = houseService.findAllHouses()
     .flatMap(house -> imageService.findByHouseId(id)
                           .filter(...)
                           .map(image -> Tuples.of(id, image)))
     .collectMultiMap(tuple -> tuple.getT1(), 
                      tuple -> tuple.getT2());

Of course, if you have a reference back to the House, then you could omit the map() again and use collectMultiMap directly:

...
    .collectMultiMap(image -> image.getHouseId(), image -> image);