Mybatis-Plus默認(rèn)主鍵策略導(dǎo)致自動(dòng)生成19位長度主鍵id的坑
某天檢查一位離職同事寫的代碼,發(fā)現(xiàn)其對應(yīng)表雖然設(shè)置了AUTO_INCREMENT自增,但頁面新增功能生成的數(shù)據(jù)主鍵id很詭異,長度達(dá)到了19位,且不是從1開始遞增的——
我檢查了一下,發(fā)現(xiàn)該表目前自增主鍵已經(jīng)變成從1468844351843872770開始遞增了——
這就很奇怪了,目前該表數(shù)據(jù)量很少,且主鍵是設(shè)置AUTO_INCREMENT,正常而言,應(yīng)該自增id仍在1000范圍內(nèi),但目前已經(jīng)變成一串長數(shù)字。
底層ORM框架用的是Mybatis-Plus,我尋思了一下,這看起來像是在插入數(shù)據(jù)庫就自動(dòng)生成的id,導(dǎo)致并非默認(rèn)使用MySql的自增AUTO_INCREMENT來生成id。
因此,決定一步步定位,先給Mybatis-Plus打印出sql日志,看下其insert語句是否自動(dòng)生成了一個(gè)id后才插入數(shù)據(jù)庫。
按照網(wǎng)上的教程,我在yaml文件里對應(yīng)的mybatis-plus配置處設(shè)置了開啟sql打印日志——
mybatis-plus: mapper-locations: classpath*:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
然而,很詭異的是,執(zhí)行操作時(shí)并沒有打印出sql日志,故而,某一瞬間,我忽然覺得,這群家伙可能都是互相抄的,沒有驗(yàn)證當(dāng)springboot集成了logback時(shí),單純這樣設(shè)置并沒有效果。
最后額外在yaml加了以下配置,才能正常打印MP的sql日志信息——
logging:
? level:
? ? com:
? ? ? zhu:
? ? ? ? test:
? ? ? ? ? mapper: debug? ?
接下來,驗(yàn)證一番后,發(fā)現(xiàn),Mybatis-Plus在做insert操作時(shí),確實(shí)自動(dòng)生成一條長19的數(shù)字當(dāng)做該條數(shù)據(jù)的id插入到MySql,導(dǎo)致雖然MySql表設(shè)置了自增,但被Mybatis-Plus生成的id為1468844351843872769所影響,導(dǎo)致下一條數(shù)據(jù)自動(dòng)遞增值變成1468844351843872770,這種過長的id值,在做索引維護(hù)時(shí),是很影響效率,占用空間過大,故而,這個(gè)問題必須得解決。
到這里,就確定,這個(gè)長數(shù)字的id,是在代碼層次就自動(dòng)生成了,最后進(jìn)入對應(yīng)的實(shí)體類中,發(fā)現(xiàn)該映射數(shù)據(jù)表的id字段,并沒有顯示設(shè)置對應(yīng)的主鍵生成策略。
@Data @TableName("test") public class Test extends Model<Test> implements Serializable { private Long id; ...... }
Mybatis-Plus主要有以下幾種主鍵生成策略——
@Getter public enum IdType { /** * 數(shù)據(jù)庫ID自增 */ AUTO(0), /** * 該類型為未設(shè)置主鍵類型 */ NONE(1), /** * 用戶輸入ID * 該類型可以通過自己注冊自動(dòng)填充插件進(jìn)行填充 */ INPUT(2), /* 以下3種類型、只有當(dāng)插入對象ID 為空,才自動(dòng)填充。 */ /** * 全局唯一ID (idWorker),根據(jù)雪花算法生成19位數(shù)字,long類型 */ ID_WORKER(3), /** * 全局唯一ID (UUID) */ UUID(4), /** * 字符串全局唯一ID (idWorker 的字符串表示),根據(jù)雪花算法生成19位字符串,String */ ID_WORKER_STR(5); private int key; IdType(int key) { this.key = key; } }
這里驗(yàn)證了一下,當(dāng)設(shè)置成這樣時(shí),就能正常生成數(shù)據(jù)庫自增的id了,使用數(shù)據(jù)庫AUTO_INCREMENT從1開始自增的效果了,當(dāng)然,其實(shí)使用IdType.AUTO也是可以的——
@Data @TableName("test") public class Test extends Model<Test> implements Serializable { @TableId(value = "id", type = IdType.INPUT) private Long id; ...... }
百度網(wǎng)上的說法,當(dāng)Mybatis-Plus實(shí)體類沒有顯示設(shè)置主鍵策略時(shí),將默認(rèn)使用雪花算法生成,也就是IdType.ID_WORKER或者IdType.ID_WORKER_STR,具體是long類型的19位還是字符串的19位,應(yīng)該是根據(jù)字段定義類型來判斷。
snowflake算法是Twitter開源的分布式ID生成算法,結(jié)果是一個(gè)long類型的ID 。其核心思想:使用41bit作為毫秒數(shù),10bit作為機(jī)器的ID(5bit數(shù)據(jù)中心,5bit的機(jī)器ID),12bit作為毫秒內(nèi)的流水號(hào)(意味著每個(gè)節(jié)點(diǎn)在每個(gè)毫秒可以產(chǎn)生4096個(gè)ID),最后還有一個(gè)符號(hào)位,永遠(yuǎn)是0。
接下來,先驗(yàn)證Mybatis-Plus默認(rèn)主鍵策略是如何的。
Mybatis-Plus項(xiàng)目在啟動(dòng)時(shí),會(huì)對注解實(shí)體類進(jìn)行初始化,然后緩存到系統(tǒng)Map中。
這里,只需要關(guān)注Mybatis-Plus源碼TableInfoHelper類中的initTableInfo方法即可,這個(gè)方法在項(xiàng)目啟動(dòng)時(shí)會(huì)被調(diào)用,然后初始化所有注解@TableName的實(shí)體類。與主鍵根據(jù)哪種策略來設(shè)置的邏輯在方法initTableFields(clazz, globalConfig, tableInfo)當(dāng)中——
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(); } /* 初始化表名相關(guān) */ initTableName(clazz, globalConfig, tableInfo); /* 初始化字段相關(guān) */ initTableFields(clazz, globalConfig, tableInfo); /* 放入緩存 */ TABLE_INFO_CACHE.put(clazz.getName(), tableInfo); /* 緩存 Lambda 映射關(guān)系 */ LambdaUtils.createCache(clazz, tableInfo); return tableInfo; }
在初始化字段相關(guān)的initTableFields方法里,會(huì)判斷是否有@TableId 注解,如果沒有,就執(zhí)行initTableIdWithoutAnnotation方法,連續(xù)前文提到的,如果實(shí)體類id沒有加@TableId(value = "id", type = IdType.INPUT),那么就會(huì)取默認(rèn)的主鍵策略。這里的判斷是否有@TableId 注解,就是判斷是否需要取默認(rèn)的主鍵策略,至于具體是如何設(shè)置默認(rèn)主鍵的,我們可以直接進(jìn)入到initTableIdWithoutAnnotation方法當(dāng)中。
public static void initTableFields(Class<?> clazz, GlobalConfig globalConfig, TableInfo tableInfo) { /* 數(shù)據(jù)庫全局配置 */ GlobalConfig.DbConfig dbConfig = globalConfig.getDbConfig(); List<Field> list = getAllFields(clazz); // 標(biāo)記是否讀取到主鍵 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 實(shí)體類 * @return true 繼續(xù)下一個(gè)屬性判斷,返回 continue; */ private static boolean initTableIdWithoutAnnotation(GlobalConfig.DbConfig dbConfig, TableInfo tableInfo, Field field, Class<?> clazz) { //獲取實(shí)體類字段名 String column = field.getName(); if (dbConfig.isCapitalMode()) { column = column.toUpperCase(); } //當(dāng)字段名為id if (DEFAULT_ID_NAME.equalsIgnoreCase(column)) { if (StringUtils.isEmpty(tableInfo.getKeyColumn())) { tableInfo.setKeyRelated(checkRelated(tableInfo.isUnderCamel(), field.getName(), column)) //設(shè)置表策略 .setIdType(dbConfig.getIdType()) .setKeyColumn(column) .setKeyProperty(field.getName()) .setClazz(field.getDeclaringClass()); return true; } else { throwExceptionId(clazz); } } return false; }
Debug到這里,可以看到,如果沒有 @TableId 注解顯示設(shè)置主鍵策略情況下,默認(rèn)設(shè)置的是 ID_WORKER(3),即會(huì)根據(jù)雪花算法生成19位數(shù)字,long類型。
可以進(jìn)一步發(fā)現(xiàn),這里的 dbConfig是GlobalConfig.DbConfig實(shí)例,進(jìn)入到DbConfig類,可以看到原來實(shí)體類映射的數(shù)據(jù)庫設(shè)置在這里,主鍵類型默認(rèn)是IdType.ID_WORKER。
@Data public static class DbConfig { /** * 數(shù)據(jù)庫類型 */ private DbType dbType = DbType.OTHER; /** * 主鍵類型(默認(rèn) ID_WORKER) */ private IdType idType = IdType.ID_WORKER; /** * 表名前綴 */ private String tablePrefix; /** * 表名、是否使用下劃線命名(默認(rèn) true:默認(rèn)數(shù)據(jù)庫表下劃線命名) */ private boolean tableUnderline = true; /** * String 類型字段 LIKE */ private boolean columnLike = false; /** * 大寫命名 */ private boolean capitalMode = false; /** * 表關(guān)鍵詞 key 生成器 */ private IKeyGenerator keyGenerator; /** * 邏輯刪除全局值(默認(rèn) 1、表示已刪除) */ private String logicDeleteValue = "1"; /** * 邏輯未刪除全局值(默認(rèn) 0、表示未刪除) */ private String logicNotDeleteValue = "0"; /** * 字段驗(yàn)證策略 */ private FieldStrategy fieldStrategy = FieldStrategy.NOT_NULL; }
至于如何生成雪花算法id,這里就不一一詳細(xì)介紹,具體邏輯是在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()); } } } ...... }
前邊提到,默認(rèn)的主鍵策略是IdType.ID_WORKER,這里有一個(gè)判斷tableInfo.getIdType() == IdType.ID_WORKER,對代碼Debug可以看到,metaObject的setValue(tableInfo.getKeyProperty(), IdWorker.getId())代碼的作用,是對注解id進(jìn)行了值填充。
填充的值為IdWorker.getId()返回的1468970800437465089,剛好是19位長度,這就意味著,這里產(chǎn)生的id值,就是我們最后要找的。
IdWorker.getId()實(shí)現(xiàn)本質(zhì),正好是基于Snowflake實(shí)現(xiàn)64位自增ID算法,而Snowflake,正是引用了雪花算法——
/** * <p> * 高效GUID產(chǎn)生算法(sequence),基于Snowflake實(shí)現(xiàn)64位自增ID算法。 <br> * 優(yōu)化開源項(xiàng)目 http://git.oschina.net/yu120/sequence * </p> * * @author hubin * @since 2016-08-01 */ public class IdWorker { /** * 主機(jī)和進(jìn)程的機(jī)器碼 */ 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); } }
到此這篇關(guān)于Mybatis-Plus默認(rèn)主鍵策略導(dǎo)致自動(dòng)生成19位長度主鍵id的坑的文章就介紹到這了,更多相關(guān)Mybatis-Plus id主鍵生成內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot中對應(yīng)2.0.x版本的Redis配置詳解
這篇文章主要為大家介紹了SpringBoot中對應(yīng)2.0.x版本的Redis配置詳解,文中的實(shí)現(xiàn)步驟講解詳細(xì),感興趣的小伙伴們可以了解一下2022-06-06java之向linux文件夾下寫文件無權(quán)限的問題
這篇文章主要介紹了java之向linux文件夾下寫文件無權(quán)限的問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09IDEA導(dǎo)入Eclipse項(xiàng)目的方法步驟(圖文教程)
這篇文章主要介紹了IDEA導(dǎo)入Eclipse項(xiàng)目的方法步驟,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03使用IDEA如何打包發(fā)布SpringBoot并部署到云服務(wù)器
這篇文章主要介紹了使用IDEA如何打包發(fā)布SpringBoot并部署到云服務(wù)器問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12PowerShell用戶認(rèn)證Function實(shí)例代碼
這篇文章主要介紹了PowerShell用戶認(rèn)證Function的資料,并附實(shí)例代碼,幫助大家學(xué)習(xí)理解,有需要的小伙伴可以參考下2016-09-09