Duplicate Key Error on saving Reactor Mongo result - spring

I'm seeing the following error when attempting to query and update some records to MongoDB using reactive streams:
org.springframework.dao.DuplicateKeyException: E11000 duplicate key error collection: testtrans.submission index: _id_ dup key: { _id: ObjectId('600b10b2fbac4f4483af3e67') }; nested exception is com.mongodb.MongoWriteException
I'm not sure what I'm doing wrong; How do I go about querying and saving results to the database using reactive mongo in a single transaction?
My service class:
package com.example.reactivemongotransaction.service;
import com.example.reactivemongotransaction.dto.Submission;
import com.example.reactivemongotransaction.repository.SubmissionRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
#Slf4j
#Service
public class SubmissionService {
#Autowired
private SubmissionRepository submissionRepository;
public Mono<Submission> saveSubmission(Submission submission) {
log.debug("saving {}", submission);
return submissionRepository.save(submission);
}
#Transactional
public Flux<Submission> lockSubmissions() {
log.debug("setting all locked");
Flux<Submission> submissionFlux = submissionRepository.findAllByLockedFalse();
return submissionFlux
.map(submission -> submission.setLocked(true))
.flatMap(submission -> submissionRepository.save(submission));
}
}
My config:
#Configuration
#EnableMongoAuditing
#EnableTransactionManagement
public class MongoConfiguration {
#Bean
public ReactiveTransactionManager transactionManager(ReactiveMongoDatabaseFactory dbFactory) {
return new ReactiveMongoTransactionManager(dbFactory);
}
}
Controller:
#RestController
#RequestMapping("/submissions")
public class SubmissionController {
#Autowired
private SubmissionService submissionService;
#PostMapping
public Mono<Submission> saveSubmission(final #RequestBody Submission submission) {
return submissionService.saveSubmission(submission);
}
#GetMapping("/lockall")
public Flux<Submission> lockAll() {
return submissionService.lockSubmissions();
}
}
Model:
#ToString
#Getter
#Setter
#Accessors(chain = true)
#Document(collection = "submission")
#TypeAlias("payload")
public class Submission implements Persistable<String> {
#Id
private String id;
#Field("role_name")
#Indexed(unique = true)
private String role;
#CreatedDate
private ZonedDateTime created;
#LastModifiedDate
private ZonedDateTime updated;
private Boolean deleted;
private Boolean enabled;
private boolean locked;
#Override
#JsonIgnore
public boolean isNew() {
if(getCreated() == null)
return true;
else
return false;
}
}
Repository:
public interface SubmissionRepository extends ReactiveMongoRepository<Submission, String> {
Flux<Submission> findAllByLockedFalse();
}
Main class:
#EnableReactiveMongoRepositories
#SpringBootApplication
public class ReactivemongotransactionApplication {
public static void main(String[] args) {
SpringApplication.run(ReactivemongotransactionApplication.class, args);
}
}
application.yml:
spring:
data:
mongodb:
uri: 'mongodb://localhost:27017/testtrans'
server:
port: 8280
Maven pom:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>reactivemongotransaction</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>reactivemongotransaction</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<lombok.version>1.18.6</lombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
My request is GET http://localhost:8280/submissions/lockall which return 500. And the logs show errors e.g.:
com.mongodb.MongoWriteException: E11000 duplicate key error collection: testtrans.submission index: _id_ dup key: { _id: ObjectId('600b10b2fbac4f4483af3e67') }
at com.mongodb.internal.async.client.AsyncMongoCollectionImpl.lambda$executeSingleWriteRequest$9(AsyncMongoCollectionImpl.java:1075) ~[mongodb-driver-core-4.1.1.jar:na]

The issue was related to the #CreatedDate not being set (see #EnableMongoAuditing and #CreatedDate Auditing not working in Spring Boot 2.4.3).
Resolved by reverting spring-boot-starter-parent to 2.3.5.RELEASE.

Related

Getting null value while fetching properties from application.properties file in Springboot

