In my Spring MVC application, I have a method in a controller that needs to save a bunch of objects (built from an uploaded file) to a database. Let us leave aside for the moment the whole question about whether transactions should be done in the controller or service layer -- the point is that it should technically be feasible to do it in the controller, but I am finding problems. If you look at the code below, what I am expecting is that if any of the three calls to saveContact fails with an Exception (any exception, since I put rollbackFor = Exception.class ), then all three should be rolled back. Still, what I see is that if for example the third one fails, the data from the first two is still present in the database. The exception thrown is a PersistenceException, so I believe this should trigger the rollback, but it doesn't (it bubbles up to the client's browser, which is what I expected since I'm not catching it).
Here's my controller code:
package ch.oligofunds.oligoworld.web;
/*imports here*/
/**
* Handles requests for the application file upload requests
*/
@Controller("ExcelUploaderImpl")
@Transactional
public class ExcelUploaderImpl implements ExcelUploader {
@Autowired
PersoninfoDAO personinfoDAO;
/**
* Upload files using Spring Controller
* @throws SecurityException
* @throws NoSuchMethodException
* @throws DataAccessException
*/
@Override
@RequestMapping(value = "/importFundNAV", method = RequestMethod.POST)
public @ResponseBody String handleFileUpload(HttpServletRequest request, @RequestParam CommonsMultipartFile[] fileUpload) throws DataAccessException, NoSuchMethodException, SecurityException {
/*here save the uploaded file and initialize the serverFile variable*/
try {
success = readExcelfile(serverFile);
} catch (IOException e) {
logger.error("Failed to read the excel file", e);
result += "Failed to read the excel file\n" + e.getStackTrace() + "\n";
} finally {
serverFile.delete();
}
if (success) {
result += "You successfully imported file " + aFile.getOriginalFilename() + "\n";
} else {
result += "Failed to import file " + aFile.getOriginalFilename() + "\n";
}
}
return result;
}
@Override
public boolean readExcelfile(File xlfile) throws IOException, DataAccessException, NoSuchMethodException, SecurityException {
FileInputStream fis = new FileInputStream(xlfile); // Finds the workbook
// instance for XLSX
// file
XSSFWorkbook myWorkBook = new XSSFWorkbook(fis); // Return first sheet
// from the XLSX
// workbook
boolean success;
success = readFundDefinition(myWorkBook);
myWorkBook.close();
return success;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean readFundDefinition(XSSFWorkbook myWorkBook) throws DataAccessException, NoSuchMethodException, SecurityException {
/*here do stuff to extract data from the excel to initialize the administrator, custodian, invContact and success variables*/
saveContact(administrator);
saveContact(custodian);
saveContact(invContact);
/*If any of the three invocations to saveContact fails, I want all three inserts to rollback*/
return success;
}
@Override
public void saveContact(Personinfo personinfo) throws DataAccessException, NoSuchMethodException, SecurityException {
/*a bunch of stuff before this line*/
personinfo.copy(personinfoDAO.store(personinfo)); // <--- this is where the transaction could fail
/*a bunch of stuff after this line*/
}
}
Here's the interface for it:
@Controller
public interface ExcelUploader {
public @ResponseBody String handleFileUpload(HttpServletRequest request, @RequestParam CommonsMultipartFile[] fileUpload) throws DataAccessException, NoSuchMethodException, SecurityException;
public boolean readExcelfile(File xlfile) throws IOException, DataAccessException, NoSuchMethodException, SecurityException;
public boolean readFundDefinition(XSSFWorkbook myWorkBook) throws DataAccessException, NoSuchMethodException, SecurityException;
public void saveContact(Personinfo personinfo, Personaddress personAddress, Personemail personEmail, Personphone personPhone) throws DataAccessException, NoSuchMethodException, SecurityException;
public void readNAV(XSSFWorkbook myWorkBook);
public boolean isEmpty(Object object, Method... methods);
}
My web-context.xml contains:
<mvc:annotation-driven/>
<mvc:default-servlet-handler/>
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
<context:component-scan base-package="ch.oligofunds.oligoworld.web" scoped-proxy="interfaces" />
And my dao-context.xml contains:
<context:component-scan base-package="ch.oligofunds.oligoworld.dao" scoped-proxy="interfaces" />
<context:component-scan base-package="ch.oligofunds.oligoworld.security" scoped-proxy="interfaces" />
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
<context:property-placeholder location="classpath:CopyofoligoWorld-dao.properties" />
<!-- Using Atomikos Transaction Manager -->
<bean id="atomikosTransactionManager" class="com.atomikos.icatch.jta.UserTransactionManager" init-method="init"
destroy-method="close">
<property name="forceShutdown" value="true" />
<property name="startupTransactionService" value="true" />
<property name="transactionTimeout" value="60" />
</bean>
<bean id="atomikosUserTransaction" class="com.atomikos.icatch.jta.UserTransactionImp" />
<!-- Configure the Spring framework to use JTA transactions from Atomikos -->
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManager" ref="atomikosTransactionManager" />
<property name="userTransaction" ref="atomikosUserTransaction" />
<property name="transactionSynchronizationName" value="SYNCHRONIZATION_ON_ACTUAL_TRANSACTION" />
</bean>
<bean name="mysqlDS,springSecurityDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" >
<property name="driverClassName" value="${mysql.connection.driver_class}" />
<property name="username" value="${mysql.connection.username}" />
<property name="password" value="${mysql.connection.password}" />
<property name="url" value="${mysql.connection.url}" />
<property name="maxIdle" value="${mysql.minPoolSize}" />
<property name="maxActive" value="${mysql.maxPoolSize}" />
</bean>
Here's the exception that I thought would trigger the rollback:
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: Column 'name' cannot be null
It's not clear to me whether the @Transactional annotation is being picked up at all - from my understanding it should, since I'm scanning the ch.oligofunds.oligoworld.web package and the controller interface is annotated with @Controller. But my understanding may be wrong. :)
Any hints? Thanks