Why do I get Detached Entity exception when upgrading Spring Boot 1.1.4 to 1.1.5

Posted by mmeany on Stack Overflow See other posts from Stack Overflow or by mmeany
Published on 2014-08-20T07:01:46Z Indexed on 2014/08/20 22:20 UTC
Read the original article Hit count: 373

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

© Stack Overflow or respective owner

Related posts about spring

Related posts about jpa