總結/朱季謙
某天同事突然問我,你知道Mybatis Plus的insert方法,插入數據後自增id是如何自增的嗎?
我愣了一下,腦海裏只想到,當在POJO類的id設置一個自增策略後,例如@TableId(value = "id",type = IdType.ID_WORKER)的註解策略時,就能實現在每次數據插入數據庫時,實現id的自增,例如以下形式——
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "用户對象")
@TableName("user_info")
public class UserInfo {
@ApiModelProperty(value = "用户ID", name = "id")
@TableId(value = "id",type = IdType.ID_WORKER)
private Integer id;
@ApiModelProperty(value = "用户姓名", name = "userName")
private String userName;
@ApiModelProperty(value = "用户年齡", name = "age")
private int age;
}
但是,説實話,我一直都沒能理解,這個註解策略實現id自增的底層原理究竟是怎樣的?
帶着這樣的疑惑,我開始研究了一番Mybatis Plus的insert自增id的策略源碼,並將其寫成了本文。
先來看一下Mybatis Plus生成id的自增策略,可以通過枚舉IdType設置以下數種策略——
@Getter
public enum IdType {
/**
* 數據庫ID自增
*/
AUTO(0),
/**
* 該類型為未設置主鍵類型
*/
NONE(1),
/**
* 用户輸入ID
* 該類型可以通過自己註冊自動填充插件進行填充
*/
INPUT(2),
/* 以下3種類型、只有當插入對象ID 為空,才自動填充。 */
/**
* 全局唯一ID (idWorker)
*/
ID_WORKER(3),
/**
* 全局唯一ID (UUID)
*/
UUID(4),
/**
* 字符串全局唯一ID (idWorker 的字符串表示)
*/
ID_WORKER_STR(5);
......
}
每個字段都有各自含義,説明如下:
AUTO(0): 用於數據庫ID自增的策略,主要用於數據庫表的主鍵,在插入數據時,數據庫會自動為新插入的記錄分配一個唯一遞增ID。NONE(1): 表示未設置主鍵類型,存在某些情況下不需要主鍵,或者主鍵由其他方式生成。INPUT(2): 表示用户輸入ID,允許用户自行指定ID值,例如前端傳過來的對象id=1,就會根據該自行定義的id=1當作ID值;ID_WORKER(3): 表示全局唯一ID,使用的是idWorker算法生成的ID,這是一種雪花算法的改進。UUID(4): 表示全局唯一ID,使用的是UUID(Universally Unique Identifier)算法。ID_WORKER_STR(5): 表示字符串形式的全局唯一ID,這是idWorker生成的ID的字符串表示形式,便於在需要字符串ID的場景下使用。
接下來,讓我們跟着源碼看一下,究竟是如何基於這些ID策略做id自增的,本文主要以ID_WORKER(3)策略id來追蹤。
先從插入insert方法開始。
基於前文創建的UserInfo類,我們寫一個test的方法,用於追蹤insert方法——
@Test
public void test(){
UserInfo userInfo = new UserInfo();
userInfo.setUserName("用户名");
userInfo.setAge(1);
userInfoMapper.insert(userInfo);
}
可以看到,此時的id=0,還沒有任何值——
執行到insert的時候,底層會執行一個動態代理,最終通過動態代理,執行DefaultSqlSession類的insert方法,可以看到,insert方法裏,最終調用的是一個update方法。
在mybatis中,無論是新增insert或者更新update,其底層都是統一調用DefaultSqlSession的update方法——
@Override
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
執行到executor.update(ms, wrapCollection(parameter))方法時,會跳轉到BaseExecutor的update方法裏——
這裏的BaseExecutor是mybatis的核心組件,它是Executor 接口的一個具體實現,提供了實際數據的增刪改查操作功能。在 MyBatis 中,基於BaseExecutor擴展了以下三種基本執行器類:
- SimpleExecutor:這是最簡單的執行器類型,它對每個數據庫CURD操作都創建一個新的
Statement對象。如果應用程序執行大量的數據庫操作,這種類型的執行器可能會產生大量的開銷,因為它不支持Statement重用。 - ReuseExecutor:這種執行器類型會嘗試重用
Statement對象。它在處理多個數據庫操作時,會嘗試使用相同的Statement對象,從而減少創建Statement對象的次數,提高性能。 - BatchExecutor:這種執行器類型用於批量操作,它會在內部緩存所有的更新操作,然後在適當的時候一次性執行它們,適合批量插入或更新操作的場景,可以顯著提高性能。
除了這三種基本的執行器類型,MyBatis 還提供了其他一些執行器,這裏暫時不展開討論。
在本文中,執行到doUpdate(ms, parameter)時,會默認跳轉到SimpleExecutor執行器的doUpdate方法裏。注意我標註出來的這兩行代碼,自動填充插入ID策略的邏輯,就是在這兩行代碼當中——
先來看第一行代碼,從類名就可以看出,這裏創建裏一個實現StatementHandler接口的對象,這個StatementHandler接口專門用來處理SQL語句的接口。從這裏就可以看出,通過創建這個對象,可以專門用來處理SQL相關語句操作,例如,對參數的設置,更具體一點,可以對參數id進行自定義設置等功能。
實現StatementHandler接口有很多類,那麼,具體需要創建哪個對象呢?
跟着代碼一定進入到RoutingStatementHandler類的RoutingStatementHandler方法當中,可以看到,這裏有一個switch,debug到這一步,最終創建的是一個PreparedStatementHandler對象——
進入到PreparedStatementHandler方法當中,可以看到會通過super調用創建其父類的構造器方法——
public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
}
從super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql)方法進去,到父類的BaseStatementHandler裏,這裏面有一行很關鍵的代碼 this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql),這是一個MyBatis內部的接口或實現類的實例,用於處理SQL的參數映射和傳遞。
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
this.configuration = mappedStatement.getConfiguration();
this.executor = executor;
this.mappedStatement = mappedStatement;
this.rowBounds = rowBounds;
this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
this.objectFactory = configuration.getObjectFactory();
if (boundSql == null) { // issue #435, get the key before calculating the statement
generateKeys(parameterObject);
boundSql = mappedStatement.getBoundSql(parameterObject);
}
this.boundSql = boundSql;
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}
進入到configuration.newParameterHandler(mappedStatement, parameterObject, boundSql)代碼裏,可以看到這裏通過createParameterHandler方法創建一個實現ParameterHandler接口的對象,至於這個對象是什麼,可以接着往下去。
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
最終來到MybatisXMLLanguageDriver類的createParameterHandler方法,可以看到,創建的這個實現ParameterHandler接口的對象,是這個MybatisDefaultParameterHandler。
public class MybatisXMLLanguageDriver extends XMLLanguageDriver {
@Override
public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject,
BoundSql boundSql) {
/* 使用自定義 ParameterHandler */
return new MybatisDefaultParameterHandler(mappedStatement, parameterObject, boundSql);
}
}
繼續跟進去,可以看到構造方法裏,有一個processBatch(mappedStatement, parameterObject)方法,我們要找的填充自增id的IdType.ID_WORKER策略實現,其實就在這個processBatch方法裏。
public MybatisDefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
super(mappedStatement, processBatch(mappedStatement, parameterObject), boundSql);
this.mappedStatement = mappedStatement;
this.configuration = mappedStatement.getConfiguration();
this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
this.parameterObject = parameterObject;
this.boundSql = boundSql;
}
至於processBatch(mappedStatement, parameterObject)中的兩個參數分別是什麼,debug就知道了,mappedStatement是一個存儲執行語句相關的Statement對象,而parameterObject則是需要插入數據庫的對象數據,此時id仍然是默認0,相當還沒有值。
繼續往下debug,因為是insert語句,故而會進入到ms.getSqlCommandType() == SqlCommandType.INSERT方法裏,將isFill賦值true,isInsert賦值true,這兩個分別表示是否需要填充以及是否插入。由此可見,它將會執行if (isFill) {}裏的邏輯——
在if(isFill)方法當中,最重要的是populateKeys(metaObjectHandler, tableInfo, ms, parameterObject, isInsert);這個方法,這個方法就是根據不同的id策略,去生成不同的id值,然後填充到id字段裏,最終插入到數據庫當中。而我們要找的最終方法,正是在這裏面——
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());
}
}
}
if (metaObjectHandler != null) {
if (isInsert && metaObjectHandler.openInsertFill()) {
// 插入填充
metaObjectHandler.insertFill(metaObject);
} else if (!isInsert) {
// 更新填充
metaObjectHandler.updateFill(metaObject);
}
}
return metaObject.getOriginalObject();
}
例如,我們設置的id策略是這個 @TableId(value = "id",type = IdType.ID_WORKER),當代碼執行到populateKeys方法裏時,就會判斷是否為 IdType.ID_WORKER策略,如果是,就會執行對應的生存id的方法。這裏的IdWorker.getId()就是獲取一個唯一ID,然後賦值給tableInfo.getKeyProperty(),這個tableInfo.getKeyProperty()正是user_info的對象id。