测试CRUD Repository的save()方法时出现ObjectOptimisticLockingFailureException错误
执行自定义Repository的save()方法测试时抛出乐观锁异常:
org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [ru.clevertec.newsonline.entity.News#7289c37a-8bfa-4092-872b-f1e4a8c319c6]
其他方法测试正常,已尝试添加@Commit注解但无效。
相关代码
NewsRepository
@Repository public interface NewsRepository extends JpaRepository<News, UUID>, IRepository<News>, IFilterEntityRepository<News, NewsFilter> { }
IRepository接口
public interface IRepository<E> { List<E> findAll(); Page<E> findAll(Pageable pageable); Optional<E> findById(UUID id); E save(E e); void delete(E e); }
测试类
@DataJpaTest @ActiveProfiles("test") @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) class NewsRepositoryIT { @Test public void shouldSaveNews() { //given News newNews = NewsTestBuilder.builder().build().buildNewsForSave(); //when newsRepository.save(newNews); //then assertEquals(21, newsRepository.findAll().size()); assertTrue(((CrudRepository<News, UUID>) newsRepository).findById(newNews.getNewsId()).isPresent()); } }
News实体类
@ToString(exclude = {"pictures", "comments"}) @Data @NoArgsConstructor @AllArgsConstructor @Builder @Entity @Table(name = "news", schema = "news_online") public class News { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "news_id") private UUID newsId; @Column(name = "header_news") private String headerNews; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "author_id") private Author author; @Column(name = "date_of_news", updatable = false) @ColumnDefault(value = "CURRENT_TIMESTAMP") private LocalDateTime dateOfNews; @Column(name = "is_published") private boolean isPublished; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "category_id") private Category category; @Column(name = "short_description", length = 500) private String shortDescription; @Column(name = "content_link") private String contentLink; @ManyToMany(mappedBy = "news", cascade = CascadeType.ALL) @Fetch(value = FetchMode.SUBSELECT) private List<Picture> pictures; @OneToMany(mappedBy = "news", cascade = CascadeType.ALL, orphanRemoval = true) @Fetch(value = FetchMode.SUBSELECT) private List<Comment> comments; public void addPicture(Picture picture) { List<News> news = picture.getNews(); news.add(this); picture.setNews(news); pictures.add(picture); } public void addComment(Comment comment) { comment.setNews(this); comments.add(comment); } }
NewsTestBuilder工具类
public News buildNewsForSave() { return News.builder().newsId(UUID.fromString("7289c37a-8bfa-4092-872b-f1e4a8c319c6")).headerNews("Breaking news") .isPublished(true).category(Category.builder().categoryId(UUID.fromString("3de78717-14f5-491e-a375-e122ec4dcbe2")).section(Section.CAR).build()) .shortDescription("Text").contentLink("static/text/news100.txt").build(); }
手动指定ID触发更新操作:
News实体的ID配置了@GeneratedValue(strategy = GenerationType.AUTO),本应由JPA自动生成ID,但测试中通过构建器手动设置了固定ID。JPA会将带有已存在ID的实体判定为已持久化对象,执行update而非insert。如果测试数据库中该ID的记录已存在,或测试过程中该记录被修改/删除,就会触发乐观锁异常。关联实体状态异常:测试中构建的
Category仅设置了ID,未被持久化上下文管理。执行save时JPA会处理关联的Category,若该Category不存在于数据库,会引发级联操作异常;若存在,也可能因关联实体状态不一致间接导致当前News的更新操作失败。测试数据残留:即使添加了
@Commit,若之前的测试已将该ID的记录提交到数据库,本次测试再次尝试更新时,数据库中记录的状态(如版本号)与实体状态不一致,也会抛出异常。
- 移除手动设置的ID:修改构建方法,让JPA自动生成ID:
public News buildNewsForSave() { return News.builder() .headerNews("Breaking news") .isPublished(true) .category(Category.builder().categoryId(UUID.fromString("3de78717-14f5-491e-a375-e122ec4dcbe2")).section(Section.CAR).build()) .shortDescription("Text") .contentLink("static/text/news100.txt") .build(); }
确保关联实体存在:测试前先通过
categoryRepository.save()将构建的Category存入数据库,或使用@Sql注解提前插入测试数据,避免关联实体不存在引发的异常。清理测试数据:利用
@DataJpaTest默认的@Rollback特性,确保每次测试后数据回滚;或在@BeforeEach方法中清理News表中对应ID的记录,避免残留数据干扰。检查乐观锁配置:若实体使用了
@Version字段,确保测试时不手动指定版本号,让JPA自动维护版本信息。
内容的提问来源于stack exchange,提问作者JavaJan987

