在将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
答案 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>
将此添加到测试用例可修复所有问题,所有测试都按预期传递。