最新版Spring Data JPA @Modifying与@Transactional使用疑问
关于@Modifying注解的最新版本使用说明
截止目前Spring Data JPA最新稳定版(3.2.x系列),自定义JPQL/原生SQL的增删改操作依然必须加@Modifying注解,没有任何版本移除过这个要求。
这个注解的核心作用是告诉框架:当前@Query标注的方法不是查询操作,底层不要用executeQuery()跑SQL,要切换成executeUpdate()执行修改逻辑。如果你不加,执行这类修改语句时会直接抛InvalidDataAccessApiUsageException,没有例外。
只有一种情况不需要手动加@Modifying:直接调用JpaRepository自带的衍生方法,比如save()、deleteById()、saveAll()这类,框架内部已经做了封装,不需要额外处理。
正确使用规则:
- @Modifying必须加在Repository接口中对应@Query方法上,不能加在Service层
- 单独加@Modifying默认只会执行修改语句,不会自动flush持久化上下文的待提交数据,也不会清空一级缓存,方法执行完如果要查询同一条数据,大概率会拿到缓存里的旧值。生产环境推荐配置两个参数:
@Modifying(flushAutomatically = true, clearAutomatically = true),前者会在执行修改前把所有待持久化的数据先刷到库,后者执行完修改直接清空EntityManager一级缓存,避免脏读 - 必须搭配事务支持,不然执行会报错
Repository层正确写法示例:
public interface UserRepository extends JpaRepository<User, Long> { // 错误写法:不加@Modifying,执行update/delete类自定义查询直接抛异常 @Query("update User u set u.status = :status where u.id = :userId") void updateUserStatusWrong(Long userId, Integer status); // 基础可用写法 @Modifying @Query("update User u set u.status = :status where u.id = :userId") int updateUserStatusBasic(Long userId, Integer status); // 生产推荐写法:避免缓存不一致问题 @Modifying(flushAutomatically = true, clearAutomatically = true) @Query("update User u set u.status = :status where u.registerTime < :expireTime") int batchDisableExpiredUsers(LocalDateTime expireTime, Integer status); }
@Transactional注解的使用规则
@Transactional没有强制和@Modifying绑定的位置要求,但有明确的场景使用规范:
- JpaRepository自带的所有CRUD方法,默认已经在Repository层加了@Transactional,调用时不需要重复加
- 自定义的@Modifying修改/删除方法,如果上层Service方法没有加事务,就必须在Repository的方法上补@Transactional,不然框架拿不到数据库连接的事务上下文,执行会报错
- 凡是涉及多步数据库写操作的业务逻辑,一律把@Transactional加在Service层的public方法上,不要只加在Repository层——Repository层的事务只覆盖单条数据库操作,没法保证整个业务逻辑的原子性,比如先插用户再插权限,只在Repository的两个save方法上加事务,插权限失败的时候已经插进去的用户不会回滚,会产生脏数据
- 纯查询方法可以加
@Transactional(readOnly = true),会给框架提示跳过flush、关闭脏检查,查询性能有明显提升,还能避免方法里误触发写操作
Service层实际场景使用示例:
@Service public class UserService { private final UserRepository userRepository; private final UserPermissionRepository permissionRepository; // 构造注入省略 /** * 多步写操作场景:创建用户+初始化默认权限,必须加事务,任意一步失败全部回滚 */ @Transactional(rollbackFor = Exception.class) public Long createUserWithDefaultPermission(UserCreateDTO dto) { // 第一步:保存用户基础信息 User user = new User(); user.setUsername(dto.getUsername()); user.setStatus(1); User savedUser = userRepository.save(user); // 第二步:给新用户绑定默认访客权限 UserPermission permission = new UserPermission(); permission.setUserId(savedUser.getId()); permission.setPermissionCode("VISITOR"); permissionRepository.save(permission); // 中间任何一步抛异常,前面插入的用户数据也会回滚,不会出现脏数据 return savedUser.getId(); } /** * 批量封禁过期用户场景:调用Repository层带@Modifying的自定义方法,Service层加事务 */ @Transactional(rollbackFor = Exception.class) public int batchDisableExpiredUsers() { LocalDateTime expireTime = LocalDateTime.now().minusYears(1); return userRepository.batchDisableExpiredUsers(expireTime, 0); } /** * 纯查询场景:加只读事务提升性能 */ @Transactional(readOnly = true) public UserVO getUserDetail(Long userId) { // 关联查询用户信息、权限信息,只读事务不会触发flush,性能更好 return userRepository.findUserWithPermission(userId); } }
新手常踩的坑:
- 不要图省事在整个Repository接口类上加@Modifying和@Transactional,只有自定义update/delete方法需要,普通查询加了会导致每次查询都触发不必要的缓存刷新,性能损耗明显
- @Transactional不要加在private、protected方法上,Spring的AOP代理扫不到,加了也不生效
- 写操作的@Transactional建议手动指定
rollbackFor = Exception.class,默认Spring只对RuntimeException和Error回滚,遇到checked异常不会回滚,容易出脏数据
内容的提问来源于stack exchange,提问作者user19254373
相关产品推荐
相关产品推荐

