javaspring-webfluxreactive-streams

Spring Webflux Proper Way To Find and Save


I created the below method to find an Analysis object, update the results field on it and then lastly save the result in the database but not wait for a return.

public void updateAnalysisWithResults(String uuidString, String results) {
        findByUUID(uuidString).subscribe(analysis -> {
            analysis.setResults(results);
            computeSCARepository.save(analysis).subscribe();
        });
    }

This feels poorly written to subscribe within a subscribe. Is this a bad practice? Is there a better way to write this?

UPDATE: entry point

@PatchMapping("compute/{uuid}/results")
    public Mono<Void> patchAnalysisWithResults(@PathVariable String uuid, @RequestBody String results) {
        return computeSCAService.updateAnalysisWithResults(uuid,results);
    }
    public Mono<Void> updateAnalysisWithResults(String uuidString, String results) {
//        findByUUID(uuidString).subscribe(analysis -> {
//            analysis.setResults(results);
//            computeSCARepository.save(analysis).subscribe();
//        });
        return findByUUID(uuidString)
                .doOnNext(analysis -> analysis.setResults(results))
                .doOnNext(computeSCARepository::save)
                .then();
    }

Solution

  • Why it is not working is because you have misunderstood what doOnNext does.

    Lets start from the beginning.

    A Flux or Mono are producers, they produce items. Your application produces things to the calling client, hence it should always return either a Mono or a Flux. If you don't want to return anything you should return a Mono<Void>.

    When the client subscribes to your application what reactor will do is call all operators in the opposite direction until it finds a producer. This is what is called the assembly phase. If all your operators don't chain together you are what i call breaking the reactive chain.

    When you break the chain, the things broken from the chain wont be executed.

    If we look at your example but in a more exploded version:

    @Test
    void brokenChainTest() {
        updateAnalysisWithResults("12345", "Foo").subscribe();
    }
    
    public Mono<Void> updateAnalysisWithResults(String uuidString, String results) {
        return findByUUID(uuidString)
                .doOnNext(analysis -> analysis.setValue(results))
                .doOnNext(this::save)
                .then();
    }
    
    private Mono<Data> save(Data data) {
        return Mono.fromCallable(() -> {
            System.out.println("Will not print");
            return data;
        });
    }
    
    private Mono<Data> findByUUID(String uuidString) {
        return Mono.just(new Data());
    }
    
    private static class Data {
        private String value;
    
        public void setValue(String value) {
            this.value = value;
        }
    }
    

    in the above example save is a callable function that will return a producer. But if we run the above function you will notice that the print will never be executed.

    This has to do with the usage of doOnNext. If we read the docs for it it says:

    Add behavior triggered when the Mono emits a data successfully. The Consumer is executed first, then the onNext signal is propagated downstream.

    doOnNext takes a Consumer that returns void. And if we look at doOnNext we see that the function description looks as follows:

    public final Mono<T> doOnNext(Consumer<? super T> onNext)`
    

    THis means that it takes in a consumer that is a T or extends a T and it returns a Mono<T>. So to keep a long explanation short, you can see that it consumes something but also returns the same something.

    What this means is that this usually used for what is called side effects basically for something that is done on the side that does not hinder the current flow. One of those things could for instance logging. Logging is one of those things that would consume for instance a string and log it, while we want to keep the string flowing down our program. Or maybe we we want to increment a number on the side. Or modify some state somewhere. You can read all about side effects here.

    you can of think of it visually this way:

         _____ side effect (for instance logging)
        /
    ___/______ main reactive flow
    

    That's why your first doOnNext setter works, because you are modifying a state on the side, you are setting the value on your class hence modifying the state of your class to have a value.

    The second statement on the other hand, the save, does not get executed. You see that function is actually returning something we need to take care of.

    This is what it looks like:

          save
         _____ 
        /     \   < Broken return
    ___/             ____ no main reactive flow
    

    all we have to do is actually change one single line:

    // From
    .doOnNext(this::save)
    
    // To
    .flatMap(this::save)
    

    flatMap takes whatever is in the Mono, and then we can use that to execute something and then return a "new" something.

    So our flow (with flatMap) now looks like this:

       setValue()    save()
        ______       _____ 
       /            /     \   
    __/____________/       \______ return to client
    

    So with the use of flatMap we are now saving and returning whatever was returned from that function triggering the rest of the chain.

    If you then choose to ignore whatever is returned from the flatMap its completely correct to do as you have done to call then which will

    Return a Mono which only replays complete and error signals from this

    The general rule is, in a fully reactive application, you should never block.

    And you generally don't subscribe unless your application is the final consumer. Which means if your application started the request, then you are the consumerof something else so you subscribe. If a webpage starts off the request, then they are the final consumer and they are subscribing.

    If you are subscribing in your application that is producing data its like you are running a bakery and eating your baked breads at the same time.

    don't do that, its bad for business :D