自学内容网 自学内容网

mybatis-plus仿 JPA的 GenerationType.TABLE 实现

概述

1)配置mybatis-plus 主键默认策略,实体添加主键注解
2)新建主键存储表
3)编写主键策略实现类
注意事项:如果 程序含数据库恢复功能,数据恢复后,需调用 CustomIdGenerator.clearTabIdStateMap() 方法

现在各个数据库都支持建表生成自增主键,这种方式性能挺好的;
mysql、postgrel、oracle 12 c +,sqlserver ;只是部分备份、恢复带了序列,没有屏蔽底层实现;

根据实际情况选择实现方式,我们这边是因为测试变态:
说系统时间改了,程序不报错,所以默认mybatis-plus的雪花不让用;其次程序部分涉及跟时间相关业务,uuid主键实现效果很差;只能折中自己实现主键策略,本人更倾向于数据库策略:自增主键;

1、配置mybatis-plus 主键默认策略,实体添加主键注解

1) 配置mybatis-plus 主键默认策略

id-type: ASSIGN_ID,默认雪花,但被替换成自定义实现

# mybatis-plus
mybatis-plus:
  # log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
  # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath*:mapper/*Mapper.xml,classpath*:mapper-mysql/*Mapper.xml
  #开启驼峰命名
  configuration:
    map-underscore-to-camel-case: true
    jdbc-type-for-null: 'null'
  global-config:
    banner: false
    db-config:
      id-type: ASSIGN_ID # 默认雪花,但被替换成自定义实现
      logic-delete-field: DELETE_FLAG
      logic-delete-value: 1
      logic-not-delete-value: 0
      column-format: "`%s`"

2) 实体添加 主键注解:

主键字段添加 @TableId 注解,实体建议添加 @TableName

package entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;

import java.io.Serial;
import java.io.Serializable;


/**
 * <p>
 * 白名单
 * </p>
 *
 * @author fisec
 * @since 2024-04-24
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper=true)
@TableName("sys_ip_whitelist")
public class IpWhitelist implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId
    private Long id;

    ....

}

2、新建主键存储表

用于存储主键的关系

CREATE TABLE `pk_gen` (
  `sequence_name` varchar(255) NOT NULL,
  `next_val` bigint DEFAULT NULL,
  PRIMARY KEY (`sequence_name`)
) ENGINE=InnoDB;

3、编写主键策略实现类

实际 包含4个类:

1、CustomIdGenerator.class :实现mybatis-plus主键接口
2、PkGenService.class:与存储数据库表交互
## 下面2个类为仿写 hibernate 实现
3、GenerationTableId.class:调用和存储生成的主键,对应 hibernate 的 PooledLoThreadLocalOptimizer.class
4、TableIdGenerator.class:调用数据交互 PkGenService,对应 hibernate 的 TableGenerator.class

1)CustomIdGenerator.class 实现mybatis-plus主键接口

实现 IdentifierGenerator 的 nextId方法,来调用主键生成类

package system.config.id;

import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import system.pkGen.service.IPkGenService;
import org.springframework.stereotype.Component;

/**
 * 替换默认ASSIGN_ID(默认的雪花算法),为自己的实现
 */
@Component
public class CustomIdGenerator implements IdentifierGenerator {

    /**
     * 数据库表生成策略
     */
    private final GenerationTableId generationTableId = new GenerationTableId();

    public void clearTabIdStateMap(){
        generationTableId.clearTabIdStateMap();
    }

    @Override
    public Number nextId(Object entity) {
        // 1 可以将当前传入的class 获取对应的数据库表名
        String tableName = TableInfoHelper.getTableInfo(entity.getClass()).getTableName();

        // 2 表主键生成策略
        TableIdGenerator tableIdGenerator = new TableIdGenerator();
        tableIdGenerator.setTenantIdentifier(tableName);

        // 3 通过上下文获取IPkGenService,防止产生循环依赖
        IPkGenService iPkGenService = SpringUtil.getBean(IPkGenService.class);

        // 4 调用策略生成主键
        return generationTableId.generate(iPkGenService, tableIdGenerator);
    }
}

2)PkGenService.class 与存储数据库表交互

核心查询 FOR UPDATE ,保证分布式系统的主键唯一性,保证每个应用或线程取到 id段 唯一

package system.pkGen.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fisec.system.pkGen.entity.PkGen;
import com.fisec.system.pkGen.mapper.PkGenMapper;
import com.fisec.system.pkGen.service.IPkGenService;
import org.springframework.stereotype.Service;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author fisec
 * @since 2024-07-08
 */
@Service
public class PkGenServiceImpl extends ServiceImpl<PkGenMapper, PkGen> implements IPkGenService {

    @Override
    public PkGen getForUpdateByName(String sequenceName) {
        LambdaQueryWrapper<PkGen> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(PkGen::getSequenceName,sequenceName);
        // 1、查询加锁,保证分布式和多线程中的唯一性
        queryWrapper.last("FOR UPDATE");
        return baseMapper.selectOne(queryWrapper);
    }
}

3) 主键生成核心类 - GenerationTableId.class

主键生成类包含2个: GenerationTableId.class 用于存储和调用生成主键,TableIdGenerator.class 数据库调用(调用核心查询 FOR UPDATE )

# 1)hibernate
对应 org.hibernate.id.enhanced.PooledLoThreadLocalOptimizer.class,与 JPA hibinate实现 区别
hibinate 是线程级别的,因为 hibinate 整个会话自己管理
ThreadLocal<GenerationState> singleTenantState = ThreadLocal.withInitial( GenerationState::new );
# 2)因为这个被调用是tomcate的线程级别
所以不推荐 使用 ThreadLocal ,这里改为了ConcurrentHashMap 应用级别;

注意:GenerationTableId类内的 incrementSize:取到的主键步长可以自定义,这边为了开发方便测试为1,正式为100,hibernate默认为50

package system.config.id;

import cn.hutool.core.util.NumberUtil;
import com.fisec.common.global.FisecConfig;
import com.fisec.system.pkGen.service.IPkGenService;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ConcurrentHashMap;

@Slf4j
public class GenerationTableId {

    private final ConcurrentHashMap<String, GenerationState> tabIdStateMap = new ConcurrentHashMap<>();


    public void clearTabIdStateMap(){
        tabIdStateMap.clear();
    }

    /**
     * 加载当前线程的 GenerationState
     *
     * @param tableName 表名
     * @return GenerationState
     */
    private GenerationState locateGenerationState(String tableName) {
        GenerationState state = tabIdStateMap.get(tableName);
        if (state == null) {
            state = new GenerationState();
            tabIdStateMap.put(tableName, state);
        }
        return state;
    }

    /**
     * 调用生成主键的方法
     *
     * @param tableIdGenerator 表主键生成策略
     * @return long
     */
    public Long generate(IPkGenService iPkGenService, TableIdGenerator tableIdGenerator) {
        return locateGenerationState(tableIdGenerator.getTenantIdentifier())
                .generate(iPkGenService, tableIdGenerator, 50);
    }

    /**
     * 当前线程的表生成策略
     */
    private static class GenerationState {
        /**
         * 上次从数据库取到的值
         */
        private Long lastSourceValue;
        /**
         * 当前的值
         */
        private Long value;
        /**
         * 更新主键的上限制
         */
        private Long upperLimitValue;

        private Long generate(IPkGenService iPkGenService, TableIdGenerator tableIdGenerator, Long incrementSize) {
            if (value == null || value >= upperLimitValue) {
                lastSourceValue = tableIdGenerator.nextValue(iPkGenService, incrementSize);
                upperLimitValue = NumberUtil.add(lastSourceValue, incrementSize).longValue();
                value = lastSourceValue;
            }
            return value++;
        }
    }
}

4)主键生成核心类 - TableIdGenerator.class

TableIdGenerator.class :查询需要到的主键状态,并把下次生成的状态存储到数据库
对应 hibernate的 org.hibernate.id.enhanced.TableGenerator.class

package system.config.id;

import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import com.fisec.system.pkGen.entity.PkGen;
import com.fisec.system.pkGen.service.IPkGenService;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Getter
@Setter
public class TableIdGenerator {

    /**
     * 生成主键的票据
     */
    private String tenantIdentifier;

    /**
     * 生成表主键的核心
     *
     * @param iPkGenService 数据库连接回话
     * @return long
     */
    public long nextValue(IPkGenService iPkGenService, Long incrementSize) {

        // 1 通过mybatis-plus 获取表名和列表
        String tableName = this.tenantIdentifier;

        final Long nextValue;
        try {
            PkGen pkGen = iPkGenService.getForUpdateByName(tableName);
            if (ObjectUtil.isNull(pkGen)) {
                nextValue = 1L;
                pkGen = new PkGen();
                pkGen.setSequenceName(tableName);

                pkGen.setNextVal(NumberUtil.add(nextValue, incrementSize).longValue());
                iPkGenService.save(pkGen);
            } else {
                nextValue = pkGen.getNextVal();
                pkGen.setNextVal(NumberUtil.add(nextValue, incrementSize).longValue());
                iPkGenService.updateById(pkGen);
            }
            return nextValue;
        } catch (Exception e) {
            throw new RuntimeException("Error executing SQL", e);
        }
    }
}


原文地址:https://blog.csdn.net/qq_26408545/article/details/140500812

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