关键机制说明:
1.事务注解生效:
@Transactional(rollbackFor = Exception.class)
java
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean saveUser(UserDTO userDto) {
SysUser sysUser = new SysUser();
BeanUtils.copyProperties(userDto, sysUser);
sysUser.setDelFlag(CommonConstants.STATUS_NORMAL);
sysUser.setPassword(ENCODER.encode(userDto.getPassword()));
baseMapper.insert(sysUser);
List<SysUserRole> userRoleList = userDto.getRole()
.stream().map(roleId -> {
SysUserRole userRole = new SysUserRole();
userRole.setUserId(sysUser.getUserId());
userRole.setRoleId(roleId);
return userRole;
}).collect(Collectors.toList());
return sysUserRoleService.saveBatch(userRoleList);
}
- 该注解将整个方法纳入同一个数据库事务。
rollbackFor = Exception.class
指定了所有异常(包括RuntimeException
和受检异常)都会触发回滚。
2.事务回滚逻辑:
- 事务内任何操作抛出异常(如数据库约束冲突、网络中断等),整个事务会回滚到方法开始前的状态。
- 本方法包含两个数据库操作:
baseMapper.insert(sysUser)
:插入用户记录sysUserRoleService.saveBatch(userRoleList)
:批量插入用户角色关联记录
- 这两个操作共享同一个事务,任何一步失败都会回滚所有操作。
3.批量保存的异常行为:
- MyBatis-Plus 的
saveBatch()
在默认配置下,如果批量操作中任意一条数据失败 ,会抛出异常 (如DataAccessException
)。 - 此时事务管理器会捕获异常,并回滚整个事务。
需要注意的细节:
-
批量操作实际执行方式:
saveBatch()
默认可能通过 分批提交 SQL (如每批 1000 条)执行。但如果某条数据失败(如唯一键冲突),MyBatis-Plus 会抛出异常,触发回滚,已插入的本批次数据也会回滚。
-
自调用问题:
- 如果该方法被同一个类中的其他方法直接调用 (而非通过 Spring 代理对象),
@Transactional
会失效。确保通过 Spring 容器管理的方法调用。
- 如果该方法被同一个类中的其他方法直接调用 (而非通过 Spring 代理对象),
验证场景:
// 模拟批量保存时部分数据失败(如角色ID不存在)
List<SysUserRole> userRoleList = Arrays.asList(
new SysUserRole(sysUser.getUserId(), 1), // 有效数据
new SysUserRole(sysUser.getUserId(), 999) // 无效数据(触发外键冲突)
);
sysUserRoleService.saveBatch(userRoleList); // 失败抛出异常
- 结果 :
sysUser
的插入操作也会被回滚,数据库无新增用户和角色关联记录。
总结:
只要批量保存中有一条记录失败抛出异常,整个事务回滚,确保用户数据和关联角色数据的一致性。这是事务 ACID 特性(原子性)的直接体现。
如果将上面的mybatisplus提供的saveBatch批量增加改成自己用for循环单条增加数据,事务还起作用吗?
答案:
事务仍然会起作用,而且效果相同! 只要循环中的任意一次单条保存失败,整个事务都会回滚 (包括
baseMapper.insert(sysUser)
插入的用户记录和之前成功的角色记录)。
详细分析:
1. 修改后的代码示例(使用循环单条保存):
java
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean saveUser(UserDTO userDto) {
// 插入用户记录
SysUser sysUser = new SysUser();
BeanUtils.copyProperties(userDto, sysUser);
sysUser.setDelFlag(CommonConstants.STATUS_NORMAL);
sysUser.setPassword(ENCODER.encode(userDto.getPassword()));
baseMapper.insert(sysUser);
// 循环单条保存用户角色
List<SysUserRole> userRoleList = ...; // 同上
for (SysUserRole userRole : userRoleList) {
sysUserRoleService.save(userRole); // ✨ 改为循环单条保存
}
return true;
}
2. 事务为何依然有效?
关键因素 | 说明 |
---|---|
Spring 事务管理机制 | @Transactional 会为整个方法创建一个数据库连接级的事务上下文。 |
同一事务上下文 | 循环中的每次 sysUserRoleService.save() 操作与用户插入操作共享同一个事务。 |
异常传播 | 循环中任意一次保存失败抛出异常时,异常会传播到 @Transactional 注解层,触发全局回滚。 |
3. 执行流程(含错误场景):
-
正确流程 :
用户插入 → 角色1保存 → 角色2保存 → ... → 全部成功 → 事务提交
-
错误流程 (假设第3次保存失败):
用户插入 → 角色1保存 → 角色2保存 → ❌ 角色3保存失败 → 抛出异常 → 事务回滚
→ 已插入的用户和角色1、2记录均被撤销
重要注意事项:
1. 性能陷阱
⚠️ 将 saveBatch()
批量操作改为循环单条保存会严重降低性能:
- N+1 问题:每条数据单独执行一次 SQL(产生 N 次网络IO + SQL 解析开销)
- 对比 :
saveBatch()
默认会合并为单条 SQL 或小批量提交(如INSERT INTO table VALUES (...), (...), ...
)
2. 异常处理建议
避免在循环内捕获异常后继续执行(除非明确需要部分提交):
// ❌ 错误做法(导致事务失效):
for (SysUserRole userRole : userRoleList) {
try {
sysUserRoleService.save(userRole);
} catch (Exception e) {
// 捕获后不抛出,事务无法感知异常,继续提交后续数据!
}
}
3. 嵌套事务风险
如果 sysUserRoleService.save()
也有 @Transactional
:
- 默认传播行为 (
REQUIRED
) 会加入当前事务 → 安全,行为一致 - 若改为
REQUIRES_NEW
则每次循环新建独立事务 → 破坏原子性(部分提交)
结论:
- 事务有效:循环单条保存不会破坏事务的原子性,失败时仍会全局回滚。
- 避免滥用循环 :务必优先使用批量操作(如
saveBatch()
)以保证性能。 - 统一事务上下文:只要不修改默认的传播行为,嵌套调用的操作仍在同一事务中。