原创/朱季谦

某天检查一位离职同事写的代码,发现其对应表虽然设置了AUTO_INCREMENT自增,但页面新增功能生成的数据主键id很诡异,长度达到了19位,且不是从1开始递增的——

我检查了一下,发现该表目前自增主键已经变成从1468844351843872770开始递增了——

这就很奇怪了,目前该表数据量很少,且主键是设置AUTO_INCREMENT,正常而言,应该自增id仍在1000范围内,但目前已经变成一串长数字。

底层ORM框架用的是Mybatis-Plus,我寻思了一下,这看起来像是在插入数据库旧自动生成的id,导致并非默认使用MySql的自增AUTO_INCREMENT的id。

因此,决定一步步定位,先给Mybatis-Plus打印出sql日志,看下其insert语句是否自动生成了一个id,然后才插入数据库。

按照网上的教程,我在yaml文件里对应的mybatis-plus配置处设置了开启sql打印日志——

mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

然而,很诡异的是,执行操作时并没有打印出sql日志,故而,某一瞬间,我忽然觉得,这群家伙可能都是互相抄的,没有验证当springboot集成了logback时,这样设置并没有效果。

最后,我额外在yaml加了以下配置,才能正常打印MP的sql日志信息——

logging:
level:
com:
zhu:
test:
mapper: debug

接下来,验证一番后,发现,Mybatis-Plus在做insert操作时,确实自动生成了一条长19的数字当做该条数据的id插入到MySql,导致虽然MySql表设置了自增,但被该1468844351843872769影响了,导致下一条数据自动递增值变成了1468844351843872770,这种过长的id值,在做索引维护时,很影响效率,故而,这个问题必须得解决。

到这里,就确定,这个长数字的id,是在代码层次就自动生成了,最后进入对应的实体类中,发现该映射数据表的id字段,并没有显示设置对应的主键生成策略。

@Data
@TableName("test")
public class Test extends Model<Test> implements Serializable { private Long id;
......
}

Mybatis-Plus主要有以下几种主键生成策略——

@Getter
public enum IdType {
/**
* 数据库ID自增
*/
AUTO(0),
/**
* 该类型为未设置主键类型
*/
NONE(1),
/**
* 用户输入ID
* 该类型可以通过自己注册自动填充插件进行填充
*/
INPUT(2), /* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */
/**
* 全局唯一ID (idWorker),根据雪花算法生成19位数字,long类型
*/
ID_WORKER(3),
/**
* 全局唯一ID (UUID)
*/
UUID(4),
/**
* 字符串全局唯一ID (idWorker 的字符串表示),根据雪花算法生成19位字符串,String
*/
ID_WORKER_STR(5); private int key; IdType(int key) {
this.key = key;
}
}

这里验证了一下,当设置成这样时,就能正常生成数据库自增的id了,使用数据库AUTO_INCREMENT从1开始自增的效果了,当然,其实使用IdType.AUTO也是可以的——

@Data
@TableName("test")
public class Test extends Model<Test> implements Serializable {
@TableId(value = "id", type = IdType.INPUT)
private Long id;
......
}

百度网上的说法,当Mybatis-Plus实体类没有显示设置主键策略时,将默认使用雪花算法生成,也就是IdType.ID_WORKER或者IdType.ID_WORKER_STR,具体是long类型的19位还是字符串的19位,应该是根据字段定义类型来判断。

snowflake算法是Twitter开源的分布式ID生成算法,结果是一个long类型的ID 。其核心思想:使用41bit作为毫秒数,10bit作为机器的ID(5bit数据中心,5bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每个毫秒可以产生4096个ID),最后还有一个符号位,永远是0。

接下来,先验证Mybatis-Plus默认主键策略是如何的。

Mybatis-Plus项目在启动时,会对注解实体类进行初始化,然后缓存到系统Map中。

这里,只需要关注Mybatis-Plus源码TableInfoHelper类中的initTableInfo方法即可,这个方法在项目启动时会被调用,然后初始化所有注解@TableName的实体类。与主键根据哪种策略来设置的逻辑在方法initTableFields(clazz, globalConfig, tableInfo)当中——

