I have a method, 'databaseChanges', which call 2 operations: A, B in iterative way. 'A' first, 'B' last. 'A' & 'B' can be Create, Update Delete functionalities in my persistent storage, Oracle Database 11g.
Let's say,
'A' update a record in table Users, attribute zip, where id = 1.
'B' insert a record in table hobbies.
Scenario: databaseChanges method is been called, 'A' operates and update the record. 'B' operates and try to insert a record, something happen, an exception is been thrown, the exception is bubbling to the databaseChanges method.
Expected: 'A' and 'B' didn't change nothing. the update which 'A' did, will be rollback. 'B' didn't changed nothing, well... there was an exception.
Actual: 'A' update seems to not been rolled back. 'B' didn't changed nothing, well... there was an exception.
Some Code
If i had the connection, i would do something like:
private void databaseChanges(Connection conn) {
try {
conn.setAutoCommit(false);
A(); //update.
B(); //insert
conn.commit();
} catch (Exception e) {
try {
conn.rollback();
} catch (Exception ei) {
//logs...
}
} finally {
conn.setAutoCommit(true);
}
}
The problem: I don't have the connection (see the Tags that post with the question)
I tried to:
@Service
public class SomeService implements ISomeService {
@Autowired
private NamedParameterJdbcTemplate jdbcTemplate;
@Autowired
private NamedParameterJdbcTemplate npjt;
@Transactional
private void databaseChanges() throws Exception {
A(); //update.
B(); //insert
}
}
My AppConfig class:
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
@Configuration
public class AppConfig {
@Autowired
private DataSource dataSource;
@Bean
public NamedParameterJdbcTemplate namedParameterJdbcTemplate() {
return new NamedParameterJdbcTemplate(dataSource);
}
}
'A' makes the update. from 'B' an exception is been thrown. The update which been made by 'A' is not been rolled back.
From what i read, i understand that i'm not using the @Transactional correctly. I read and tried several blogs posts and stackverflow Q & A without succeess to solve my problem.
Any suggestions?
EDIT
There is a method that call databaseChanges() method
public void changes() throws Exception {
someLogicBefore();
databaseChanges();
someLogicAfter();
}
Which method should be annotated with @Transactional,
changes()? databaseChanges()?
What you seem to be missing is a
TransactionManager
. The purpose of theTransactionManager
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)
Define a Transaction Manager in Spring Boot Configuration
Something like this
You would also need to add the
@EnableTransactionManagement
annotation (not sure if this is for free in newer versions of spring boot.Add @Transactional
Here you would add the
@Transactional
annotation for the method that you want to participate in the transactionNote that this method should be public and not private. You may want to consider putting
@Transactional
on the public method callingdatabaseChanges()
.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
@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:
But this is what you actually get when you inject a bean:
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 exampleb()
will not be wrapped in transaction:It is also because
@Transactional
works by proxying your object. In example abovea()
will callX.b()
not a enhanced "spring proxy" methodb()
so there will be no transaction. As a workaround you have to callb()
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 useTransactionTemplate
instead of declarative transactions:Update
Answering to OP updated question using info above.
Make sure
changes()
is called "from outside" of a bean, not from class itself and after context was instantiated (e.g. this is notafterPropertiesSet()
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).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
Then it should rollback properly. You can find more details here Does Spring @Transactional attribute work on a private method?
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 aRuntimeException
) 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 addrollbackFor = Exception.class
The change supposed to be on the service class, the code will be like that:
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:The final code will be like that:
Try this: