Home > Software design >  Overriding transaction propagation levels for methods having Spring's @transactional
Overriding transaction propagation levels for methods having Spring's @transactional

Time:05-11

I have multiple methods in my codebase annotated with Spring's @transactional with different propgation levels (lets ignore the idea behind choosing the propagation levels). Example -

public class X {
    @Transactional(Propagation.NOT_SUPPORTED)
    public void A() { do_something; }

    @Transactional(Propagation.REQUIRED)
    public void B() { do_something; }

    @Transactional(Propagation.REQUIRES_NEW)
    public void C() { do_something; }
}

Now I have a new use case where I want to perform all these operations in a single transaction (for this specific use case only, without modifying existing behavior), overriding any annotated propagation levels. Example -

public class Y {
    private X x;

    // Stores application's global state
    private GlobalState globalState;

    @Transactional
    public void newOperation() {
         // Set current operation as the new operation in the global state, 
         // in case this info might be required somewhere
         globalState.setCurrentOperation("newOperation");

         // For this new operation A, B, C should be performed in the current 
         // transaction regardless of the propagation level defined on them
         x.A();
         x.B();
         x.C();
    }
}

Does Spring provide some way to achieve this ? Is this not possible ?

  • One way I could think of is to split the original methods
    @Transactional(Propagation.NOT_SUPPORTED)
    public void A() { A_actual(); }
    
    // Call A_actual from A and newOperation
    public void A_actual() { do_something; }
    
    But this might not be as simple to do as this example (there can be a lot of such methods and doing this might not scale). Also it does not look much clean.
  • Also the use case might also appear counter intuitive, but anyway let's keep that out of scope of this question.

CodePudding user response:

I do believe the only option is to replace TransactionInterceptor via BeanPostProcessor, smth. like:

public class TransactionInterceptorExt extends TransactionInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // here some logic determining how to proceed invocation
        return super.invoke(invocation);
    }

}
public class TransactionInterceptorPostProcessor implements BeanFactoryPostProcessor, BeanPostProcessor, BeanFactoryAware {

    @Setter
    private BeanFactory beanFactory;

    @Override
    public void postProcessBeanFactory(@NonNull ConfigurableListableBeanFactory beanFactory) throws BeansException {
        beanFactory.addBeanPostProcessor(this);
    }

    @Override
    public Object postProcessBeforeInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException {
        if (bean instanceof TransactionInterceptor) {
            TransactionInterceptor interceptor = (TransactionInterceptor) bean;
            TransactionInterceptor result = new TransactionInterceptorExt();
            result.setTransactionAttributeSource(interceptor.getTransactionAttributeSource());
            result.setTransactionManager(interceptor.getTransactionManager());
            result.setBeanFactory(beanFactory);
            return result;
        }
        return bean;
    }

}
@Configuration
public class CustomTransactionConfiguration {

    @Bean
    //@ConditionalOnBean(TransactionInterceptor.class)
    public static BeanFactoryPostProcessor transactionInterceptorPostProcessor() {
        return new TransactionInterceptorPostProcessor();
    }

}

However, I would agree with @jim-garrison suggestion to refactor your spring beans.

UPD.

But you favour refactoring the beans instead of following this approach. So for the sake of completeness, can you please mention any issues/shortcomings with this

Well, there are a plenty of things/concepts/ideas in spring framework which were implemented without understanding/anticipating consequences (I believe the goal was to make framework attractive to unexperienced developers), and @Transactional annotation is one of such things. Let's consider the following code:

    @Transactional(Propagation.REQUIRED)
    public void doSomething() { 
        do_something; 
    }

The question is: why do we put @Transactional(Propagation.REQUIRED) annotation above that method? Someone might say smth. like this:

that method modifies multiple rows/tables in DB and we would like to avoid inconsistencies in our DB, moreover Propagation.REQUIRED does not hurt anything, because according to the contract it either starts new transaction or joins to the exisiting one.

and that would be wrong:

  • @Transactional annotation poisons stacktraces with irrelevant information
  • in case of exception it marks existing transaction it joined to as rollback-only - after that caller side has no option to compensate that exception

In the most cases developers should not use @Transactional(Propagation.REQUIRED) - technically we just need a simple assertion about transaction status.

Using @Transactional(Propagation.REQUIRES_NEW) is even more harmful:

  • in case of existing transaction it acquires another one JDBC-connection from connection pool, and hence you start getting 2 connections per thread - this hurts performance sizing
  • you need to carefully watch for data you are working with - data corruptions and self-locks are the consequences of using @Transactional(Propagation.REQUIRES_NEW), cause now you have two incarnations of the same data within the same thread

In the most cases @Transactional(Propagation.REQUIRES_NEW) is an indicator that you code requires refactoring.

So, the general idea about @Transactional annotation is do not use it everywhere just because we can, and your question actually confirms this idea: you have failed to tie up 3 methods together just because developer had some assumptions about how those methods should being executed.

  • Related