MyBatis-Plus 主键策略与事务处理中的主键跳跃问题解决
背景
在某个业务场景中,有一张主表 `BizExamineeInfo`,其主键 `id` 是自增主键。而其他几张从表(如成绩表、工作经历表、教育经历表等)的主键 `id` 需要与主表的主键 `id` 保持一致。这种设计可以确保从表与主表之间的引用关系更加紧密。代码示例
以下是调整前的业务逻辑代码片段:
@Override
@Transactional(rollbackFor = Exception.class)
public void addSubstituteExaminee(SubstituteExamineeAddReq req) {
// ……各种任务校验
// 检查是否已有考生 ID,如果没有则插入新的考生信息
if (ObjectUtil.isNotNull(examineeId)) {
examineeInfo.setId(examineeId);
baseMapper.enableExamineeIdSingle(examineeId);
} else {
examineeInfo.setDelFlag(0);
baseMapper.insert(examineeInfo); // 插入主表
}
// 查询考试注册记录
BizExamRegister register = new LambdaQueryChainWrapper<>(examRegisterMapper)
.eq(BizExamRegister::getExamineeId, examineeInfoTarget.getId()).one();
if (ObjectUtil.isNotNull(register)) {
BizExamLocationRecord bizExamLocationRecord = examLocationRecordMapper.selectById(register.getExamLocationId());
// 各种业务校验
if (ProjectConstants.ONE.equals(bizExamLocationRecord.getAssignExamineeToRoomFlag())) {
throw new CheckedException("替补对象所在考点已完成面试抽签,不支持替补");
} else if (ProjectConstants.ONE.equals(bizExamLocationRecord.getAssignExamineeToGroupFlag())) {
throw new CheckedException("替补对象所在考点已完成体侧抽签,不支持替补");
} else if (ProjectConstants.ONE.equals(bizExamLocationRecord.getGroupSetState())) {
throw new CheckedException("替补对象所在考点已完成体侧分组,不支持替补");
}
// 更新考试注册记录
new LambdaUpdateChainWrapper<>(examRegisterMapper)
.set(BizExamRegister::getExamineeId, examineeInfo.getId())
.eq(BizExamRegister::getExamineeId, examineeInfoTarget.getId()).update();
}
// 插入从表数据:考生成绩
BizExamineeScore bizExamineeScore = new BizExamineeScore();
BeanUtils.copyProperties(req.getExamineeScoreDetailResp(), bizExamineeScore);
bizExamineeScore.setExamineeId(examineeInfo.getId()); // 主表的主键 ID
bizExamineeScore.setId(examineeInfo.getId()); // 从表的主键 ID
examineeScoreMapper.insert(bizExamineeScore);
// 插入从表数据:工作经历
BizWorkExperience bizWorkExperience = new BizWorkExperience();
BeanUtils.copyProperties(req.getWorkExperienceDetailResp(), bizWorkExperience);
bizWorkExperience.setExamineeId(examineeInfo.getId());
bizWorkExperience.setId(examineeInfo.getId());
workExperienceMapper.insert(bizWorkExperience);
// 插入从表数据:教育经历
BizEducationExperience bizEducationExperience = new BizEducationExperience();
BeanUtils.copyProperties(req.getEducationExperienceDetailResp(), bizEducationExperience);
bizEducationExperience.setExamineeId(examineeInfo.getId());
bizEducationExperience.setId(examineeInfo.getId());
educationExperienceMapper.insert(bizEducationExperience);
// 插入从表数据:岗位要求
BeanUtils.copyProperties(req.getPostRequireDetailResp(), bizPostRequire);
bizPostRequire.setExamineeId(examineeInfo.getId());
bizPostRequire.setId(examineeInfo.getId());
postRequireMapper.insert(bizPostRequire);
// ……
}
MySQL主键跳跃
主键跳跃是指在使用自增主键的数据库表中,某些情况下主键 ID 并不会按照严格的顺序递增,而是出现跳跃现象。例如:假设主表的自增主键从 1001
开始,当插入一条记录时,理论上下一条记录应该是 1002
,但是由于某些事务回滚或其他数据库操作,下一条记录的主键可能直接跳到了 1003
或者更大,导致主键 ID 中间出现空洞。
事务回滚只会撤销已插入的数据,而不会回退数据库自增主键的计数器(即已占用的主键 ID)。
主键跳跃问题分析
在上述代码中,BizExamineeInfo
表的主键 ID 使用了 MySQL 的自增策略。当插入主表数据时,如果遇到事务回滚或者某些条件导致插入失败,BizExamineeInfo
表的自增主键 ID 就会跳跃,导致下次插入的主键 ID 不再是紧邻的数字。
多个从表(如 BizExamineeScore
、BizWorkExperience
等)需要使用主表的主键 ID 作为它们的主键。 由于 MyBatis-Plus 的全局主键策略是 AUTO
(即依赖数据库自增),即使手动给从表的主键赋值,它们仍然会使用 MySQL 的自增主键,导致主表和从表的主键 ID 不一致,进而引发数据关联问题。
MyBatis-Plus主键策略
全局策略与局部策略
MyBatis-Plus 支持全局和局部两种主键策略:
- 全局策略:在 MyBatis-Plus 的配置文件中配置全局的主键生成策略,适用于整个项目中所有的实体类。
mybatis-plus:
global-config:
db-config:
id-type: AUTO # 全局主键策略
- 局部策略:在实体类的字段上通过注解指定局部主键策略,覆盖全局策略。
@TableId(type = IdType.INPUT) // 局部主键策略
private Long id;
主键生成策略
MyBatis-Plus 提供了多种主键生成策略,可以根据实际需求选择合适的策略。从版本 3.3.0 开始,MyBatis-Plus 会自动识别主键类型,因此不再需要手动指定主键类型。
MyBatis-Plus 支持如下几种主键生成策略:
值 | 描述 |
---|---|
AUTO | 数据库 ID 自增策略,依赖数据库的 AUTO_INCREMENT 。 |
NONE | 无状态策略,未设置主键类型(跟随全局配置)。 |
INPUT | 手动设置主键值,适用于用户自行生成主键 ID。 |
ASSIGN_ID | 分配全局唯一 ID(通常基于雪花算法)。适用于 Long 、Integer 或 String 类型。 |
ASSIGN_UUID | 分配 UUID,适用于 String 类型。 |
解决方法
为了避免主键跳跃问题,需要做以下几点:
- 取消从表的自增主键设置:从表的主键
id
需要手动赋值,不再依赖数据库自动生成。 - 处理业务逻辑异常:先处理所有可能的业务逻辑异常, 插入主表之前处理所有可能导致事务回滚的业务逻辑,确保插入主表时不会因业务逻辑问题导致主键跳跃。
- 先插入主表,获取主键 ID:插入主表
BizExamineeInfo
后,获取其主键 ID。 - 使用主表的主键 ID 插入从表:将获取的主键 ID 赋值给从表的主键,确保主表和从表的主键一致。
- 更新从表的主键策略: 将从表的主键策略设置为
INPUT
,允许手动插入主键 ID。
修改后的代码
@Override
@Transactional(rollbackFor = Exception.class)
public void addSubstituteExaminee(SubstituteExamineeAddReq req) {
// ……
// 查询考试注册记录
BizExamRegister register = new LambdaQueryChainWrapper<>(examRegisterMapper)
.eq(BizExamRegister::getExamineeId, examineeInfoTarget.getId()).one();
if (ObjectUtil.isNotNull(register)) {
BizExamLocationRecord bizExamLocationRecord = examLocationRecordMapper.selectById(register.getExamLocationId());
// 各种业务校验
if (ProjectConstants.ONE.equals(bizExamLocationRecord.getAssignExamineeToRoomFlag())) {
throw new CheckedException("替补对象所在考点已完成面试抽签,不支持替补");
} else if (ProjectConstants.ONE.equals(bizExamLocationRecord.getAssignExamineeToGroupFlag())) {
throw new CheckedException("替补对象所在考点已完成体侧抽签,不支持替补");
} else if (ProjectConstants.ONE.equals(bizExamLocationRecord.getGroupSetState())) {
throw new CheckedException("替补对象所在考点已完成体侧分组,不支持替补");
}
}
// 检查是否已有考生 ID,如果没有则插入新的考生信息
if (ObjectUtil.isNotNull(examineeId)) {
examineeInfo.setId(examineeId);
baseMapper.enableExamineeIdSingle(examineeId);
} else {
examineeInfo.setDelFlag(0);
baseMapper.insert(examineeInfo); // 插入主表, 获取主键 ID
}
Long examineeInfoId = examineeInfo.getId(); // 获取主表的主键 ID
if (ObjectUtil.isNotNull(register)) {
// 更新考试注册记录
new LambdaUpdateChainWrapper<>(examRegisterMapper)
.set(BizExamRegister::getExamineeId, examineeInfoId)
.eq(BizExamRegister::getExamineeId, examineeInfoTarget.getId()).update();
}
// 插入从表数据:考生成绩
BizExamineeScore bizExamineeScore = new BizExamineeScore();
BeanUtils.copyProperties(req.getExamineeScoreDetailResp(), bizExamineeScore);
bizExamineeScore.setExamineeId(examineeInfoId); // 主表的主键 ID
bizExamineeScore.setId(examineeInfoId); // 从表的主键 ID
examineeScoreMapper.insert(bizExamineeScore);
// 插入从表数据:工作经历
BizWorkExperience bizWorkExperience = new BizWorkExperience();
BeanUtils.copyProperties(req.getWorkExperienceDetailResp(), bizWorkExperience);
bizWorkExperience.setExamineeId(examineeInfoId);
bizWorkExperience.setId(examineeInfoId);
workExperienceMapper.insert(bizWorkExperience);
// 插入从表数据:教育经历
BizEducationExperience bizEducationExperience = new BizEducationExperience();
BeanUtils.copyProperties(req.getEducationExperienceDetailResp(), bizEducationExperience);
bizEducationExperience.setExamineeId(examineeInfoId);
bizEducationExperience.setId(examineeInfoId);
educationExperienceMapper.insert(bizEducationExperience);
// 插入从表数据:岗位要求
BeanUtils.copyProperties(req.getPostRequireDetailResp(), bizPostRequire);
bizPostRequire.setExamineeId(examineeInfoId);
bizPostRequire.setId(examineeInfoId);
postRequireMapper.insert(bizPostRequire);
// ……
}
代码调整要点
- 主表所有异常校验通过后插入:在插入主表之前,尽可能校验所有可能导致事务回滚的业务错误。
- 从表主键手动赋值:mysql几个从表的主键
id
不再是自增,而是手动赋值为主表的主键 ID,保证主表和从表的主键一致。 - 事务一致性:通过 MyBatis-Plus 配合事务管理,确保主表和从表的操作要么全部成功,要么全部失败,避免数据不一致性。
原文地址:https://blog.csdn.net/weixin_43993310/article/details/143891473
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!