public synchronized static TableInfo initTableInfo(MapperBuilderAssistant builderAssistant, Class<?> clazz) {
TableInfo tableInfo = TABLE_INFO_CACHE.get(clazz.getName());
if (tableInfo != null) {
if (tableInfo.getConfigMark() == null && builderAssistant != null) {
tableInfo.setConfigMark(builderAssistant.getConfiguration());
}
return tableInfo;
} /* 没有获取到缓存信息,则初始化 */
tableInfo = new TableInfo();
GlobalConfig globalConfig;
if (null != builderAssistant) {
tableInfo.setCurrentNamespace(builderAssistant.getCurrentNamespace());
tableInfo.setConfigMark(builderAssistant.getConfiguration());
tableInfo.setUnderCamel(builderAssistant.getConfiguration().isMapUnderscoreToCamelCase());
globalConfig = GlobalConfigUtils.getGlobalConfig(builderAssistant.getConfiguration());
} else {
// 兼容测试场景
globalConfig = GlobalConfigUtils.defaults();
} /* 初始化表名相关 */
initTableName(clazz, globalConfig, tableInfo); /* 初始化字段相关 */
initTableFields(clazz, globalConfig, tableInfo); /* 放入缓存 */
TABLE_INFO_CACHE.put(clazz.getName(), tableInfo); /* 缓存 Lambda 映射关系 */
LambdaUtils.createCache(clazz, tableInfo);
return tableInfo;
}

在初始化字段相关的initTableFields方法里,会判断是否有@TableId 注解,如果没有,就执行initTableIdWithoutAnnotation方法,连续前文提到的,如果实体类id没有加@TableId(value = "id", type = IdType.INPUT),那么就会取默认的主键策略。这里的判断是否有@TableId 注解,就是判断是否需要取默认的主键策略,至于具体是如何设置默认主键的,我们可以直接进入到initTableIdWithoutAnnotation方法当中。

public static void initTableFields(Class<?> clazz, GlobalConfig globalConfig, TableInfo tableInfo) {
/* 数据库全局配置 */
GlobalConfig.DbConfig dbConfig = globalConfig.getDbConfig();
List<Field> list = getAllFields(clazz);
// 标记是否读取到主键
boolean isReadPK = false;
// 是否存在 @TableId 注解
boolean existTableId = isExistTableId(list); List<TableFieldInfo> fieldList = new ArrayList<>();
for (Field field : list) {
/*
* 主键ID 初始化
*/
if (!isReadPK) {
if (existTableId) {
isReadPK = initTableIdWithAnnotation(dbConfig, tableInfo, field, clazz);
} else {
isReadPK = initTableIdWithoutAnnotation(dbConfig, tableInfo, field, clazz);
}
if (isReadPK) {
continue;
}
}
......
}
......
}

initTableIdWithoutAnnotation方法——

private static final String DEFAULT_ID_NAME = "id";
/**
* <p>
* 主键属性初始化
* </p>
*
* @param tableInfo 表信息
* @param field 字段
* @param clazz 实体类
* @return true 继续下一个属性判断,返回 continue;
*/
private static boolean initTableIdWithoutAnnotation(GlobalConfig.DbConfig dbConfig, TableInfo tableInfo,
Field field, Class<?> clazz) {
//获取实体类字段名
String column = field.getName();
if (dbConfig.isCapitalMode()) {
column = column.toUpperCase();
}
//当字段名为id
if (DEFAULT_ID_NAME.equalsIgnoreCase(column)) {
if (StringUtils.isEmpty(tableInfo.getKeyColumn())) {
tableInfo.setKeyRelated(checkRelated(tableInfo.isUnderCamel(), field.getName(), column))
//设置表策略
.setIdType(dbConfig.getIdType())
.setKeyColumn(column)
.setKeyProperty(field.getName())
.setClazz(field.getDeclaringClass());
return true;
} else {
throwExceptionId(clazz);
}
}
return false;
}

Debug到这里,可以看到,如果没有 @TableId 注解显示设置主键策略情况下,默认设置的是 ID_WORKER(3),即会根据雪花算法生成19位数字,long类型。

可以进一步发现,这里的 dbConfig是GlobalConfig.DbConfig实例,进入到DbConfig类,可以看到原来实体类映射的数据库设置在这里,主键类型默认是IdType.ID_WORKER。

