Spring Data JPA乐观锁测试:如何强制从数据库加载实体?
Spring Data JPA乐观锁测试问题及解决方法
问题描述
我尝试在Spring Data JPA中测试乐观锁机制:两次调用查询方法加载同一实体,更新第一个实体后,断言第二个实体更新时会抛出OptimisticLockingFailureException。但实际未抛出异常,第二次更新成功。排查发现,查询方法仅首次查询数据库并缓存实体,再次调用时返回缓存实例,导致两个加载的实体为同一对象,第一个实体的更新会同步到第二个实体,使其不存在过期数据。需要强制第二次调用查询方法时从数据库加载实体。
相关代码
测试类
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; @DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) class PersistenceTests { @Autowired private ProductRepository repository; private ProductEntity savedEntity; @DynamicPropertySource static void databaseProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", () -> "jdbc:mysql://localhost:3306/code_snippet"); registry.add("spring.datasource.username", () -> "root"); registry.add("spring.datasource.password", () -> "System"); registry.add("spring.jpa.hibernate.ddl-auto", () -> "create-drop"); } @BeforeEach void setupDb() { repository.deleteAll(); ProductEntity entity = new ProductEntity(1, "n", 1); savedEntity = repository.save(entity); assertEqualsProduct(entity, savedEntity); } @Test void optimisticLockError() { // 尝试获取两个独立的实体对象 ProductEntity entity1 = repository.findById(savedEntity.getId()).get(); ProductEntity entity2 = repository.findById(savedEntity.getId()).get(); // 使用第一个实体更新数据 entity1.setName("n1"); repository.save(entity1); // 尝试使用第二个实体更新,预期抛出乐观锁异常 assertThrows(OptimisticLockingFailureException.class, () -> { entity2.setName("n2"); repository.save(entity2); }); // 从数据库获取最新实体并验证状态 ProductEntity updatedEntity = repository.findById(savedEntity.getId()).get(); assertEquals(1, (int) updatedEntity.getVersion()); assertEquals("n1", updatedEntity.getName()); } private void assertEqualsProduct(ProductEntity expectedEntity, ProductEntity actualEntity) { assertEquals(expectedEntity.getId(), actualEntity.getId()); assertEquals(expectedEntity.getVersion(), actualEntity.getVersion()); assertEquals(expectedEntity.getProductId(), actualEntity.getProductId()); assertEquals(expectedEntity.getName(), actualEntity.getName()); assertEquals(expectedEntity.getWeight(), actualEntity.getWeight()); } }
实体类
import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Table; import javax.persistence.Version; @Entity @Table(name = "product") public class ProductEntity { @Id @GeneratedValue private Integer id; @Version private Integer version; private int productId; private String name; private int weight; public ProductEntity() { } public ProductEntity(int productId, String name, int weight) { this.productId = productId; this.name = name; this.weight = weight; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public Integer getVersion() { return version; } public void setVersion(Integer version) { this.version = version; } public int getProductId() { return productId; } public void setProductId(int productId) { this.productId = productId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; } }
仓库接口
import java.util.Optional; import org.springframework.data.repository.PagingAndSortingRepository; public interface ProductRepository extends PagingAndSortingRepository<ProductEntity, Integer> { Optional<ProductEntity> findByProductId(int productId); }
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.2</version> <relativePath /> <!-- lookup parent from repository --> </parent> <groupId>com.javaworld.codesnippet</groupId> <artifactId>writing-persistence-tests</artifactId> <version>0.0.1-SNAPSHOT</version> <name>writing-persistence-tests</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-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</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>
主类
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class WritingPersistenceTestsApplication { public static void main(String[] args) { SpringApplication.run(WritingPersistenceTestsApplication.class, args); } }
解决方法
1. 清除持久化上下文缓存
在第二次查询前,注入EntityManager并调用clear()方法,清空当前持久化上下文的所有缓存实体,这样第二次查询会强制从数据库加载新实例:
@Autowired private EntityManager entityManager; @Test void optimisticLockError() { ProductEntity entity1 = repository.findById(savedEntity.getId()).get(); // 清除上下文缓存 entityManager.clear(); ProductEntity entity2 = repository.findById(savedEntity.getId()).get(); // 后续更新逻辑不变 }
2. 强制刷新指定实体
如果需要保留上下文其他实体,可在获取entity2后调用EntityManager.refresh()方法,强制从数据库同步最新状态:
@Autowired private EntityManager entityManager; @Test void optimisticLockError() { ProductEntity entity1 = repository.findById(savedEntity.getId()).get(); ProductEntity entity2 = repository.findById(savedEntity.getId()).get(); // 刷新entity2,同步数据库最新数据 entityManager.refresh(entity2); // 后续更新逻辑不变 }
3. 使用自定义查询绕过缓存
在仓库接口中添加自定义查询方法,显式指定查询语句,避免Spring Data JPA的一级缓存优化:
public interface ProductRepository extends PagingAndSortingRepository<ProductEntity, Integer> { @Query("SELECT p FROM ProductEntity p WHERE p.id = ?1") Optional<ProductEntity> findByIdFromDb(Integer id); Optional<ProductEntity> findByProductId(int productId); }
测试时调用findByIdFromDb方法获取实体:
ProductEntity entity1 = repository.findByIdFromDb(savedEntity.getId()).get(); ProductEntity entity2 = repository.findByIdFromDb(savedEntity.getId()).get();
4. 拆分事务执行查询
@DataJpaTest默认每个测试方法处于同一个事务中,共享持久化上下文。可以使用TransactionTemplate让两次查询在独立事务中执行,这样每次查询都会重新加载实体:
@Autowired private TransactionTemplate transactionTemplate; @Test void optimisticLockError() { ProductEntity entity1 = transactionTemplate.execute(status -> repository.findById(savedEntity.getId()).get() ); ProductEntity entity2 = transactionTemplate.execute(status -> repository.findById(savedEntity.getId()).get() ); // 后续更新逻辑不变 }
内容的提问来源于stack exchange,提问作者Abdelrahman_Attya
相关产品推荐
相关产品推荐

