8

I started using Project reactor and one place where I'm struggling little is how do I combine things coming from Mono with Flux. Here's my use case:

public interface GroupRepository {
       Mono<GroupModel> getGroup(Long groupId);
}

public interface UserRepository {
       Flux<User> getUsers(Set<Long> userIds);   
}

Mono<GroupModel> groupMono = getGroup(groupId);
Flux<User> userFlux = getUsers(Set<Long> users);
//run above instrtuction in parallel and associate user to group.

Now what I want to achieve is:

How can I combine response from UserFlux and associate those users with that group, with something like group.addUsers(userfromFlux).

Can someone help with how to combine results coming from userFlux and groupMono. I think I use something like Zip but then it does one to one mapping from source. In my case, I need to do 1 to N mapping. Here I have one group but multiple user that I need to add to that group. Is it a good idea to return Mono<List<Users> and then use zip operator with both mono and provide a combinator as mentioned here
public static <T1, T2, O> Flux<O> zip(Publisher<? extends T1> source1, Publisher<? extends T2> source2, final BiFunction<? super T1, ? super T2, ? extends O> combinator)?

pixel
  • 24,905
  • 36
  • 149
  • 251
User5817351
  • 989
  • 2
  • 16
  • 36

5 Answers5

10

This 1 to N mapping sounds similar to a question I gave an answer to here:

Can you Flux.zip a mono and a flux and and repeat the mono value for every flux value?

Incase that link goes down, here's the answer again. I don't think this method would have good performance as the mono would be recomputed every time. For better performance, if your Mono is wraps around a slow operation, it might be good to have some caching layer.

Suppose you have a flux and a mono like this:

 // a flux that contains 6 elements.
 final Flux<Integer> userIds = Flux.fromIterable(List.of(1,2,3,4,5,6));

 // a mono of 1 element.
 final Mono<String> groupLabel = Mono.just("someGroupLabel");

First, I'll show you the wrong way of trying to zip the 2 which I tried, and I think other people would try:

 // wrong way - this will only emit 1 event 
 final Flux<Tuple2<Integer, String>> wrongWayOfZippingFluxToMono = userIds
         .zipWith(groupLabel);

 // you'll see that onNext() is only called once, 
 //     emitting 1 item from the mono and first item from the flux.
 wrongWayOfZippingFluxToMono
         .log()
         .subscribe();

wrong way

 // this is how to zip up the flux and mono how you'd want, 
 //     such that every time the flux emits, the mono emits. 
 final Flux<Tuple2<Integer, String>> correctWayOfZippingFluxToMono = userIds
         .flatMap(userId -> Mono.just(userId)
                 .zipWith(groupLabel));

 // you'll see that onNext() is called 6 times here, as desired. 
 correctWayOfZippingFluxToMono
         .log()
         .subscribe();

enter image description here

SomeGuy
  • 1,702
  • 1
  • 20
  • 19
5

I think Flux.combineLatest static method could help you there: since your Mono only ever emits 1 element, that element will always be the one combined with each incoming value from the Flux.

Flux.combineLatest(arr -> new Combination((GroupModel) arr[0], (User) arr[1]),
                   groupMono, userFlux);
Simon Baslé
  • 27,105
  • 5
  • 69
  • 70
  • But I ended up using `Flux.zip(groupMono, userMono, BiFunction)` since in my case, underlying data structure inside group model was HashSet ( which holds users which are part of the group) and it's not thread safe to add users flowing from user flux in group, in a reactive fashion. So rather I'm using BiFunction to populate users in group in that separate method. Thanks for the help, really appreciate your help! – User5817351 Apr 20 '17 at 20:02
  • 3
    Maybe I don't get something but if I use `combineLatest` with a `Flux` and a `Mono` for this isn't it possible that I "drop" the first events in the `Flux` if the first event of the `Mono` is coming in later? For example: The user repository is super fast an already returned 10 users before the group repository returned the one group then I would expect that the first the first element in the stream behind `combineLatest` is a combination of the 10th user with the group because `combineLatest` actually combines the *latest* elements. Can someone clear this up to me? – Tobske Jun 13 '18 at 08:49
4
Flux.fromIterable(List.of(1,2,3,4,5,6))
      .zipWith(Mono.just("groupLabel").cache().repeat())

will zip your label to each value emitted by flux

Przemek
  • 39
  • 2
1

Adding an answer for other folks, I used, Flux.zip(groupMono, userMono(holding list of users), this::biFunctionToPopulateGroupWithUsers). I used this approach rather than as suggested by @Simon, since underlying group that holds users is a HashSet and adding users in a reactive way will not be thread safe. But if you've a thread safe data structure I would use suggestion by @Simon.

User5817351
  • 989
  • 2
  • 16
  • 36
0

I have checked both answers and there seems to be a bit of confusion regarding the goal:

  • Evaluating a Mono once and combine these results with the Flux. Then repeat/cache is the way to go.
  • Combining a Mono with a Flux and re-evaluating it every time. Here the trick is to create a tuple of two Monos: One from the Flux value and one that should be called. To do some calculations afterwards, use map and re-combine with the desired operation.
penguineer
  • 96
  • 5