AdrienW
AdrienW

Reputation: 3442

How to chain reactive operations after empty Mono without blocking?

Basically what I am trying to achieve is to call a second repository (a ReactiveCrudRepository) or throw an exception, depending on the result of a call to a first repository.

My original idea looks like this:

/** Reactive with blocking code */
public Flux<SecondThing> getThings(String firstThingName) {
    FirstThing firstThing = firstRepo
        .findByName(firstThingName)
        // Warning: "Inappropriate blocking method call"
        .blockOptional()  // this fails in test-context
        .orElseThrow(() -> new FirstThingNotFound(firstThingName));

    return secondRepo.findAllByFirstThingId(firstThing.getId());
}

Which would correspond to the following non-reactive approach:

/** Non-reactive */
public List<SecondThing> getThings(String firstThingName) {
    FirstThing firstThing = firstRepo
        .findByName(firstThingName)
        .orElseThrow(() -> new FirstThingNotFound(firstThingName));

    return secondRepo.findAllByFirstThingId(firstThing.getId());
}

I haven't found a way to do this in a reactive non-blocking way. All I need is to throw an error if an empty Mono comes out of the first call, and continue the pipeline if not empty; but I could not seem to use onErrorStop or doOnError correctly here, and map does not help as it skips the empty Mono.

What I have is a workaround if I use id instead of name, but I'm not quite satisfied with it as it shows a different behaviour in the case where is an instance of FirstThing but no SecondThing linked to it:

/** Reactive workaround 1 */
public Flux<SecondThing> getThings(Long firstThingId) {
    return secondRepo
        .findAllByFirstThingId(firstThingId)
        .switchIfEmpty(
            Flux.error(() -> new FirstThingNotFound(firstThingName))
        );
}

Another workaround I've found is the following, that replaces the empty Mono with a null value, but it doesn't look right and throws a warning too:

/** Reactive workaround 2 */
public Flux<SecondThing> getThings(String firstThingName) {
    return firstRepo
        .findByName(firstThingName)
        // Warning: "Passing 'null' argument to parameter annotated as @NotNull"
        .defaultIfEmpty(null)
        .flatMapMany(
            firstThing -> secondRepo.findAllByFirstThingId(firstThing.getId()
        )
        .onErrorMap(
            NullPointerException.class, e -> new FirstThingNotFound(firstThingName)
        );
}

What is the correct way to chain the calls to both repositories so that the presence or absence of a FirstThing with the requested firstThingName conditions the call to the second repo?

Upvotes: 1

Views: 2753

Answers (1)

AdrienW
AdrienW

Reputation: 3442

I found a solution so simple, that I could be ashamed not to have found it earlier:

public Flux<SecondThing> getThings(String firstThingName) {
    return firstRepo
        .findByName(firstThingName)
        .switchIfEmpty(Mono.error(() -> new FirstThingNotFound(firstThingName)))
        .flatMapMany(
            firstThing -> secondRepo.findAllByFirstThingId(firstThing.getId()
        );
}

The trick is that switchIfEmpty does not force you to pick a "valid" replacement value, so it is possible to use a Mono.error to propagate the right exception directly.

Upvotes: 5

Related Questions