@Data
public static class DbConfig { /**
* 数据库类型
*/
private DbType dbType = DbType.OTHER;
/**
* 主键类型(默认 ID_WORKER)
*/
private IdType idType = IdType.ID_WORKER;
/**
* 表名前缀
*/
private String tablePrefix;
/**
* 表名、是否使用下划线命名(默认 true:默认数据库表下划线命名)
*/
private boolean tableUnderline = true;
/**
* String 类型字段 LIKE
*/
private boolean columnLike = false;
/**
* 大写命名
*/
private boolean capitalMode = false;
/**
* 表关键词 key 生成器
*/
private IKeyGenerator keyGenerator;
/**
* 逻辑删除全局值(默认 1、表示已删除)
*/
private String logicDeleteValue = "1";
/**
* 逻辑未删除全局值(默认 0、表示未删除)
*/
private String logicNotDeleteValue = "0";
/**
* 字段验证策略
*/
private FieldStrategy fieldStrategy = FieldStrategy.NOT_NULL;
}

至于如何生成雪花算法id,这里就不一一详细介绍,具体逻辑是在MybatisDefaultParameterHandler类populateKeys方法里,核心代码如下——

protected static Object populateKeys(MetaObjectHandler metaObjectHandler, TableInfo tableInfo,
MappedStatement ms, Object parameterObject, boolean isInsert) {
if (null == tableInfo) {
/* 不处理 */
return parameterObject;
}
/* 自定义元对象填充控制器 */
MetaObject metaObject = ms.getConfiguration().newMetaObject(parameterObject);
// 填充主键
if (isInsert && !StringUtils.isEmpty(tableInfo.getKeyProperty())
&& null != tableInfo.getIdType() && tableInfo.getIdType().getKey() >= 3) {
Object idValue = metaObject.getValue(tableInfo.getKeyProperty());
/* 自定义 ID */
if (StringUtils.checkValNull(idValue)) {
if (tableInfo.getIdType() == IdType.ID_WORKER) {
metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.getId());
} else if (tableInfo.getIdType() == IdType.ID_WORKER_STR) {
metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.getIdStr());
} else if (tableInfo.getIdType() == IdType.UUID) {
metaObject.setValue(tableInfo.getKeyProperty(), IdWorker.get32UUID());
}
}
}
......
}

前边提到,默认的主键策略是IdType.ID_WORKER,这里有一个判断tableInfo.getIdType() == IdType.ID_WORKER,对代码Debug可以看到,metaObject的setValue(tableInfo.getKeyProperty(), IdWorker.getId())代码的作用,是对注解id进行了值填充。



填充的值为IdWorker.getId()返回的1468970800437465089,刚好是19位长度,这就意味着,这里产生的id值,就是我们最后要找的。

IdWorker.getId()实现本质,正好是基于Snowflake实现64位自增ID算法,而Snowflake,正是引用了雪花算法——

/**
* <p>
* 高效GUID产生算法(sequence),基于Snowflake实现64位自增ID算法。 <br>
* 优化开源项目 http://git.oschina.net/yu120/sequence
* </p>
*
* @author hubin
* @since 2016-08-01
*/
public class IdWorker { /**
* 主机和进程的机器码
*/
private static final Sequence WORKER = new Sequence(); public static long getId() {
return WORKER.nextId();
} public static String getIdStr() {
return String.valueOf(WORKER.nextId());
} /**
* <p>
* 获取去掉"-" UUID
* </p>
*/
public static synchronized String get32UUID() {
return UUID.randomUUID().toString().replace(StringPool.DASH, StringPool.EMPTY);
} }

