You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 20:18:20