Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nested transaction on Spring

I found some strange behavior when using nested Spring transactions: when, in the same class, a method annotated as @Transactional calls another method also annotated as @Transactional the second annotation is not used.

Let's consider the following class:

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        final Main main = context.getBean(Main.class);
        // First Op
        System.out.println("Single insert: " + main.singleInsert());
        // Second Op
        main.batchInsert();
        // Third Op
        main.noTransBatchInsert();
    }

    @PersistenceContext
    private EntityManager pm;

    @Transactional(propagation=Propagation.REQUIRED)
    public void batchInsert() {
        System.out.println("batchInsert");
        System.out.println("First insert: " + singleInsert());
        System.out.println("Second insert: " + singleInsert());
    }

    public void noTransBatchInsert() {
        System.out.println("noTransBatchInsert");
        System.out.println("First insert: " + singleInsert());
        System.out.println("Second insert: " + singleInsert());
    }

    @Transactional(propagation=Propagation.REQUIRES_NEW)
    public int singleInsert() {
        System.out.println("singleInsert");
        Pojo p = new Pojo();
        pm.persist(p);
        return p.getId();
    }
}

The entity if the following class:

@Entity
public class Pojo {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Override
    public String toString() {
        return "Pojo: " + id;
    }

    public int getId() {
        return id;
    }
}

and the String parts applicationContext.xml:

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:context="http://www.springframework.org/schema/context" xmlns:jee="http://www.springframework.org/schema/jee"
    xmlns:tx="http://www.springframework.org/schema/tx" xmlns:task="http://www.springframework.org/schema/task"
    xsi:schemaLocation="
   http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-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/jee http://www.springframework.org/schema/jee/spring-jee-3.0.xsd
   http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
   http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd">

    <tx:annotation-driven />

    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
        <property name="persistenceUnitName" value="MyPersistenceUnit" />
    </bean>

    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>
</beans>

and the configuration class (I could have merge this in applicationContext.xml).

@Configuration
@ImportResource("/META-INF/applicationContext.xml")
public class Config {

    @Bean
    public Main main() {
        return new Main();
    }
}

For completeness the persistence.xml file:

<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0">

    <persistence-unit name="MyPersistenceUnit" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>

        <properties>
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
            <property name="hibernate.connection.driver_class" value="org.h2.Driver" />
            <property name="hibernate.connection.url" value="jdbc:h2:mem:TestDSJPA2;DB_CLOSE_DELAY=-1;LOCK_MODE=0" />
            <!--<property name="hibernate.connection.url" value="jdbc:h2:mem:TestDSJPA2;DB_CLOSE_DELAY=-1;LOCK_MODE=0" />-->
            <property name="hibernate.connection.username" value="sa" />
            <property name="hibernate.connection.password" value="" />
            <property name="hibernate.connection.autocommit" value="false"/>

            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="300" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="3000" />
        </properties>

    </persistence-unit>
</persistence>

So in the main class, the first operation is performed as expected that is in a new transaction. The output (including some DEBUG messages) is:

DEBUG o.h.transaction.JDBCTransaction  - begin
singleInsert
DEBUG o.h.transaction.JDBCTransaction  - commit
Single insert: 1

The second operation gives the following output:

batchInsert
singleInsert
DEBUG o.h.transaction.JDBCTransaction  - begin
First insert: 2
singleInsert
Second insert: 3
DEBUG

This is not what I expected since in annotating singleInsert with @Transactional(propagation=Propagation.REQUIRES_NEW) I would expect a new transaction to be created for every call which is not what's happening since the same top level transaction is used for both insertion.

The third operation fails as well as no transaction is created at all:

noTransBatchInsert
singleInsert
DEBUG o.h.e.def.AbstractSaveEventListener  - delaying identity-insert due to no transaction in progress
First insert: 0
singleInsert
DEBUG o.h.e.def.AbstractSaveEventListener  - delaying identity-insert due to no transaction in progress
Second insert: 0

In the @Configuration beans Spring ensures that calls to the method on the same class are proxified which is obviously not happening here. Is there a way do change this behavior?

like image 897
gabuzo Avatar asked Mar 08 '11 12:03

gabuzo


People also ask

Can transaction be nested?

A nested transaction is used to provide a transactional guarantee for a subset of operations performed within the scope of a larger transaction. Doing this allows you to commit and abort the subset of operations independently of the larger transaction.

What does @transactional do in Spring?

The @Transactional annotation makes use of the attributes rollbackFor or rollbackForClassName to rollback the transactions, and the attributes noRollbackFor or noRollbackForClassName to avoid rollback on listed exceptions. The default rollback behavior in the declarative approach will rollback on runtime exceptions.

Can @transactional annotation only be used at class level?

Annotation Type Transactional. Describes a transaction attribute on an individual method or on a class. When this annotation is declared at the class level, it applies as a default to all methods of the declaring class and its subclasses.


2 Answers

This behavior is the documented behavior of Spring when using the proxy mode for AOP. It can be changed by switching to the aspectj mode which perform code instrumentation either on compilation or at runtime.

like image 196
gabuzo Avatar answered Sep 20 '22 20:09

gabuzo


This is not specifically a problem with @Transactional. It is due to the configuration of your <tx:annotation-driven/>.

Spring uses two different AOP mechanisms: JDK dynamic proxies or CGLIB. JDK dynamic proxies is the default and it works through the use of interfaces at run-time. CGLIB works by generating subclasses at compile-time. If you specify <tx:annotation-driven proxy-target-class="true"/>, Spring will use CGLIB, and your second @Transactional will fire.

You can read more about the subject here.

like image 41
brettjonesdev Avatar answered Sep 22 '22 20:09

brettjonesdev