Mybatis-Plus默认主键策略导致自动生成19位长度主键id的坑的更多相关文章

  1. 主键策略+mybayisPlus自动增长

    主键策略: 1.自动增长 有一点小缺陷:例如当一张表里的数据过于庞大时我们会进行分表操作,若是用自动增长策略,那么除了第一张表外的每一张表都必须知道上一张的表的的最后ID值.这个操作便会造成效率的变低 ...

  2. mybatis自动生成后无法获取主键id问题

    使用mybatis的自动生成工具生成的主键id: <selectKey resultType="java.lang.Integer" keyProperty="id ...

  3. mybatis中Oracle及mysql插入时自动生成主键以及返回主键

    mysql的方式: 方式一: useGeneratedKeys="true" keyProperty="id" 方式二: <selectKey keyPr ...

  4. mybatis 添加后获得该新增数据自动生成的 id

    // useGeneratedKeys默认值为false,keyProperty的值对应的是User类中的主键字段名 // mybatis 写法如下 <insert id="inser ...

  5. Spring boot JPA 用自定义主键策略 生成自定义主键ID

    最近学习Spring boot JPA 学习过程解决的一些问题写成随笔,大家一起成长.这次遇到自定义主键的问题 package javax.persistence; public enum Gener ...

  6. 【Mybatis】MyBatis之Generator自动生成代码(九)

    MyBatis Generator 简介 MyBatis Generator 连接数据库表并生成MyBatis或iBatis文件.这有助于最大限度地减少使用MyBatis时为数据库文件创建简单CRUD ...

  7. mybatis新增对象自动生成uuid方案

    mybatis新增对象时, 使用拦截器自动生成uuid方案有了它不再去xml中手动添加selectKey了, 生成id方案实现类自行实现, 实现IdGenerator接口便可, 主要代码由公司同事编写 ...

  8. MyBatis 逆向工程(MyBatis 自动生成接口以及xml)的使用

    刚学MyBatis逆向工程(还以为要反汇编呢.....) MyBatis逆向工程 个人理解就是链接数据库自动生成相关的增删改查相关的类 以及xml文件 (其中有一些不足 应该就是多表链接的问题需要自己 ...

  9. 【MyBatis】MyBatis自动生成代码查询之爬坑记

    前言 项目使用SSM框架搭建Web后台服务,前台后使用restful api,后台使用MyBatisGenerator自动生成代码,在前台使用关键字进行查询时,遇到了一些很宝贵的坑,现记录如下.为展示 ...

随机推荐

  1. dotnet templating 定制自己的项目模板

    由于工作需要,研究了一下VS 项目模板生成的相关内容,本文做一下记录借助.NET Core Template Engine创建一个加单的项目模板. 创建项目代码和配置文件 首先创建一个Minimal ...

  2. LINKERD 2.11 中文实战手册

    1. 将您的服务添加到 Linkerd. 为了让您的服务利用 Linkerd,它们还需要通过将 Linkerd 的数据平面代理注入到它们的 pod 中来进行 网格化 . 2. 自动化的金丝雀发布. 通 ...

  3. 重写(Override)与重载(Overload)区别

    重写是子类对父类的允许访问的方法的实现过程进行重新编写. 方法重写三要素: (1)方法名形参列表相同: (2)返回值类型和声明异常类型子类小于父类: (3)访问权限,子类大于等于父类. 重写的好处在于 ...

  4. InnoDB 索引详解

    1.什么是索引 索引是存储引擎用于快速找到记录的一种数据结构. 2.索引有哪些数据结构 顺序查找结构:这种查找效率很低,复杂度为O(n).大数据量的时候查询效率很低. 有序的数据排列:二分查找法又称折 ...

  5. [hdu7097]Just a Data Structure Problem

    (四边形不等式的套路题) 对于某一组$a_{i}$,显然可以区间dp,设$f_{l,r}$表示区间$[l,r]$​的答案,则转移即$$f_{l,r}=\begin{cases}0&(l=r)\ ...

  6. [tc13008]Egalitarianism2

    考虑对于$n-1$个数$a_{i}$,函数$f(x)=\frac{\sum_{i=1}^{n-1}(x-a_{i})^{2}}{n-1}$的最小值恰在$x=\frac{\sum_{i=1}^{n-1} ...

  7. 【IDEA】字体大小和类型

    字体大小和类型 2020-09-08  09:06:21  by冲冲 1.工具界面的字体 2.代码的字体 注意:如果已经设置颜色主题,则还需要设置颜色主题的字体,才能生效.

  8. 【PS】证件照转换背景色

    证件照转换背景色 2019-07-14  12:18:49  by冲冲 1. 需求 自由切换证件照的背景颜色(白底.蓝底.红底...) 2. 步骤 ① 双击 图层锁 解锁,弹出的"新建图层0 ...

  9. 小白都能看懂的 Spring 源码揭秘之依赖注入(DI)源码分析

    目录 前言 依赖注入的入口方法 依赖注入流程分析 AbstractBeanFactory#getBean AbstractBeanFactory#doGetBean AbstractAutowireC ...

  10. 8.4 k8s实现Nginx+Php+WordPress+MySQL实现完全容器化的web站点案例

    1.制作Nginx镜像 1.1 使用nginx:1.21.1官方镜像 # 下载官方镜像 docker pull nginx:1.21.1 # 打本地harbor的tag docker tag 192. ...