On updating Spring Boot from 1.1.4 to 1.1.5 a simple web application started generating detached entity exceptions. Specifically, a post authentication inteceptor that bumped number of visits was causing the problem.
A quick check of loaded dependencies showed that Spring Data has been updated from 1.6.1 to 1.6.2 and a further check of the change log shows a couple of issues relating to optimistic locking, version fields and JPA issues that have been fixed.
Well I am using a version field and it starts out as Null following recommendation to not set in the specification.
I have produced a very simple test scenario where I get detached entity exceptions if the version field starts as null or zero. If I create an entity with version 1 however then I do not get these exceptions.
Is this expected behaviour or is there still something amiss?
Below is the test scenario I have for this condition. In the scenario the service layer that has been annotated @Transactional. Each test case makes multiple calls to the service layer - the tests are working with detached entities as this is the scenario I am working with in the full blown application.
The test case comprises four tests:
Test 1 - versionNullCausesAnExceptionOnUpdate()
In this test the version field in the detached object is Null. This is how I would usually create the object prior to passing to the service.
This test fails with a Detached Entity exception.
I would have expected this test to pass. If there is a flaw in the test then the rest of the scenario is probably moot.
Test 2 - versionZeroCausesExceptionOnUpdate()
In this test I have set the version to value Long(0L). This is an edge case test and included because I found reference to Zero values being used for version field in the Spring Data change log.
This test fails with a Detached Entity exception.
Of interest simply because the following two tests pass leaving this as an anomaly.
Test 3 - versionOneDoesNotCausesExceptionOnUpdate()
In this test the version field is set to value Long(1L). Not something I would usually do, but considering the notes in the Spring Data change log I decided to give it a go.
This test passes.
Would not usually set the version field, but this looks like a work-around until I figure out why the first test is failing.
Test 4 - versionOneDoesNotCausesExceptionWithMultipleUpdates()
Encouraged by the result of test 3 I pushed the scenario a step further and perform multiple updates on the entity that started life with a version of Long(1L).
This test passes.
Reinforcement that this may be a useable work-around.
The entity:
package com.mvmlabs.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Version;
@Entity
@Table(name="user_details")
public class User {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
@Version
private Long version;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private Integer numberOfVisits;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
public Integer getNumberOfVisits() {
return numberOfVisits == null ? 0 : numberOfVisits;
}
public void setNumberOfVisits(Integer numberOfVisits) {
this.numberOfVisits = numberOfVisits;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
The repository:
package com.mvmlabs.dao;
import org.springframework.data.repository.CrudRepository;
import com.mvmlabs.domain.User;
public interface UserDao extends CrudRepository<User, Long>{
}
The service interface:
package com.mvmlabs.service;
import com.mvmlabs.domain.User;
public interface UserService {
User save(User user);
User loadUser(Long id);
User registerVisit(User user);
}
The service implementation:
package com.mvmlabs.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import com.mvmlabs.dao.UserDao;
import com.mvmlabs.domain.User;
@Service
@Transactional(propagation=Propagation.REQUIRED, readOnly=false)
public class UserServiceJpaImpl implements UserService {
@Autowired
private UserDao userDao;
@Transactional(readOnly=true)
@Override
public User loadUser(Long id) {
return userDao.findOne(id);
}
@Override
public User registerVisit(User user) {
user.setNumberOfVisits(user.getNumberOfVisits() + 1);
return userDao.save(user);
}
@Override
public User save(User user) {
return userDao.save(user);
}
}
The application class:
package com.mvmlabs;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
The 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mvmlabs</groupId>
<artifactId>jpa-issue</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-boot-jpa-issue</name>
<description>JPA Issue between spring boot 1.1.4 and 1.1.5</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.5.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>com.mvmlabs.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>
</project>
The application properties:
spring.jpa.hibernate.ddl-auto: create
spring.jpa.hibernate.naming_strategy: org.hibernate.cfg.ImprovedNamingStrategy
spring.jpa.database: HSQL
spring.jpa.show-sql: true
spring.datasource.url=jdbc:hsqldb:file:./target/testdb
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driverClassName=org.hsqldb.jdbcDriver
The test case:
package com.mvmlabs;
import org.junit.Assert;
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;
import com.mvmlabs.domain.User;
import com.mvmlabs.service.UserService;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
@Autowired
UserService userService;
@Test
public void versionNullCausesAnExceptionOnUpdate() throws Exception {
User user = new User();
user.setUsername("Version Null");
user.setNumberOfVisits(0);
user.setVersion(null);
user = userService.save(user);
user = userService.registerVisit(user);
Assert.assertEquals(new Integer(1), user.getNumberOfVisits());
Assert.assertEquals(new Long(1L), user.getVersion());
}
@Test
public void versionZeroCausesExceptionOnUpdate() throws Exception {
User user = new User();
user.setUsername("Version Zero");
user.setNumberOfVisits(0);
user.setVersion(0L);
user = userService.save(user);
user = userService.registerVisit(user);
Assert.assertEquals(new Integer(1), user.getNumberOfVisits());
Assert.assertEquals(new Long(1L), user.getVersion());
}
@Test
public void versionOneDoesNotCausesExceptionOnUpdate() throws Exception {
User user = new User();
user.setUsername("Version One");
user.setNumberOfVisits(0);
user.setVersion(1L);
user = userService.save(user);
user = userService.registerVisit(user);
Assert.assertEquals(new Integer(1), user.getNumberOfVisits());
Assert.assertEquals(new Long(2L), user.getVersion());
}
@Test
public void versionOneDoesNotCausesExceptionWithMultipleUpdates() throws Exception {
User user = new User();
user.setUsername("Version One Multiple");
user.setNumberOfVisits(0);
user.setVersion(1L);
user = userService.save(user);
user = userService.registerVisit(user);
user = userService.registerVisit(user);
user = userService.registerVisit(user);
Assert.assertEquals(new Integer(3), user.getNumberOfVisits());
Assert.assertEquals(new Long(4L), user.getVersion());
}
}
The first two tests fail with detached entity exception. The last two tests pass as expected.
Now change Spring Boot version to 1.1.4 and rerun, all tests pass.
Are my expectations wrong?
Edit: This code saved to GitHub at https://github.com/mmeany/spring-boot-detached-entity-issue