You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

Spring Data JPA @Modifying DELETE查询失效:旧令牌仍留存数据库

问题:Spring Data JPA @Modifying DELETE查询未生效,直接执行SQL正常

在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”,但令牌仍留存数据库,后续查询仍能找到旧令牌。

完整流程:

  1. 用户使用已存在的未验证邮箱注册(POST /auth/register)
  2. 后端返回400 Bad Request,提示"This email is registered but not verified."
  3. 后端日志:
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直接从缓存中返回旧对象,导致你误以为数据未被删除。

解决方案

  1. 清除持久化上下文
    在删除操作后,注入EntityManager并调用clear()方法,清空一级缓存:

    @Autowired
    private EntityManager entityManager;
    
    // 删除操作后执行
    entityManager.clear();
    

    之后再查询时,JPA会从数据库重新获取数据。

  2. 改用JPQL删除而非原生SQL
    让JPA管理缓存同步,修改仓库方法的查询为JPQL:

    @Modifying
    @Transactional
    @Query("DELETE FROM EmailVerificationToken t WHERE t.user.id = :userId")
    void deleteByUserId(@Param("userId") Long userId);
    

    这种方式下,JPA会自动更新持久化上下文,缓存中的对象会被标记为删除,后续查询能得到正确结果。

  3. 删除后用userId而非user对象查询
    绕过一级缓存,改用userId查询剩余令牌:

    List<EmailVerificationToken> remainingTokens = emailVerificationTokenRepository.findByUserId(user.getId());
    // 需要在仓库中添加该方法:List<EmailVerificationToken> findByUserId(Long userId);
    

    这种方式会强制JPA从数据库查询最新数据。

  4. 手动刷新缓存
    在删除后调用emailVerificationTokenRepository.flush(),然后清除缓存:

    emailVerificationTokenRepository.deleteByUserId(user.getId());
    emailVerificationTokenRepository.flush();
    entityManager.clear();
    

内容的提问来源于stack exchange,提问作者Ankit Arsh

火山引擎 最新活动