I am facing some issue with transactions configured in my code.
Below is the code with transactions which writes data to DB.
Writer.java
class Writer {
#Inject
private SomeDAO someDAO;
#Transactional(propagation = Propagation.REQUIRES_NEW)
public void write(){
this.batchWrite();
}
private void batchWrite () {
try {
someDAO.writeToTable1(true);
} catch(Exception ex) {
someDAO.writeToTable1(false);
}
someDAO.writeToTable2();
}
}
SomeDAO.java
class SomeDAO {
#Inject
private JdbcTemplate JdbcTemplate;
public void writeToTable1(boolean flag) {
// Writes data to table 1 using jdbcTemplate
jdbcTemplate.update();
}
pulic void writeToTable2() {
// Writes data to table 2 using jdbcTemplate
jdbcTemplate.update();
}
}
Here data is getting stored into table 1 properly but sometimes, table 2 is getting skipped.
I am not sure how this is happening as both the tables have been written within same transaction.
Either transaction is partially committing the data or partially rolling back.
I have doubt that in the SomeDAO class I am injecting JdbcTemplate object which is creating new connection instead of using existing connection of transaction.
Can anyone please help me here?
Try binding a Transaction Manager bean having your jdbcTemplate inside #Transactional:
//Create a bean
#Bean
public PlatformTransactionManager txnManager() throws Exception {
return new DataSourceTransactionManager(jdbcTemplate().getDataSource());
}
And then use this transaction manager in #Transactional("txnManager").
Related
I am working on a spring-batch, where after reader and processor, writer is responsible to populate data to DB. Writer is calling Service which internally calls DAO layer. In method insertToDB() if some exception occurs the transaction is not being rolled back. PSB my code.
public class MyWriter{
#Autowired
private MyService myService;
#Override
public void write(List<? extends MyBO> list) {
try{
for(MyBO bo: list){
myService.insert(bo);
}
}
catch(Exception e){
log.error("Cant write to DB")
}
}
public class MyService{
#Autowired
private TableOneDAO tableOneDao;
#Autowired
private TableTwoDAO tableTwoDAO;
#Autowired
private TableThreeDAO tableThreeDAO;
public void insert(MyBO bo){
try{
// do other stuff of processing bo and create entity
MyEntityTableOne myentity1 = getEntityT1(bo);
MyEntityTableTwo myentity2 = getEntityT2(bo);
MyEntityTableThree myentity3 = getEntityT3(bo);
insertToDB(myEntity1,myEntity2,myEntity3);
}
catch(Exception e){
log.error("Error occured.");
throw new MyException("Error Blah blah occured");
}
}
#Transactional(value = "txn1")
public void insertToDB(MyEntityTableOne entity1, MyEntityTableTwo entity2, MyEntityTableThree entity3) {
try{
tableOneDao.insert(entity1);
tableTwoDAO.insert(entity2);
tableThreeDAO.insert(entity3);
}
catch(Exception e){
log.error("Error occured during insert to DB");
throw new MyException("Error Blah blah occured during DB insert");
}
}
The code goes to the catch block, but doesn't rollback records. If some error occurs during insert of Table2 then entry for Table1 is not rolled-back. And if occurs during table3 insertion then table1 and table2 records are not rolled-back.
If I move the #Transactional annotation to insert() method it works fine. What is root cause of this issue. What I have to do if I want to have transaction only on insertToDB() method.
I am trying to make it simple: To support #Transactional spring wraps the implementing class into a so called proxy and surrounds the method call / class with the transactional logic.
Now you are calling the #Transactional annotated method within the same class. Therefore the proxy is not invoked and the transactional does not work. When moving the annotation to your insert method you are invoking the method from outside of the class which means you invoke the method against the proxy.
Thats a limitation of Spring AOP (?) I think.
You can do something like following to achieve what you want:
public class MyService{
#Ressource
private MyService self;
...
self.insertToDB(myEntity1,myEntity2,myEntity3)
Your item writer will be already called in a transaction driven by Spring Batch and that you can configure at the step level by providing the transaction manager and transaction attributes. So there is no need to use #Transactional in the downstream service used by the writer.
You need to remove that annotation from MyService and it should work as expected.
I have a ProcessRecon usecase class with a single method named execute. It saves an entity Reconciliation using paymentRepository.saveRecon and calls a web service as part of acknowledgement using paymentRepository.sendReconAck.
Now there's a chance that this external web service might fail in which case I want to rollback the changes i.e. the saved entity. Since I am using Unirest, it throws UnirestException which is a checked exception.
There are no errors on the console but this will probably be helpful [UPDATED].
2020-08-20 17:21:42,035 DEBUG [http-nio-7012-exec-6] org.springframework.transaction.support.AbstractPlatformTransactionManager: Creating new transaction with name [com.eyantra.payment.features.payment.domain.usecases.ProcessRecon.execute]:PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-com.mashape.unirest.http.exceptions.UnirestException
...
2020-08-20 17:21:44,041 DEBUG [http-nio-7012-exec-2] org.springframework.transaction.support.AbstractPlatformTransactionManager: Initiating transaction rollback
2020-08-20 17:21:44,044 DEBUG [http-nio-7012-exec-2] org.springframework.orm.jpa.JpaTransactionManager: Rolling back JPA transaction on EntityManager [SessionImpl(621663440<open>)]
2020-08-20 17:21:44,059 DEBUG [http-nio-7012-exec-2] org.springframework.orm.jpa.JpaTransactionManager: Not closing pre-bound JPA EntityManager after transaction
2020-08-20 17:22:40,020 DEBUG [http-nio-7012-exec-2] org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor: Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
What I see at the moment is that entity gets pushed to database even if there's a UnirestException. But I expect no data be saved to database.
I am using Spring Boot 2.3.3 with MySQL 5.7. This is the code I have for it.
ProcessRecon.java
#Usecase // this custom annotation is derived from #service
public class ProcessRecon {
private final PaymentRepository paymentRepository;
#Autowired
public ProcessRecon(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}
#Transactional(rollbackFor = UnirestException.class)
public Reconciliation execute(final Reconciliation reconciliation) throws UnirestException {
PaymentDetails paymentDetails = paymentRepository.getByReqId(reconciliation.getReqId());
if (paymentDetails == null)
throw new EntityNotFoundException(ExceptionMessages.PAYMENT_DETAILS_NOT_FOUND);
reconciliation.setPaymentDetails(paymentDetails);
Long transId = null;
if (paymentDetails.getImmediateResponse() != null)
transId = paymentDetails.getImmediateResponse().getTransId();
if (transId != null)
reconciliation.setTransId(transId);
if (reconciliation.getTransId() == null)
throw new ValidationException("transId should be provided in Reconciliation if there is no immediate" +
" response for a particular reqId!");
// THIS GETS SAVED
Reconciliation savedRecon = paymentRepository.saveRecon(reconciliation);
paymentDetails.setReconciliation(savedRecon);
// IF THROWS SOME ERROR, ROLLBACK
paymentRepository.sendReconAck(reconciliation);
return savedRecon;
}
}
PaymentRepositoryImpl.java
#CleanRepository
public class PaymentRepositoryImpl implements PaymentRepository {
#Override
public String sendReconAck(final Reconciliation recon) throws UnirestException {
// Acknowledge OP
return sendAck(recon.getRequestType(), recon.getTransId());
}
String sendAck(final String requestType, final Long transId) throws UnirestException {
// TODO: Check if restTemplate can work with characters (requestType)
final Map<String, Object> queryParams = new HashMap<String, Object>();
queryParams.put("transId", transId);
queryParams.put("requestType", requestType);
logger.debug("{}", queryParams);
final HttpResponse<String> result = Unirest.get(makeAckUrl()).queryString(queryParams).asString();
logger.debug("Output of ack with queryParams {} is {}", queryParams, result.getBody());
return result.getBody();
}
#Override
public Reconciliation saveRecon(final Reconciliation recon) {
try {
return reconDS.save(recon);
}
catch (DataIntegrityViolationException ex) {
throw new EntityExistsException(ExceptionMessages.CONSTRAINT_VIOLATION);
}
}
}
ReconciliationDatasource.java
#Datasource // extends from #Repository
public interface ReconciliationDatasource extends JpaRepository<Reconciliation, Long> {
List<Reconciliation> findByPaymentDetails_User_Id(Long userId);
}
To make annotations work you have to use interfaces instead of classes for dependency injection.
interface ProcessRecon {
Reconciliation execute(final Reconciliation reconciliation)
throws UnirestException;
}
Then
#Usecase
public class ProcessReconImpl implements ProcessRecon {
private final PaymentRepository paymentRepository;
#Autowired
public ProcessReconImpl(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}
#Transactional(rollbackFor = UnirestException.class)
public Reconciliation execute(final Reconciliation reconciliation) throws UnirestException {
//method implementation...
}
}
Usage
#Autowired
ProcessRecon processRecon;
public void executeServiceMethod(Reconciliation reconciliation) {
processRecon.execute(reconciliation)
}
This way you have got proxy of ProcessReconImpl with provided by annotations additional functionality.
I assumed the default engine for the tables would be InnoDB but to my surpise, the tables were created using MyISAM engine which doesn't support transactions.
I resolved the problem by using the below property as suggested here
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
instead of
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
That was the only change required. Thanks!
I'm implementing a JDBC database access API (basically a wrapper) and I'm usnig Spring JdbcTemplate with PlatformTransactionManager to handle transactional operations. Everything looks ok, but I cannot understand how jdbcTemplate manage concurrent transactions.
I'll give you a simplified example based on the creation of students to make my point. Let's create 2 students, John and Jack. The first without erros and the seconds with one error, there's the steps and the code below.
John starts a transaction
Execute John insert without commit
Wait for Jack insert
Jack starts a transaction
Execute Jack insert with an error (age as null but database required NON - NULL)
Rollback Jack transaction
Commit John trasaction
StudentDAO
public class StudentJDBCTemplate implements StudentDAO {
private DataSource dataSource;
private JdbcTemplate jdbcTemplateObject;
private PlatformTransactionManager transactionManager;
// constructor, getters and setters
public TransactionStatus startTransaction() throws TransactionException {
TransactionDefinition def = new DefaultTransactionDefinition();
transactionManager.getTransaction(def);
}
public void commitTransaction(TransactionStatus status) throws TransactionException {
transactionManager.commit(status);
}
public void rollbackTransaction(TransactionStatus status) throws TransactionException {
transactionManager.rollback(status);
}
public void create(String name, Integer age){
String SQL1 = "insert into Student (name, age) values (?, ?)";
jdbcTemplateObject.update( SQL1, name, age);
return;
}
}
MainApp
public class MainApp {
public static void main(String[] args){
// setup db connection etc
StudentJDBCTemplate studentDao = new StudentJDBCTemplate();
TransactionStatus txJohn = studentDao.startTransaction();
TransactionStatus txJack = studentDao.startTransaction();
studentDao.create("John", 20);
try {
studentDao.create("Jack", null); // **FORCE EXCEPTION**
} catch(Exception e){
studentDao.rollback(txJack);
}
studentDao.commit(txJohn);
}
}
How JdbcTemplate knows that 1 transaction is ok but the other is not? From my undertanding, despite we have created 2 transactions, JdbcTemplate will rollback Jack AND John transactions, because query, execute and update methods does not require TransactionStatus as a parameter. That means that Spring jdbcTemplate only supports 1 transaction at time?!
All the operations in a single transaction are always executed as a single unit so either all will be committed or none.
If John starts a transaction which insert and then update then either both (insert and update) will succeed or none and will not be impacted by the transaction started by Jack.
Now how the concurrent transactions interfere with each other is controlled by isolation level i.e. how a transaction sees data modified by another concurrent transaction.
I am using JPA with Spring. I am trying to do batch import. If there is problem with batch import then I would like to insert individually, and if this fails also then I would like to save to duplicates table. I wrote a logic for this but I get this error everytime:
Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly
Mine setting for JPA are like this:
#Bean(name = "dataSource", destroyMethod = "")
public DataSource getDataSource() {
return new JndiDataSourceLookup().getDataSource(props.getDbJndiName());
}
#Bean
public JpaVendorAdapter getHibernateJpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
#Bean
public LocalContainerEntityManagerFactoryBean getEntityManagerFactoryBean() {
LocalContainerEntityManagerFactoryBean lcemfb = new LocalContainerEntityManagerFactoryBean();
lcemfb.setDataSource(getDataSource());
lcemfb.setPersistenceUnitName("MyPU");
lcemfb.setPackagesToScan("com.project");
lcemfb.setJpaVendorAdapter(getHibernateJpaVendorAdapter());
lcemfb.setJpaProperties(getHibernateProperties());
return lcemfb;
}
#Bean
public Properties getHibernateProperties() {
Properties jpaProperties = new Properties();
jpaProperties.put(DIALECT, "org.hibernate.dialect.Oracle10gDialect");
jpaProperties.put(SHOW_SQL, true);
jpaProperties.put(AUTOCOMMIT, true);
jpaProperties.put(FORMAT_SQL, true);
jpaProperties.put(USE_SQL_COMMENTS, true);
jpaProperties.put(STATEMENT_BATCH_SIZE, 20);
jpaProperties.put(ORDER_INSERTS, true);
jpaProperties.put("hibernate.ejb.entitymanager_factory_name", "MyEM");
return jpaProperties;
}
#Bean
public JpaTransactionManager getTransactionManager() {
return new JpaTransactionManager(getEntityManagerFactoryBean().getObject());
}
#Bean
public PersistenceExceptionTranslationPostProcessor getPersistenceExceptionTranslationPostProcessor() {
return new PersistenceExceptionTranslationPostProcessor();
}
I get entity manager like this
#PersistenceContext(unitName = "MyPU")
private EntityManager em;
protected EntityManager em() {
return em;
}
my import method is:
#Override
#Transactional
public void importBusinessFile(MultipartFile file)
throws GeneralException, IOException {
// process file
//save batch
dealsRepository.saveBatch(deals);
}
and saveBatch method from repository:
public void saveBatch(List<Deal> list) {
for (Deal deal : list) {
em().persist(deal);
}
try {
em().flush();
em().clear();
} catch (Exception e) {
log.info("Duplicates detected, save individually.", e);
for (Deal deal : list) {
try {
save(deal);
} catch (Exception ex) {
log.error("Problem saving individual deal", e);
// TODO write to duplicates
}
}
}
}
I tried setting dontRollbackOn but I can't get past this exception. I found some other similar threads but none helped me.
In case if you method has #Transactional annotation, occurrence of any exception inside your method marks the surrounding transaction as roll-back.
You can add an attribute for #Transactional annotation to prevent it of rolling back like : #Transactional(noRollbackFor=Exception.class). Spring rollback transaction for all sub type of runtime exceptions.
If you want to do something when you catch you should try to do it in new transaction.But remeber that self invocation in spring not supported , you can't just call transactional method2 from method1 , you should get from spring context current service and call method2.
PROPAGATION_NESTED uses a single physical transaction with multiple
savepoints that it can roll back to. Such partial rollbacks allow an
inner transaction scope to trigger a rollback for its scope, with the
outer transaction being able to continue the physical transaction
despite some operations having been rolled back. This setting is
typically mapped onto JDBC savepoints, so will only work with JDBC
resource transactions. See Spring’s DataSourceTransactionManager.
simple variant :
#Autowired
private ApplicationContext context.
#Override
#Transactional
public void importBusinessFile(MultipartFile file)
throws GeneralException, IOException {
// process file
try{
dealsRepository.saveBatch(deals);
//in case fail-transaction for saveBatch is rollback main transactio is active
}catch(Exception e){
context.getBean(curent serivce).tryReSaveBatch(deals);
//in case fail - transaction for tryReSaveBatchis rollback ,
main transactio is active
}
// main transaction commited
}
#Transactional(propagation = NESTED)
public void saveBatch(List<Deal> list) {
for (Deal deal : list) {
em().persist(deal);
}
}
#Transactional(propagation = NESTED)
public void tryReSaveBatch(List<Deal> list) {
for (Deal deal : list) {
try {
save(deal);
} catch (Exception ex) {
log.error("Problem saving individual deal", e);
// TODO write to duplicates
}
}
}
I only managed to fix this by creating another bean containing batch import method. So after that Spring can intercept the call from this bean and start a new transaction.
In our application we have upgraded from Hibernate 3.5.6-final to 4.2.21.Final and now we are getting a ConcurrentModificationException when a database transaction is committed:
java.util.ConcurrentModificationException: null
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:386)
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:304)
at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:349)
at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:56)
at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1195)
at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:404)
at org.hibernate.engine.transaction.internal.jdbc.JdbcTransaction.beforeTransactionCommit(JdbcTransaction.java:101)
at org.hibernate.engine.transaction.spi.AbstractTransactionImpl.commit(AbstractTransactionImpl.java:175)
Is this a known issue with Hibernate 4.2?
The exception turned out to be caused by a problem with a Hibernate custom constraint validator we use. The validator's isValid was running a Hibernate criteria query. The query triggered a Hibernate session flush which resulted in the ConcurrentModificationException. We fixed the problem by temporarily disabling auto flush in the isValid method:
#Override
public boolean isValid(Object object, final ConstraintValidatorContext c) {
try {
sessionFactory.getCurrentSession().setFlushMode(FlushMode.MANUAL);
...
} finally {
sessionFactory.getCurrentSession().setFlushMode(FlushMode.AUTO);
}
}
The problem may also manifest itself as a StackOverflowError.
I had this with hibernate 5.0.11 and verified it happened also with 5.2.5. My solution was to annotate the custom validator to open new transaction.
#Transactional(propagation=Propagation.REQUIRES_NEW)
I guess hibernate has still some way to go before custom constraint validators are made easy to setup and use since this took me way more time than it should.
Edit: Issue related. I think using the same transaction violates jpa2.1 specs https://hibernate.atlassian.net/browse/HHH-7537
In the class which implements a ConstraintValidator, we need to have an instance of EntityManager but we are not in a Spring context in order to instanciate automatically an EntityManager object with annotation #Autowired. So, in the configuration package, we can write a factory wich allows to have an instance of Application in order to instanciate beans when we are not in a Spring context.
#Configuration
public class ApplicationContextConf {
#Bean
public static ApplicationContextProvider contextProvider() {
return new ApplicationContextProvider();
}
}
#Component
public class ApplicationContextProvider implements ApplicationContextAware {
private static ApplicationContext context;
public ApplicationContext getApplicationContext() {
return context;
}
#Override
public void setApplicationContext(final ApplicationContext ctx) {
context = ctx;
}
}
In the class which implements a ConstraintValidator, we instanciate an EntityManager thanks to the factory created previously in the method initialize.
Before to call to a method which calls the repository, we change the flush mode of the Hibernate current session to FlushMode.MANUAL in order to avoid having the automatic flush after the call to the repository while keeping the default flush mode. In the block finally, we restore the default value of the flush mode previously retained.
private EntityManager entityManager;
#Override
public void initialize(final Object object ) {
// ...
try {
this.entityManager = ApplicationContextConf
.contextProvider()
.getApplicationContext()
.getBean(EntityManager.class);
}
catch (final BeansException ex) {
// ...
}
}
#Override
public boolean isValid(final Object object, final ConstraintValidatorContext context) {
Session hibernateSession = null;
FlushMode originalFlushMode = null;
try {
hibernateSession = this.entityManager.unwrap(Session.class);
originalFlushMode = hibernateSession.getFlushMode();
hibernateSession.setFlushMode(FlushMode.MANUAL);
// ...
}
finally {
if (hibernateSession != null) {
hibernateSession.setFlushMode(originalFlushMode);
}
}
}