Spring Data JPA @Modifying DELETE查询失效:旧令牌仍留存数据库
在Spring Boot应用中,创建新邮箱验证令牌前尝试删除旧令牌,直接在数据库执行DELETE SQL可成功删除,但通过带@Modifying注解的Spring Data JPA仓库方法调用时,旧令牌未被删除仍留存数据库。
环境
- Spring Boot 3.x
- Spring Data JPA
- MySQL数据库
- Java 17+
实体类
用户实体
@Entity @Table(name = "user_details") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "email_id", unique = true, nullable = false) private String emailId; @Column(name = "email_verified", nullable = false) private boolean emailVerified = false; // 其他字段、构造方法、getter/setter... }
邮箱验证令牌实体
@Entity @Table(name = "email_verification_token") public class EmailVerificationToken { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "token", nullable = false, unique = true) private String token; @Column(name = "expiryDate", nullable = false) private Instant expiryDate; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", referencedColumnName = "id", nullable = false) private User user; // 构造方法、getter/setter... }
仓库接口
@Repository public interface EmailVerificationTokenRepository extends JpaRepository<EmailVerificationToken, Long> { Optional<EmailVerificationToken> findByToken(String token); List<EmailVerificationToken> findByUser(User user); @Modifying @Transactional @Query(value = "DELETE FROM email_verification_token WHERE user_id = :userId", nativeQuery = true) void deleteByUserId(@Param("userId") Long userId); }
服务方法
@Service public class UserService { @Transactional public void resendVerificationEmail(String emailId) { User user = userRepository.findByEmailId(emailId) .orElseThrow(() -> new IllegalArgumentException("User not found")); if (user.isEmailVerified()) { throw new IllegalArgumentException("Email is already verified"); } // 检查现有令牌 List<EmailVerificationToken> existingTokens = emailVerificationTokenRepository.findByUser(user); logger.info("Found {} existing tokens for user {}", existingTokens.size(), emailId); if (!existingTokens.isEmpty()) { // 该删除操作未生效 emailVerificationTokenRepository.deleteByUserId(user.getId()); logger.info("Attempted to delete tokens for user ID: {}", user.getId()); // 删除后令牌仍存在 List<EmailVerificationToken> remainingTokens = emailVerificationTokenRepository.findByUser(user); logger.info("After deletion, {} tokens remain", remainingTokens.size()); // 仍显示旧数量 } // 创建新令牌 String token = UUID.randomUUID().toString(); Instant expiry = Instant.now().plusSeconds(1800); EmailVerificationToken verificationToken = new EmailVerificationToken(token, expiry, user); emailVerificationTokenRepository.save(verificationToken); // 发送邮件... } }
问题现象
✅ 有效:直接在数据库执行SQL
DELETE FROM email_verification_token WHERE user_id = 5; -- 成功删除令牌
❌ 无效:Spring Data JPA仓库方法deleteByUserId()方法执行无错误,日志显示“Attempted to delete tokens for user ID: X”,但令牌仍留存数据库,后续查询仍能找到旧令牌。
完整流程:
- 用户使用已存在的未验证邮箱注册(POST /auth/register)
- 后端返回400 Bad Request,提示"This email is registered but not verified."
- 后端日志:
Found 1 existing tokens for user test@example.com Attempted to delete tokens for user ID: 5 After deletion, 1 tokens remain // ← 预期为0
已尝试方案
- 为仓库方法添加@Transactional和@Modifying注解
- 使用原生SQL查询而非JPQL
- 循环调用
emailVerificationTokenRepository.delete(token)手动删除 - 删除操作后添加flush()
- 验证SQL查询直接执行有效
- 尝试不同的事务传播设置
排查思路与解决方案
核心原因:JPA一级缓存(持久化上下文)未更新
在调用deleteByUserId()之前,你先执行了findByUser(user),查询到的EmailVerificationToken对象被存入JPA的一级缓存中。而原生SQL DELETE操作绕过了JPA的持久化上下文,数据库中的数据确实被删除了,但缓存中的对象依然存在。后续调用findByUser(user)时,JPA直接从缓存中返回旧对象,导致你误以为数据未被删除。
解决方案
清除持久化上下文
在删除操作后,注入EntityManager并调用clear()方法,清空一级缓存:@Autowired private EntityManager entityManager; // 删除操作后执行 entityManager.clear();之后再查询时,JPA会从数据库重新获取数据。
改用JPQL删除而非原生SQL
让JPA管理缓存同步,修改仓库方法的查询为JPQL:@Modifying @Transactional @Query("DELETE FROM EmailVerificationToken t WHERE t.user.id = :userId") void deleteByUserId(@Param("userId") Long userId);这种方式下,JPA会自动更新持久化上下文,缓存中的对象会被标记为删除,后续查询能得到正确结果。
删除后用userId而非user对象查询
绕过一级缓存,改用userId查询剩余令牌:List<EmailVerificationToken> remainingTokens = emailVerificationTokenRepository.findByUserId(user.getId()); // 需要在仓库中添加该方法:List<EmailVerificationToken> findByUserId(Long userId);这种方式会强制JPA从数据库查询最新数据。
手动刷新缓存
在删除后调用emailVerificationTokenRepository.flush(),然后清除缓存:emailVerificationTokenRepository.deleteByUserId(user.getId()); emailVerificationTokenRepository.flush(); entityManager.clear();
内容的提问来源于stack exchange,提问作者Ankit Arsh




