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.
Related
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 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").
I have two classes:
#Service
#Transaction
class A {
public void method1() {
private B;
try {
save1()
b.method2()
} catch (SqlException e) {
doSomeThing();
}
#Autowired
public setB(){
this.B = B;
}
}
}
#Service
class B {
#Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void method2(){
save2()
throw new SqlException();
}
}
I got an SqlException as expected, but also an UnexpectedRollBackException, and the program stops.
I want to know why the data persisted by save2() is not rolled back?
Is it a problem with outer transaction?
UPDATE: I tried catching UnexpectedRollBackException in class A and everything works fine. But I still need some kind of explanation why I get the exception? I suppose the outer transaction should be suspended when the inner transaction begins, so why the rollback is unexpected for the outer transaction?
Thanks.
First of all: you are not injecting the instance of B into class A via spring. i.e. your b is not managed by spring => this leads to the behaviour: Spring is ignoring the #Transactional annotation on method.
Second if you don't want to rollback on SqlException you have to specify noRollbackFor=SqlException.class
UPDATE: after clarification, the call is happening on managed bean. But the problem that expected behaviour - transaction inside of transaction is not supported in general by the transaction management system out of the box. https://docs.spring.io/spring/docs/4.3.11.RELEASE/javadoc-api/org/springframework/transaction/annotation/Propagation.html#REQUIRES_NEW
Unless the details on that system are provided it is impossible to step forward. Out of content the NESTED transaction propagation could be better, then REQUIRES_NEW, because it is making a rollback point for the external transaction and starts new one.
I have a simliar scenario like this and resolved it using propagation = Propagation.NESTED from where the second method is calling. rewrite the code as mentioned below and try.
#Service
class A {
#Transactional(rollbackFor = { HibernateException.class}, propagation = Propagation.NESTED)
public void method1() {
private B;
try {
save1()
b.method2()
} catch (SqlException e) {
doSomeThing();
}
#Autowired
public setB(){
this.B = B;
}
}
}
Note: Transactional is used in method level for class A with propagation nested . Class B shown below.
#Service
class B {
#Transactional(rollbackFor = {Exception.class}, propagation = Propagation.REQUIRED)
public void method2(){
save2()
throw new SqlException();
}
}
This has resolved the issue in my case.
In the service layer class in a method i'm calling a delete method and an insert method of a DAO layer as follows.
Service class:
#Service("workflowService")
#Transactional
public class WorkFlowServiceImpl implements WorkFlowService {
#Autowired
WorkFlowDao workFlowDao;
public String deleteSelectedTask(String strWorkFlowName, int intIndex) {
JSONObject res = new JSONObject();
try{
List<WorkflowPermission> listWorkflowPermission = workFlowDao.getWorkFlowPermissionByName(strWorkFlowName);
listWorkflowPermission.remove(intIndex-1);
boolean flag = workFlowDao.deleteWorkFlowPermissionByName(strWorkFlowName);
for(int i =0;i<listWorkflowPermission.size();i++){
listWorkflowPermission.get(i).setOrderNo(i+1);
flag = workFlowDao.createWorkFlowPermission(listWorkflowPermission.get(i));
}
if(flag==true){
res.put("status", "Success");
res.put("message", "Task Deleted Successfully");
}else{
res.put("status", "Fail");
res.put("message", "Cannot Delete Task");
}
}catch (Exception e) {
logger.error(e);
}
return res.toString();
}
}
Here first i'm getting a list from DAO and deleting the entries on that table and using the for loop i'm inserting as new record by changing the order by calling setOrderNO.
DAO class:
#Repository("workflowDao")
public class WorkFlowDaoImpl implements WorkFlowDao {
#Autowired
private SessionFactory sessionFactory;
private Session session;
#Override
public boolean deleteWorkFlowPermissionByName(String strWorkFlowName) {
try{
session = sessionFactory.getCurrentSession();
SQLQuery sqlQuery= session.createSQLQuery("DELETE FROM UPS_ESC_WTL WHERE workflow_name='"+strWorkFlowName+"'");
sqlQuery.executeUpdate();
session.flush();
return true;
}catch(Exception e){
e.printStackTrace();
}
return false;
}
public boolean createWorkFlowPermission(
WorkflowPermission workFlowPermission) {
boolean blStatus = false;
try {
session = sessionFactory.getCurrentSession();
session.saveOrUpdate(workFlowPermission);
session.flush();
blStatus = true;
} catch (Exception e) {
logger.error(e);
throw new DataAccessException();
}
return blStatus;
}
}
While debugging the code i'm getting the list form database and all records from the table gets deleted. While iterating over the list its properly going through the method of inserting the record. But its not reflecting in the db.
I want to handle transaction with rollback for this and to insert the records to db. Also i have added the HibernateTransactionManager in applicationContext.xml
<tx:annotation-driven transaction-manager="transactionManager"/>
<bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
Can anyone please help me to resolve this issue. Many thanks in advance.
Add the annotation #Transactional to all methods you need to share the same DB transaction, then the rollback would be applied to all operations.
Example
public class ServiceFirstImpl
#Autowired
FirstDao firstDao;
#Autowired
SecondDao secondDao;
#Transactional
public void mixActions(){
firstDao.delete();
secondDao.create();
}
One of your problem is on deleteWorkFlowPermissionByName method
}catch(Exception e){
e.printStackTrace();
}
Becouse the Spring Transaction will do an rollback if the method throw an RuntimeException or one declared exception.
Now your code will not trigger an rollback if there are an exception in deleteWorkFlowPermissionByName method.
It is enough to have #Transactional on the class in your case. Write to the method unless you want to have different transaction type to some methods.
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);
}
}
}