Objective : reading property value from application.properties file in my Java class .
Current behaviour : getting null values
Expected behaviour :
value1
value3
application.properties
category1.subcategory2=value1
category1.subcategory3=value3
greeting.salutation=Hello
TryValueApplication.java
package com.example.tryvalue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class TryValueApplication {
#Value("${category1.subcategory2}")
private String category1;
#Value("basic value")
private String category3;
public static void main(String[] args) {
SpringApplication.run(TryValueApplication.class, args);
TryValueApplication tryValueApplication = new TryValueApplication();
System.out.println("Running main method");
tryValueApplication.printCategory();
}
public void printCategory(){
System.out.println("Print environment values");
System.out.println(category1);
System.out.println(category3);
}
}
Tried :
I tried accessing same value from within #RestController class and it was working .
GreetingController.java
package com.example.tryvalue.controller;
import com.example.tryvalue.EnvironmentValue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class GreetingController {
#Value("${greeting.salutation}")
private String greetingSalutation;
#Autowired
private EnvironmentValue environmentValue;
#GetMapping
public String greeting(){
return greetingSalutation + " world";
}
#GetMapping("/category1")
public String getCategory1(){
return environmentValue.getCategory1();
}
}
EnvironmentValue.java
package com.example.tryvalue;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
#Component
public class EnvironmentValue {
#Value("${category1.subcategory2}")
private String category1;
#Value("${category1.subcategory3}")
private String category2;
#Value("basic value")
private String category3;
public EnvironmentValue(){
System.out.println("Creating instance of EnvironmentValue");
}
public String getCategory1(){
return category1;
}
public String getCategory2(){
return category2;
}
public String getCategory3(){
return category3;
}
}
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>tryValue</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>tryValue</name>
<description>tryValue</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
What can be the reason behind this difference in behaviour ?
Thanks .
You are creating a second unmanaged instance of TryValueApplication in your main method so the properties won't be injected into that.
The managed instance of TryValueApplication should have the values injected. Add this to TryValueApplication to print the values
#Value("${category1.subcategory2}")
private String category1;
#Value("${category1.subcategory3}")
private String category3;
public static void main(String[] args) {
SpringApplication.run(TryValueApplication.class, args);
}
#PostConstruct
public void printCategory(){
System.out.println("Print environment values");
System.out.println(category1);
System.out.println(category3);
}

