将Spring Boot 1.1.4升级到1.1.5时,为什么会出现Detached Entity异常

时间:2014-08-20 07:01:47

标签: spring jpa spring-data spring-data-jpa spring-boot

在将Spring Boot从1.1.4更新到1.1.5时,一个简单的Web应用程序开始生成分离的实体异常。具体来说,导致访问次数增加的后验证感知器会引起问题。

快速检查加载的依赖项显示Spring Data已从1.6.1更新到1.6.2并且对更改日志的进一步检查显示了与乐观锁定,版本字段和JPA问题相关的一些问题固定的。

好吧,我正在使用版本字段,并且在建议未按规范设置的情况下以Null开头。

我已经制作了一个非常简单的测试场景,如果版本字段从null或零开始,我会得到分离的实体异常。如果我使用版本1创建实体,那么我不会得到这些例外。

这是预期的行为还是还有什么不妥之处?

以下是我对此情况的测试方案。在场景中已注释@Transactional的服务层。每个测试用例都会对服务层进行多次调用 - 测试正在使用分离的实体,因为这是我在完整的应用程序中使用的场景。

测试用例包括四个测试:

测试1 - versionNullCausesAnExceptionOnUpdate()

在此测试中,分离对象中的版本字段为Null。这是我通常在传递给服务之前创建对象的方式。

此测试因Detached Entity异常而失败。

我原本预计这个测试会通过。如果测试中存在缺陷,则情景的其余部分可能没有实际意义。

测试2 - versionZeroCausesExceptionOnUpdate()

在此测试中,我将版本设置为值Long(0L)。这是一个边缘案例测试,因为我在Spring Data更改日志中找到了用于版本字段的Zero值的引用。

此测试因Detached Entity异常而失败。

感兴趣的仅仅是因为以下两个测试通过了将其视为异常。

测试3 - versionOneDoesNotCausesExceptionOnUpdate()

在此测试中,版本字段设置为值Long(1L)。不是我通常会做的事情,但考虑到Spring Data更改日志中的注释,我决定试一试。

此测试通过。

通常不会设置版本字段,但这看起来像是解决方法,直到我找出第一个测试失败的原因。

测试4 - versionOneDoesNotCausesExceptionWithMultipleUpdates()

在测试3的结果的鼓励下,我进一步推动了场景,并对使用Long(1L)版本开始生命的实体执行了多次更新。

此测试通过。

强化这可能是一种可用的解决方法。

实体:

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;
    }
}

存储库:

package com.mvmlabs.dao;

import org.springframework.data.repository.CrudRepository;

import com.mvmlabs.domain.User;

public interface UserDao extends CrudRepository<User, Long>{

}

服务界面:

package com.mvmlabs.service;

import com.mvmlabs.domain.User;

public interface UserService {
    User save(User user);
    User loadUser(Long id);
    User registerVisit(User user);
}

服务实施:

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);
    }
}

申请类:

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);
    }
}

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>

应用程序属性:

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

测试用例:

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());
    }
}

前两个测试因分离实体异常而失败。最后两个测试按预期通过。

现在将Spring Boot版本更改为1.1.4并重新运行,所有测试都通过。

我的期望是错的吗?

编辑:此代码已保存到GitHub https://github.com/mmeany/spring-boot-detached-entity-issue

1 个答案:

答案 0 :(得分:1)

版本1.6.2中的spring-data-jpa存在问题,这已在spring-data-jpa 1.6.4-RELEASE中得到解决。一旦Spring Boot更新引入新版本的spring数据JPA,这将成为一个非问题,直到覆盖POM中spring-data-jpa的版本。

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>1.6.4.RELEASE</version>
</dependency>

将此添加到测试用例可修复所有问题,所有测试都按预期传递。