Mybatis Plus 3.X版本的insert填充自增id的IdType.ID_WORKER策略原始碼分析

朱季谦發表於2024-07-09

總結/朱季謙

某天同事突然問我,你知道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);
    
    ......
}

每個欄位都有各自含義,說明如下:

  1. AUTO(0): 用於資料庫ID自增的策略,主要用於資料庫表的主鍵,在插入資料時,資料庫會自動為新插入的記錄分配一個唯一遞增ID。
  2. NONE(1): 表示未設定主鍵型別,存在某些情況下不需要主鍵,或者主鍵由其他方式生成。
  3. INPUT(2): 表示使用者輸入ID,允許使用者自行指定ID值,例如前端傳過來的物件id=1,就會根據該自行定義的id=1當作ID值;
  4. ID_WORKER(3): 表示全域性唯一ID,使用的是idWorker演算法生成的ID,這是一種雪花演算法的改進。
  5. UUID(4): 表示全域性唯一ID,使用的是UUID(Universally Unique Identifier)演算法。
  6. 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,還沒有任何值——

image

執行到insert的時候,底層會執行一個動態代理,最終透過動態代理,執行DefaultSqlSession類的insert方法,可以看到,insert方法裡,最終呼叫的是一個update方法。

image

在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方法裡——

image

這裡的BaseExecutor是mybatis的核心元件,它是Executor 介面的一個具體實現,提供了實際資料的增刪改查操作功能。在 MyBatis 中,基於BaseExecutor擴充套件了以下三種基本執行器類:

  1. SimpleExecutor:這是最簡單的執行器型別,它對每個資料庫CURD操作都建立一個新的 Statement 物件。如果應用程式執行大量的資料庫操作,這種型別的執行器可能會產生大量的開銷,因為它不支援 Statement 重用。
  2. ReuseExecutor:這種執行器型別會嘗試重用 Statement 物件。它在處理多個資料庫操作時,會嘗試使用相同的 Statement 物件,從而減少建立 Statement 物件的次數,提高效能。
  3. BatchExecutor:這種執行器型別用於批次操作,它會在內部快取所有的更新操作,然後在適當的時候一次性執行它們,適合批次插入或更新操作的場景,可以顯著提高效能。

除了這三種基本的執行器型別,MyBatis 還提供了其他一些執行器,這裡暫時不展開討論。

在本文中,執行到doUpdate(ms, parameter)時,會預設跳轉到SimpleExecutor執行器的doUpdate方法裡。注意我標註出來的這兩行程式碼,自動填充插入ID策略的邏輯,就是在這兩行程式碼當中——

image

先來看第一行程式碼,從類名就可以看出,這裡建立裡一個實現StatementHandler介面的物件,這個StatementHandler介面專門用來處理SQL語句的介面。從這裡就可以看出,透過建立這個物件,可以專門用來處理SQL相關語句操作,例如,對引數的設定,更具體一點,可以對引數id進行自定義設定等功能。

實現StatementHandler介面有很多類,那麼,具體需要建立哪個物件呢?

跟著程式碼一定進入到RoutingStatementHandler類的RoutingStatementHandler方法當中,可以看到,這裡有一個switch,debug到這一步,最終建立的是一個PreparedStatementHandler物件——

image

進入到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,相當還沒有值。

image

繼續往下debug,因為是insert語句,故而會進入到ms.getSqlCommandType() == SqlCommandType.INSERT方法裡,將isFill賦值true,isInsert賦值true,這兩個分別表示是否需要填充以及是否插入。由此可見,它將會執行if (isFill) {}裡的邏輯——

image

在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。

image

相關文章