created repository method is returning wrong output like packagename.classname.table (com.blogconduitapi.entity.Users#6a3258a4)

I am new lerner of spring boot, i am creating a project after inserting user data into database when i am fetching data from table using method "User findByEmail(String email) it should return user object but it returning something like "com.blogconduitapi.entity.Users#5e2f219c".
All the codes are mentioned below
this is entity class
package com.blogconduitapi.entity;
import javax.persistence.*;
#Entity
#Table(name = "users")
public class Users {
#Id
#GeneratedValue
private int id;
private String name;
private String email;
private String password;
private boolean isEnabled;
public Users() {
}
public Users(String name, String email, String password, boolean isEnabled) {
this.name = name;
this.email = email;
this.password = password;
this.isEnabled = isEnabled;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public boolean isEnabled() {
return isEnabled;
}
public void setEnabled(boolean enabled) {
isEnabled = enabled;
}
}
this is controller
package com.blogconduitapi.controller;
import com.blogconduitapi.entity.Users;
import com.blogconduitapi.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class UserController {
#Autowired
UserService userService;
#PostMapping(value = "/register")
public String registerUser(#RequestBody Users users) {
Users existingUsers1 = userService.findUser(users.getEmail());
users.setEnabled(true);
userService.createUser(users);
return "user save successfully";
}
}
this is UserService
package com.blogconduitapi.service;
import com.blogconduitapi.entity.Users;
public interface UserService {
Users findUser(String email);
void createUser(Users users);
}
this is repository
package com.blogconduitapi.repository;
import com.blogconduitapi.entity.Users;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
#Repository
public interface UserRepository extends JpaRepository<Users, Integer> {
Users findByEmail(String email);
}
5.this is serviceImpl
package com.blogconduitapi.service.impl;
import com.blogconduitapi.entity.Users;
import com.blogconduitapi.exception.UserDefinedException;
import com.blogconduitapi.repository.UserRepository;
import com.blogconduitapi.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
#Service
public class UserServiceImpl implements UserService {
#Autowired
UserRepository userRepository;
#Override
public Users findUser(String email) {
Users users = userRepository.findByEmail(email);
System.out.println("{{{{{{{{{}}}}}}}}}}} " + users);
if (users != null) throw new UserDefinedException("mail already exist");
return users;
}
#Override
public void createUser(Users users) {
if (users.getName().isEmpty() || users.getEmail().isEmpty() || users.getPassword().isEmpty()) {
throw new UserDefinedException("There are some required fields are missing");
}
userRepository.save(users);
}
}
application.property
spring.datasource.url= jdbc:postgresql://localhost:5432/apidb
spring.datasource.username=postgres
spring.datasource.password=admin
spring.jpa.hibernate.ddl-auto=update
7.this is 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>blogconduitapi</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>blogconduitapi</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-jdbc -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
The method is working it's just the issue with print. Override toString() method in your User class and try printing the user again. Java has no idea how to convert your User to string and print it in format you would like otherwise.

Unable to read configurationProperties

I am trying to use ConfiguartionProperties to read properties from application.prop files. But I am getting NullPointerException because at the time of initiating the bean the properties are not read properly from application.prop file.
#Configuration
#ConfigurationProperties(prefix = "httpool")
public class ClientHttpPoolConfig {
private Integer maxPerRoute;
private Integer maxTotal;
private Integer connectionRequestTimeout;
private Integer connectTimeout;
private Integer socketTimeout;
#Bean
public PoolingHttpClientConnectionManager poolingHttpClntConnMger() {
PoolingHttpClientConnectionManager result = new PoolingHttpClientConnectionManager();
result.setDefaultMaxPerRoute(maxPerRoute); // maxPerRoute is null.
result.setMaxTotal(maxTotal);
return result;
}
}
I simplified as much as I could, so I'm using very simple bean:
StringWrapper
package betlista.springTests.beanInConfiguration;
/** Represents a very simple Bean */
public class StringWrapper {
private String name;
public StringWrapper(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
There is nothing special in configuration class too...
Config
package betlista.springTests.beanInConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
#Component
#ConfigurationProperties
public class Config {
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#Bean
public StringWrapper getStringWrapper() {
return new StringWrapper(name);
}
}
and finally application
package betlista.springTests.beanInConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class SpringBootConsoleApplication implements CommandLineRunner {
private static Logger LOG = LoggerFactory.getLogger(SpringBootConsoleApplication.class);
#Autowired
StringWrapper stringWrapper;
public static void main(String[] args) {
SpringApplication.run(SpringBootConsoleApplication.class, args);
}
#Override
public void run(String... args) {
LOG.info("stringWrapper.name: {}", stringWrapper.getName());
}
}
Just for completeness
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>betlista</groupId>
<artifactId>springTests-beanInConfiguration</artifactId>
<version>1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.2.5.RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Prefix
Pretty much same works with #ConfigurationProperties("my") and my.name=Betlista in property file.
All available in GitHub: https://github.com/Betlista/SpringTests/tree/master/BeanInConfiguration
in your application.properties files, parameters should be as follow:
httpool.max-per-route=//here your Integer value
httpool.max-total=//here your Integer value
httpool.connection-requestTimeout=//here your Integer value
httpool.connect-timeout=//here your Integer value
httpool.socket-timeout=//here your Integer value
EDITED:
and you must write this annotation to your Main class
#EnableConfigurationProperties({
ClientHttpPoolConfig.class
})

#RequestMapping not redirecting to the specified URL : Spring Boot application

I am trying to create a sample spring boot application which can connect to postgres. But I am not able to redirect to the path specified in the rest controller. The code for my project is given below:
pom.xml
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.vmware.springboot</groupId>
<artifactId>SpringBootSample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>SpringBootDemo1</name>
<description>Sample project for Spring Eureka</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
server.port=3000
spring.datasource.url= jdbc:postgresql://localhost:5432/local_db
spring.datasource.data-username=postgres
spring.datasource.data-password=postgres
spring.jpa.hibernate.ddl-auto=create-drop
SpringBootExampleApplication.java
package org.kumar.springboot;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.kumar.spring.enitiy.Employee;
#SpringBootApplication
#ComponentScan
public class SpringBootExampleApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootExampleApplication.class, args);
}
}
EmployeeController.java
package org.kumar.spring.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.kumar.spring.enitiy.Employee;
import org.kumar.spring.repository.EmployeeRepository;
#RestController
#RequestMapping("/data")
public class EmployeeController {
#Autowired
private EmployeeRepository employeeRepository;
#RequestMapping("/employees")
public List<Employee> getEmployees(){
return (List<Employee>)employeeRepository.findAll();
}
}
Employee.java
package org.kumar.spring.enitiy;
public class Employee {
private String employeeId;
private String employeeName;
public String getEmployeeId() {
return employeeId;
}
public void setEmployeeId(String employeeId) {
this.employeeId = employeeId;
}
public String getEmployeeName() {
return employeeName;
}
public void setEmployeeName(String employeeName) {
this.employeeName = employeeName;
}
#Override
public String toString() {
return "Employee [employeeId=" + employeeId + ", employeeName=" + employeeName + "]";
}
public Employee(String employeeId, String employeeName) {
super();
this.employeeId = employeeId;
this.employeeName = employeeName;
}
}
EmployeeRepository.java
package org.kumar.spring.repository;
import org.springframework.data.repository.CrudRepository;
import org.kumar.spring.enitiy.Employee;
public interface EmployeeRepository extends CrudRepository<Employee, String>
{
}
I am trying to run the following:
Request : GET http://localhost:3000/data/employees
Response :
{
"timestamp": 1492152367659,
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/data/employees"
}
What mistake am I committing.
Basically, your EmployeeController class has not been detected/scanned by the Spring container because of the classes are in different package hierarchy and the container has not been instructed to scan in which package to look for.
So there are two options to solve the issue:
(1) Change your SpringBootExampleApplication class package to org.kumar.spring
(2) Add #ComponentScan(basePackages = {"org.kumar.spring"})
I suggest you prefer the option(1) above otherwise, for option(2), you need to add #EnableJpaRepositories to make the program to work from end to end (i.e., connecting to the database using your EmployeeRepository class).

Spring Boot + Spring Data JPA + Transactions not working properly

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);
}
}
Change Transactional annotation to
#Transactional(rollbackFor=Exception.class)
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.
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

Resources