1
votes

I'm developing an application that receives various XML files containing different 'Individual' objects that I store in a database. The goal is that all the files are read and treated. If a file is processed correctly it moves to the "processed" folder. If an exception is thrown it moves to an error folder.

The desired behaviour is that if an error occurs in one of the files, everything is rollbacked , nothing is saved in the database and all the files are copied to the error folder (also the already processed ones)

The copying of the folders probably can't be done using transactions so I do them manually...

The structure of my project is the following:

project structure

Technologies:

  • Hibernate : 3.5.0-Final
  • Spring : 3.1.1.RELEASE
  • Server : Tomcat 7
  • Database : SQL Server

I start from the idea that the best location to put the transaction is the service. I don't add the propagation property, since I want the default Property.REQUIRED behaviour:

@Transactional(rollbackFor = Exception.class)
private Feedback readIndividuals(File fileLocation) throws Exception {
    System.out.println("Start reading individuals");
    //Set the status of all database entries to DELETED
    individualEntityService.setAllStatussesToDeleted();
    }
    final File individualsProcessedFolder = new File(individualsProcessedFolderLocation);
    for (final File fileEntry : fileLocation.listFiles()) {
        if (fileEntry.isDirectory()) {
            readIndividuals(fileEntry, feedback);
         } else {
            individualReader.read(fileEntry.getAbsolutePath());
    ....

Here I start the transaction. The individualReader is a service that performs the actual reading of the file and the writing to the DB.

EDIT Here the code of the IndividualReader where I call the add method in the EntityService:

 @Override
@Transactional
public void read(String fileLocation) throws Exception {

    JAXBContext jaxbContext = JAXBContext.newInstance(CDM.class);
    Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();

    XMLInputFactory factory = XMLInputFactory.newInstance();
    FileInputStream fileInputStream = new FileInputStream(fileLocation);
    XMLStreamReader xmlr = factory.createXMLStreamReader(fileInputStream, ENCODING);

    try {
        xmlr.nextTag();
        xmlr.require(XMLStreamConstants.START_ELEMENT, null, "CDM");

        xmlr.nextTag();
        while (xmlr.getEventType() == XMLStreamConstants.START_ELEMENT) {

            JAXBElement<CDM.Individual> jaxbIndividual = unmarshaller.unmarshal(xmlr,
                    CDM.Individual.class);
            CDM.Individual individual = jaxbIndividual.getValue();
            Individual individualDO = individualBuilder.build(individual);
            Set<Diploma> diplomas = diplomaBuilder.build(individual.getDiplomas(), individualDO);
            Set<HealthCareProfessional> healthCareProfessionals = healthCareProfessionalBuilder.build(individual.getHCProfessionals());
            individualDO.addHealthCareProfessionals(healthCareProfessionals);
            individualDO.addDiplomas(diplomas);
            LOG.debug("Adding individual with SSIN  [" + individualDO.getSsin() + "] into DB");
            Individual retrievedIndividual = individualEntityService.read(individualDO.getSsin());
            if (retrievedIndividual != null) {
                  individualEntityService.remove(retrievedIndividual);
                  individualDO.setStatus(EntryStatus.UPDATED);
            }
            individualEntityService.add(individualDO);
            LOG.debug("Individual with SSIN [" + individualDO.getSsin() + "] successfully added to DB");
            LOG.debug(getIndividualXMLAsString(individualDO));

            if (xmlr.getEventType() == XMLStreamConstants.CHARACTERS) {
                xmlr.next();
            }
        }
    } finally {
        xmlr.close();
        fileInputStream.close();
    }
}

The lower level is the EntityService:

@Override
@Transactional
public void add(Individual individual) {
    individualDao.addIndividual(individual);
}

This class doesn't do anything more than calling the DAO, I annotated it with the @Transactional annotation. Since the default is Propagation.REQUIRED it won't start a new physical transaction, but it will join the transaction of the service.

Finally we have the DAO:

@Transactional
public void addIndividual(Individual individual) {
    em.persist(individual);
} 

I also annotate this method with Transactional, with the same reason as above. The Entity manager is autowired in the DAO using Spring:

@PersistenceContext
private EntityManager em;

The Entity Manager is defined in the applicationContext as follows:

<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="persistenceUnitName" value="individual"/>
    <property name="jpaVendorAdapter">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
            <property name="databasePlatform" value="org.hibernate.dialect.SQLServerDialect"/>
            <property name="generateDdl" value="true"/>
            <property name="showSql" value="false"/>
        </bean>
    </property>
    <property name="jpaProperties">
        <props>
            <prop key="hibernate.dialect">org.hibernate.dialect.SQLServerDialect</prop>
        </props>
    </property>
    <property name="dataSource" ref="dataSource"/>
</bean>

Now everything compiles and deploys fine and works even as expected. But when I make one of the XML files corrupt all the files before the corrupt file end up in the DB and the transaction is not rollbacked.

I guess I must be missing something and probably my mistake is in the wrong usage of the combination @Transaction and the Spring EntityManager. I never use the explicit em.flush() to push the data to the DB. Maybe the em.persist is wrong and stores the data to the database and I can't recover from it...

Anyone has an idea of what I'm doing wrong? Help would be highly appreciated.

EDIT Hereby the complete context:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:context="http://www.springframework.org/schema/context"
   xmlns:tx="http://www.springframework.org/schema/tx"
   xmlns:task="http://www.springframework.org/schema/task"
   xsi:schemaLocation="http://www.springframework.org/schema/task
   http://www.springframework.org/schema/task/spring-task-3.0.xsd
   http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
   http://www.springframework.org/schema/context
   http://www.springframework.org/schema/context/spring-context-3.0.xsd
   http://www.springframework.org/schema/tx
   http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">

<context:component-scan base-package="be.healthconnect.pwg" />
<task:annotation-driven />
<tx:annotation-driven />

<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="location">
        <value>classpath:/be/healthconnect/pwg/core/properties/pwg.properties</value>
    </property>
</bean>

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
      destroy-method="close">
    <property name="driverClassName" value="${datasource.driver.class.name}" />
    <property name="url" value="${datasource.url}" />
    <property name="username" value="${datasource.username}" />
    <property name="password" value="${datasource.password}" />
</bean>

<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="persistenceUnitName" value="individual"/>
    <property name="jpaVendorAdapter">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
            <property name="databasePlatform" value="org.hibernate.dialect.SQLServerDialect"/>
            <property name="generateDdl" value="true"/>
            <property name="showSql" value="false"/>
        </bean>
    </property>
    <property name="jpaProperties">
        <props>
            <prop key="hibernate.dialect">org.hibernate.dialect.SQLServerDialect</prop>
        </props>
    </property>
    <property name="dataSource" ref="dataSource"/>
</bean>

<bean id="jpaVendorAdaptor"
      class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="emf" />
</bean>
</beans>
1
Do you have <tx:annotation-driven>?Sotirios Delimanolis
Yes, I have specified it in my application-context.xml, including the correct namespace definitionsMathias G.
Please post the whole context. Some config is probably off.Sotirios Delimanolis
Done, I added it to my initial postMathias G.
I don't see where it is you're calling add. The most likely cause of something like this is inadvertently crossing a transaction boundary somewhere.chrylis -cautiouslyoptimistic-

1 Answers

0
votes

The mistake I made was the following: @Transactional annotation had no effect because it annotated a private method. The proxy generator was ignoring it.

I found the solution in the Spring Manual chapter 10.5.6:

Method visibility and @Transactional

When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.