11

I have created a Spring Boot application using 1.2.0 version with spring-boot-starter-data-jpa and I am using MySQL. I have configured my MySQL properties in application.properties file correctly.

I have a simple JPA Entity, Spring Data JPA Repository and a Service as follows:

@Entity
class Person
{
    @Id @GeneratedValue(strategy=GenerationType.AUTO)
    private Integer id;
    private String name;
    //setters & getters
}

@Repository
public interface PersonRepository extends JpaRepository<Person, Integer>{

}


import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
class PersonService
{
    @Autowired PersonRepository personRepository;

    @Transactional
    void save(List<Person> persons){
        for (Person person : persons) {         
            if("xxx".equals(person.getName())){
                throw new RuntimeException("boooom!!!");
            }
            personRepository.save(person);          
        }
    }
}

I have the following JUnit test:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {

    @Autowired
    PersonService personService;

    @Test
    public void test_logging() {
        List<Person> persons = new ArrayList<Person>();
        persons.add(new Person(null,"abcd"));
        persons.add(new Person(null,"xyz"));
        persons.add(new Person(null,"xxx"));
        persons.add(new Person(null,"pqr"));

        personService.save(persons);
    }

}

The expectation here is it should not insert any records into PERSON table as it will throw Exception while inserting 3rd person object. But it is not getting rolled back, first 2 records are getting inserted and committed.

Then I thought of quickly try with JPA EntityManager.

@PersistenceContext
private EntityManager em;

em.save(person);

Then I am getting javax.persistence.TransactionRequiredException: No transactional EntityManager available Exception.

After googling for sometime I encounter this JIRA thread https://jira.spring.io/browse/SPR-11923 on the same topic.

Then I updated the Spring Boot version to 1.1.2 to get Spring version older than 4.0.6.

Then em.save(person) working as expected and Transaction is working fine (means it is rollbacking all the db inserts when RuntimeException occurred).

But even with Spring 4.0.5 + Spring Data JPA 1.6.0 versions transactions are not working when personRepository.save(person) is used instead of em.persist(person).

It seems Spring Data JPA repositories are committing the transactions.

What am I missing? How to make Service level @Transactional annotations work?

PS:

Maven pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.sivalabs</groupId>
    <artifactId>springboot-data-jpa</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>springboot-data-jpa</name>
    <description>Spring Boot Hello World</description>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.0.RELEASE</version>
        <relativePath />
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>com.sivalabs.springboot.Application</start-class>
        <java.version>1.7</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <dependencies>
    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>
</project>

Application.java

@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
K. Siva Prasad Reddy
  • 11,786
  • 12
  • 68
  • 95

3 Answers3

4

Change Transactional annotation to @Transactional(rollbackFor=Exception.class)

Parveen Verma
  • 16,690
  • 1
  • 14
  • 19
Abhinay
  • 464
  • 5
  • 13
  • 2
    That's default Spring behavior, no need to specify explicitly. – K. Siva Prasad Reddy Jun 04 '15 at 06:41
  • I faced same problem . So I kept my Exception class, It solved – Abhinay Jun 04 '15 at 07:05
  • If it is checked exception then we need to specify explicitly. If it is unchecked exception like RuntimeException or any subclasses of it then spring will automatically rollsback transaction. – K. Siva Prasad Reddy Jun 04 '15 at 07:31
  • 1
    Adding `@Transactional` to the repository was previously unnecessary when I was developing with the Spring auto-configured `EntityManagerFactory` but now that I have changed to creating my own, this annotation is necessary #SpringVoodoo – Adam Jun 07 '17 at 09:11
2

From comment by @m-deinum:

Make your PersonService public as well as the method you are calling.

This seems to have done the trick for several users. The same thing is also covered in this answer, citing manual saying:

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.

Community
  • 1
  • 1
eis
  • 51,991
  • 13
  • 150
  • 199
1

I tested this with SpringBoot v1.2.0 and v1.5.2 and all is working as expected, you don't need to use entity manager neither @Transactional(rollbackFor=Exception.class).

I could not see which part you misconfigured, all seems fine at first glance, so I am just posting all my config as reference with more updated annotations and H2 embedded memory DB:

application.properties

# Embedded memory database
spring.jpa.hibernate.ddl-auto=create
spring.datasource.url=jdbc:h2:~/test;AUTO_SERVER=TRUE

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.demo</groupId>
    <artifactId>jpaDemo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.2.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
    </dependencies>
</project>

Person.java

@Entity
public class Person
{
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private Integer id;
    private String name;

    public Person() {}
    public Person(String name) {this.name = name;}
    public Integer getId() {return id;}
    public void setId(Integer id) {this.id = id;}
    public String getName() {return name;}
    public void setName(String name) {this.name = name;}
}

PersonRepository.java

@Repository
public interface PersonRepository extends JpaRepository<Person, Integer> {}

PersonService.java

@Service
public class PersonService
{
    @Autowired
    PersonRepository personRepository;

    @Transactional
    public void saveTransactional(List<Person> persons){
        savePersons(persons);
    }

    public void saveNonTransactional(List<Person> persons){
        savePersons(persons);
    }

    private void savePersons(List<Person> persons){
        for (Person person : persons) {
            if("xxx".equals(person.getName())){
                throw new RuntimeException("boooom!!!");
            }
            personRepository.save(person);
        }
    }
}

Application.java

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

PersonServiceTest.java

@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonServiceTest {

    @Autowired
    PersonService personService;

    @Autowired
    PersonRepository personRepository;

    @Test
    public void person_table_size_1() {
        List<Person> persons = getPersons();
        try {personService.saveNonTransactional(persons);}
        catch (RuntimeException e) {}

        List<Person> personsOnDB = personRepository.findAll();
        assertEquals(1, personsOnDB.size());
    }

    @Test
    public void person_table_size_0() {
        List<Person> persons = getPersons();
        try {personService.saveTransactional(persons);}
        catch (RuntimeException e) {}

        List<Person> personsOnDB = personRepository.findAll();
        assertEquals(0, personsOnDB.size());
    }

    public List<Person> getPersons(){
        List<Person> persons = new ArrayList() {{
            add(new Person("aaa"));
            add(new Person("xxx"));
            add(new Person("sss"));
        }};

        return persons;
    }
}

*The database is cleared and re-initialized for each test so that we always validate against a known state

Javier Sánchez
  • 984
  • 6
  • 7