動態

詳情 返回 返回

Mybatis Plus 3.X版本的insert填充自增id的IdType.ID_WORKER策略源碼分析 - 動態 詳情

總結/朱季謙

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

Add a new 評論

Some HTML is okay.