總結/朱季謙
某天同事突然問我,你知道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。