简体   繁体   中英

Using reactive(r2dbc) batch with transaction

I am trying to use create a batch using reactive r2dbc and use transactional to annotate the method. But it looks like that if I use both @Transactional and batch code the code just hangs and doesn't work.

Below is the code:

@Transactional
@GetMapping("/batchFetchData")
public Flux<Object> batchFetch() {
    long startTime = System.currentTimeMillis();
    Mono.from(databaseConfiguration.connectionFactory().create())
            .flatMapMany(connection -> Flux.from(connection
                    .createBatch() /* **Creating batch***/
                    .add("SELECT * FROM xtable where xId = 232323")
                    .add("SELECT * FROM ytable where yId = 454545")
                    .add("SELECT * FROM ztable where zId = 676767")
                    //.execute()));  /* **Execution batch***/
                    .execute())).as(StepVerifier::create)
            .expectNextCount(3) /* **Expect count batch***/
            .verifyComplete();  /* **Verify batch***/

    LOGGER.info("Time taken to batchFetch "+(System.currentTimeMillis() - startTime));
    return null;
}

You are breaking the reactive chain.

In reactive programming nothing happens until you subscribe .

So what does that mean, well I can show it in a little example.

// If running this, nothing happens
Mono.just("Foobar");

while:

Mono.just("Foobar").subscribe(s -> System.out.println(s));

Will print:

Foobar

This also applies if you have a function

public void getString() {
    Mono.just("Foobar");
}

// Nothing happens, you have declared something 
// but it will never get run, no one is subscribing
getString();

What you need to do:

public Mono<String> getString() {
    // This could be saving to a database or anything, this will now get run
    return Mono.just("Now this code will get run");
}

// The above got run, we can prove it by printing
getString().subscribe(s -> System.out.println(s));

So what is happening? Well, in reactive programming as soon as someone subscribes to a Mono or a Flux, the program will traverse up and build a chain of callbacks until it finds the producer that starts producing values (in my case the just statement). This phase is called the "assembly phase". When this phase is done, the reactive chain will start producing values to whomever that is subscribing.

If no one is subscribing, no chain will be built.

So who is the subscriber? It is usually the end consumer of the value. So for instance, the webpage that initiated the call, or a mobile app, but could also be your Spring Boot service if it's the one that initiates the call (in for instance a cron job).

So lets look at your code:

@Transactional /* **Transaction** */
@GetMapping("/batchFetchData")
public Flux<Object> batchFetch() {
    long startTime = System.currentTimeMillis();

    // Here you declare a Mono but ignoring the return type so breaking the reactive chain
    Mono.from(databaseConfiguration.connectionFactory().create()) 
            .flatMapMany(connection -> Flux.from(connection
                    .createBatch() /* **Creating batch***/
                    .add("SELECT * FROM xtable where xId = 232323")
                    .add("SELECT * FROM ytable where yId = 454545")
                    .add("SELECT * FROM ztable where zId = 676767")
                    //.execute()));  /* **Execution batch***/
                    .execute())).as(StepVerifier::create)
            .expectNextCount(3) /* **Expect count batch***/
            .verifyComplete();  /* **Verify batch***/
            // Here at the end you have no subscriber

    LOGGER.info("Time taken to batchFetch "+(System.currentTimeMillis() - startTime));

    // Null is not allowed in reactive chains
    return null;
}

So how do you solve it?

Well you need to not break the reactive chain. This is basic reactive programming.

@Transactional
@GetMapping("/batchFetchData")
public Mono<Void> batchFetch() {
    long startTime = System.currentTimeMillis();

    // we return here so that the calling client 
    // can subscribe and start the chain
    return Mono.from(databaseConfiguration.connectionFactory().create()) 
            .flatMapMany(connection -> Flux.from(connection
                    .createBatch()
                    .add("SELECT * FROM xtable where xId = 232323")
                    .add("SELECT * FROM ytable where yId = 454545")
                    .add("SELECT * FROM ztable where zId = 676767")
                    .execute()))
                    .then(); 
                    // then() statement throws away whatever the return 
                    // value is and just signals to the calling client 
                    // when everything is done.       
}

"I don't want to return anything"

Well that's what the Mono#then statement is for. You see when each part in the chain is completed it will signal its completion and then pass the values from one part to the next, and then signal again, and pass the value etc. When we reach the then statement it will just signal COMPLETE and not return anything (or actually it is returning a Mono<Void> because null is not allowed in reactive chains). You must always return, so that each step can pass on its COMPLETE signal.

Also I removed the StepVerifier you are using in your code, because it is usually used to verify steps in unit tests, and not used in production code. You can read more about it here StepVerifier .

If you want to learn reactive programming, which I suggest you do because it is amazing and I love it, I recommend strongly that you read the excellent reactor documentation Introduction to reactive programming where they will explain the concepts of nothing happens until you subscribe etc.

Your problem is:

return null;

You should return a Mono/Flux in a reactive application, even there is no items in the flow, return a Mono.emtpy instead.

Check my example insert Multi records .

And the tests, use StepVerify to verify the result .

For transaction support in WebFlux applications, you have to read the relative docs to check if it is supported well as a general local transaction or the limitation when using it.

There are two approaches to use the transaction if it is supported well.

  1. Inject a TransactionalOperator (similar to the traditional TransactionTemplate ) to wrap your business logic.
  2. Apply @Transaction annotation on classes or methods as general.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM