自学内容网 自学内容网

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 不再是紧邻的数字。

  多个从表(如 BizExamineeScoreBizWorkExperience 等)需要使用主表的主键 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(通常基于雪花算法)。适用于 LongIntegerString类型。
ASSIGN_UUID分配 UUID,适用于 String类型。

解决方法

为了避免主键跳跃问题,需要做以下几点:

  1. 取消从表的自增主键设置:从表的主键 id 需要手动赋值,不再依赖数据库自动生成。
  2. 处理业务逻辑异常:先处理所有可能的业务逻辑异常, 插入主表之前处理所有可能导致事务回滚的业务逻辑,确保插入主表时不会因业务逻辑问题导致主键跳跃。
  3. 先插入主表,获取主键 ID:插入主表 BizExamineeInfo 后,获取其主键 ID。
  4. 使用主表的主键 ID 插入从表:将获取的主键 ID 赋值给从表的主键,确保主表和从表的主键一致。
  5. 更新从表的主键策略: 将从表的主键策略设置为 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);
// ……
}

代码调整要点

  1. 主表所有异常校验通过后插入:在插入主表之前,尽可能校验所有可能导致事务回滚的业务错误。
  2. 从表主键手动赋值:mysql几个从表的主键 id 不再是自增,而是手动赋值为主表的主键 ID,保证主表和从表的主键一致。
  3. 事务一致性:通过 MyBatis-Plus 配合事务管理,确保主表和从表的操作要么全部成功,要么全部失败,避免数据不一致性。


原文地址:https://blog.csdn.net/weixin_43993310/article/details/143891473

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!