Roll back A if B goes wrong. spring boot, jdbctemplate

后端 未结 5 746
自闭症患者
自闭症患者 2020-12-23 13:00

I have a method, \'databaseChanges\', which call 2 operations: A, B in iterative way. \'A\' first, \'B\' last. \'A\' & \'B\' can be Create, U

相关标签:
5条回答
  • 2020-12-23 13:00

    Any RuntimeException triggers rollback, and any checked Exception does not.

    This is common behavior across all Spring transaction APIs. By default, if a RuntimeException is thrown from within the transactional code, the transaction will be rolled back. If a checked exception (i.e. not a RuntimeException) is thrown, then the transaction will not be rolled back.

    It depends on which exception you are getting inside databaseChanges function. So in order to catch all exceptions all you need to do is to add rollbackFor = Exception.class

    The change supposed to be on the service class, the code will be like that:

    @Service
    public class SomeService implements ISomeService {
        @Autowired
        private NamedParameterJdbcTemplate jdbcTemplate;
        @Autowired
        private NamedParameterJdbcTemplate npjt;
    
        @Transactional(rollbackFor = Exception.class)
        private void databaseChanges() throws Exception {   
            A(); //update
            B(); //insert
        }
    }
    

    In addition you can do something nice with it so not all the time you will have to write rollbackFor = Exception.class. You can achieve that by writing your own custom annotation:

    @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Transactional(rollbackFor = Exception.class)
    @Documented
    public @interface CustomTransactional {  
    }
    

    The final code will be like that:

    @Service
    public class SomeService implements ISomeService {
        @Autowired
        private NamedParameterJdbcTemplate jdbcTemplate;
        @Autowired
        private NamedParameterJdbcTemplate npjt;
    
        @CustomTransactional
        private void databaseChanges() throws Exception {   
            A(); //update
            B(); //insert
        }
    }
    
    0 讨论(0)
  • 2020-12-23 13:02

    @Transactional annotation in spring works by wrapping your object in a proxy which in turn wraps methods annotated with @Transactional in a transaction. Because of that annotation will not work on private methods (as in your example) because private methods can't be inherited => they can't be wrapped (this is not true if you use declarative transactions with aspectj, then proxy-related caveats below don't apply).

    Here is basic explanation of how @Transactional spring magic works.

    You wrote:

    class A {
        @Transactional
        public void method() {
        }
    }
    

    But this is what you actually get when you inject a bean:

    class ProxiedA extends A {
       private final A a;
    
       public ProxiedA(A a) {
           this.a = a;
       }
    
       @Override
       public void method() {
           try {
               // open transaction ...
               a.method();
               // commit transaction
           } catch (RuntimeException e) {
               // rollback transaction
           } catch (Exception e) {
               // commit transaction
           }
       }
    } 
    

    This has limitations. They don't work with @PostConstruct methods because they are called before object is proxied. And even if you configured all correctly, transactions are only rolled back on unchecked exceptions by default. Use @Transactional(rollbackFor={CustomCheckedException.class}) if you need rollback on some checked exception.

    Another frequently encountered caveat I know:

    @Transactional method will only work if you call it "from outside", in following example b() will not be wrapped in transaction:

    class X {
       public void a() {
          b();
       }
    
       @Transactional
       public void b() {
       }
    }
    

    It is also because @Transactional works by proxying your object. In example above a() will call X.b() not a enhanced "spring proxy" method b() so there will be no transaction. As a workaround you have to call b() from another bean.

    When you encountered any of these caveats and can't use a suggested workaround (make method non-private or call b() from another bean) you can use TransactionTemplate instead of declarative transactions:

    public class A {
        @Autowired
        TransactionTemplate transactionTemplate;
    
        public void method() {
            transactionTemplate.execute(status -> {
                A();
                B();
                return null;
            });
        }
    
    ...
    } 
    

    Update

    Answering to OP updated question using info above.

    Which method should be annotated with @Transactional: changes()? databaseChanges()?

    @Transactional(rollbackFor={Exception.class})
    public void changes() throws Exception {
        someLogicBefore();
        databaseChanges();
        someLogicAfter();
    }
    

    Make sure changes() is called "from outside" of a bean, not from class itself and after context was instantiated (e.g. this is not afterPropertiesSet() or @PostConstruct annotated method). Understand that spring rollbacks transaction only for unchecked exceptions by default (try to be more specific in rollbackFor checked exceptions list).

    0 讨论(0)
  • 2020-12-23 13:02

    What you seem to be missing is a TransactionManager. The purpose of the TransactionManager is to be able to manage database transactions. There are 2 types of transactions, programmatic and declarative. What you are describing is a need for a declarative transaction via annotations.

    So what you need to be in place for your project is the following:

    Spring Transactions Dependency (Using Gradle as example)

    compile("org.springframework:spring-tx")
    

    Define a Transaction Manager in Spring Boot Configuration

    Something like this

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource)
    {
        return new DataSourceTransactionManager(dataSource);
    }
    

    You would also need to add the @EnableTransactionManagement annotation (not sure if this is for free in newer versions of spring boot.

    @EnableTransactionManagement
    public class AppConfig {
    ...
    }
    

    Add @Transactional

    Here you would add the @Transactional annotation for the method that you want to participate in the transaction

    @Transactional
    public void book(String... persons) {
        for (String person : persons) {
            log.info("Booking " + person + " in a seat...");
            jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", person);
        }
    };
    

    Note that this method should be public and not private. You may want to consider putting @Transactional on the public method calling databaseChanges().

    There are also advanced topics about where @Transactional should go and how it behaves, so better to get something working first and then explore this area a bit later:)

    After all these are in place (dependency + transactionManager configuration + annotation), then transactions should work accordingly.

    References

    Spring Reference Documentation on Transactions

    Spring Guide for Transactions using Spring Boot - This has sample code that you can play with

    0 讨论(0)
  • 2020-12-23 13:24

    The first code you present is for UserTransactions, i.e. the application has to do the transaction management. Usually you want the container to take care of that and use the @Transactional annotation. I think the problem in you case might be, that you have the annotation on a private method. I'd move the annotation to the class level

    @Transactional
    public class MyFacade {
    
    public void databaseChanges() throws Exception {   
        A(); //update.
        B(); //insert
    }
    

    Then it should rollback properly. You can find more details here Does Spring @Transactional attribute work on a private method?

    0 讨论(0)
  • 2020-12-23 13:24

    Try this:

    @TransactionManagement(TransactionManagementType.BEAN)
    public class MyFacade {
    
    @TransactionAttribute(TransactionAttribute.REQUIRES_NEW)
    public void databaseChanges() throws Exception {   
        A(); //update.
        B(); //insert
    }
    
    0 讨论(0)
提交回复
热议问题