Spring Boot+JPA关联实体时抛出EntityNotFoundException问题排查
我正在开发一个处理足球相关处罚(如训练迟到)的Spring Boot应用,前端采用Vaadin,数据库用H2,持久层基于Spring Data JPA。单独插入Player和Punishment实体无异常,但通过PunishmentToPlayer表关联两者ID时,抛出javax.persistence.EntityNotFoundException,且数据库中Player和Punishment的实体数据是正确的。
相关实体类代码
Player实体
@Setter @Getter @Entity @AllArgsConstructor @NoArgsConstructor @Builder public class Player extends AbstractEntity{ private String firstName; private String lastName; }
Punishment实体
@Setter @Getter @Entity @AllArgsConstructor @NoArgsConstructor @Builder public class Punishment extends AbstractEntity{ private String description; private Double amount; }
PunishmentToPlayer实体
@Setter @Getter @Entity @AllArgsConstructor @NoArgsConstructor @Builder public class PunishmentToPlayer extends AbstractEntity{ @OneToOne private Player player; @OneToOne private Punishment punishment; }
数据初始化代码
@Component public class DataHelper { public void addPlayer(PlayerService playerService){ playerService.saveOrUpdate(buildPlayer("Player1_Name", "Player1_Lastname")); playerService.saveOrUpdate(buildPlayer("Player2_Name", "Player2_Lastname")); playerService.saveOrUpdate(buildPlayer("Player3_Name", "Player3_Lastname")); playerService.saveOrUpdate(buildPlayer("Player4_Name", "Player4_Lastname")); } public void addPunishments(PunishmentService punishmentService){ punishmentService.saveOrUpdate(buildPunishment("Description 1", 20.00)); punishmentService.saveOrUpdate(buildPunishment("Description 2", 15.00)); punishmentService.saveOrUpdate(buildPunishment("Description 3", 10.00)); punishmentService.saveOrUpdate(buildPunishment("Description 4", 5.00)); } public void addConnections(PunishmentToPlayerService punishmentToPlayerService, PlayerService playerService, PunishmentService punishmentService){ List<Player> players = playerService.getAllPlayer(); List<Punishment> punishments = punishmentService.getAllPunishments(); punishmentToPlayerService.saveOrUpdate(buildPunishmentToPlayer(players.get(0), punishments.get(0))); punishmentToPlayerService.saveOrUpdate(buildPunishmentToPlayer(players.get(0), punishments.get(2))); punishmentToPlayerService.saveOrUpdate(buildPunishmentToPlayer(players.get(1), punishments.get(1))); punishmentToPlayerService.saveOrUpdate(buildPunishmentToPlayer(players.get(2), punishments.get(0))); punishmentToPlayerService.saveOrUpdate(buildPunishmentToPlayer(players.get(2), punishments.get(3))); punishmentToPlayerService.saveOrUpdate(buildPunishmentToPlayer(players.get(3), punishments.get(0))); punishmentToPlayerService.saveOrUpdate(buildPunishmentToPlayer(players.get(3), punishments.get(3))); punishmentToPlayerService.saveOrUpdate(buildPunishmentToPlayer(players.get(3), punishments.get(3))); } private Player buildPlayer(String firstname, String lastname){ return Player.builder() .firstName(firstname) .lastName(lastname) .build(); } private Punishment buildPunishment(String description, double value){ return Punishment.builder() .description(description) .amount(value) .build(); } private PunishmentToPlayer buildPunishmentToPlayer(Player player, Punishment punishment){ return PunishmentToPlayer.builder() .player(player) .punishment(punishment) .build(); } }
PunishmentToPlayerService代码
public void saveOrUpdate(PunishmentToPlayer punishmentToPlayer){ punishmentToPlayerRepository.save(punishmentToPlayer); }
异常堆栈信息
Caused by: org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find de.nils.Tus_Strafen.model.Player with id a62dde9d-2514-44d8-9d97-7d54afd69ef8; nested exception is javax.persistence.EntityNotFoundException: Unable to find de.nils.Tus_Strafen.model.Player with id a62dde9d-2514-44d8-9d97-7d54afd69ef8 at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:379) ~[spring-orm-5.3.22.jar:5.3.22] at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:235) ~[spring-orm-5.3.22.jar:5.3.22] at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:551) ~[spring-orm-5.3.22.jar:5.3.22] at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61) ~[spring-tx-5.3.22.jar:5.3.22] at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242) ~[spring-tx-5.3.22.jar:5.3.22] at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:152) ~[spring-tx-5.3.22.jar:5.3.22] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.22.jar:5.3.22] at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:174) ~[spring-data-jpa-2.7.2.jar:2.7.2] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.22.jar:5.3.22] at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) ~[spring-aop-5.3.22.jar:5.3.22] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.22.jar:5.3.22] at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215) ~[spring-aop-5.3.22.jar:5.3.22] at com.sun.proxy.$Proxy123.save(Unknown Source) ~[na:na] at de.nils.Tus_Strafen.service.PunishmentToPlayerService.saveOrUpdate(PunishmentToPlayerService.java:48) ~[classes/:na] at de.nils.Tus_Strafen.DataHelper.addConnections(DataHelper.java:35) ~[classes/:na] at de.nils.Tus_Strafen.view.views.MainAppLayout.addData(MainAppLayout.java:81) ~[classes/:na] at de.nils.Tus_Strafen.view.views.MainAppLayout.<init>(MainAppLayout.java:41) ~[classes/:na] at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:na] at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:64) ~[na:na] at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:na] at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:500) ~[na:na] at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481) ~[na:na] at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:211) ~[spring-beans-5.3.22.jar:5.3.22]
原因分析及解决办法
1. 事务边界问题
添加Player、Punishment和关联数据的操作可能不在同一个事务中,导致关联时Player/Punishment的事务未提交,JPA无法找到对应实体。
解决:将addPlayer、addPunishments、addConnections的调用放在同一个带@Transactional注解的方法中,确保所有操作在同一事务内完成,保证数据持久化的一致性。
2. 实体状态问题
从getAllPlayer()和getAllPunishments()获取的实体可能处于游离状态(detached),JPA无法识别其为已持久化实体。
解决:关联前通过ID重新查询实体,将其转换为持久化状态:
Player managedPlayer = playerService.getPlayerById(players.get(0).getId()); Punishment managedPunishment = punishmentService.getPunishmentById(punishments.get(0).getId()); punishmentToPlayerService.saveOrUpdate(buildPunishmentToPlayer(managedPlayer, managedPunishment));
3. 关联注解逻辑错误
当前PunishmentToPlayer使用@OneToOne注解,但实际业务中一个球员可能有多个处罚、一个处罚可能对应多个球员,不符合业务逻辑,会导致JPA关联校验失败。
解决:修改为@ManyToOne注解:
@Setter @Getter @Entity @AllArgsConstructor @NoArgsConstructor @Builder public class PunishmentToPlayer extends AbstractEntity{ @ManyToOne private Player player; @ManyToOne private Punishment punishment; }
4. ID生成策略问题
检查AbstractEntity的ID生成逻辑,如果是手动生成ID但未正确持久化,可能导致查询到的实体ID与数据库实际ID不一致。
解决:确保AbstractEntity使用JPA标准ID生成策略,例如:
@Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id;
内容的提问来源于stack exchange,提问作者Nils Teipel

