3

I'm using Spring Boot. I can use @Transactional to force transaction on a method. Sometimes I need for some method to use two or more transactions.

Naive approach wouldn't work:

public void doActions() {
    doAction1();
    doAction2();
}

@Transactional
void doAction1() { ... }

@Transactional
void doAction2() { ... }

because Spring uses proxies to implement transactions.

Usually I've used the following approach:

@Autowired
private ThisService self;

public void doActions() {
    self.doAction1();
    self.doAction2();
}

@Transactional
void doAction1() { ... }

@Transactional
void doAction2() { ... }

It worked, but in Spring 2.6.0 this circular dependency causes application to fail to start with scary error unless I set spring.main.allow-circular-references to true.

I don't really understand the reason why circular references are bad. But apparently Spring Boot developers want to discourage this kind of design, so, I guess, I better follow their advice.

Another approach is to use transaction manager and programmatically call transaction api:

@Autowired
private TransactionTemplate transactionTemplate;

public void doActions() {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(@NonNull TransactionStatus status) {
                doAction1();
            }
        });
        transactionTemplate.execute(status -> {
            doAction2();
            return null;
        });

It works but it's a little bit verbose and ugly in my opinion.

Is there any other approaches I missed? Should I just set spring.main.allow-circular-references to true? I'm afraid that developers will make those circular references thing unsupported in the future and I'd need to rework my application because of that.

vbezhenar
  • 11,148
  • 9
  • 49
  • 63
  • 3
    In situations like this I often used a separate "executor" service that got a lambda for execution. Calls might then look like this: `executor.executeInNewTx(() -> doAction1());` etc. The executor service would then have the `@Transactional` annotations etc. - In essence the problem is you need to cross a service boundary for the automatic transaction handling (i.e. interceptors) to kick in. Another option might be to split your service into multiple portions but having such an executor service makes life easier. – Thomas Jan 17 '22 at 09:03
  • Could you elaborate what you mean with "some method to use two or more transactions"? Normally, transaction context is associated with the current thread, so there's always _one_ active transaction per thread only. In addition, your 1st approach's doActions() won't create any tx as these are intra class calls (see e.g. [this question](https://stackoverflow.com/questions/56193642)) – Robin Jan 17 '22 at 10:45

1 Answers1

7

Yes. I agree it is ugly. You can create a service that is just responsible to execute some codes in a transaction . Same idea as TransactionTemplate but it uses @Transational to manage the transaction.

@Service
public class TransactionExecutor {

    @Transactional(propagation = Propagation.REQUIRED)
    public <T> T execute(Supplier<T> action) {
        return action.get();
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void execute(Runnable action) {
        action.run();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public <T> T executeInNewTx(Supplier<T> action) {
        return action.get();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void executeInNewTx(Runnable action) {
        action.run();
    }

}

And then inject it to use it :

@Service
public class FooService {
    
    @Autowired
    private TransactionExecutor txExecutor;


    public void doActions() {
        txExecutor.execute(()->doAction1());
        txExecutor.execute(()->doAction2());
    }

    void doAction1() { ... }

    void doAction2() { ... }

}
Ken Chan
  • 84,777
  • 26
  • 143
  • 172