1. 程式人生 > >mybatis的插入與批量插入的返回ID的原理

mybatis的插入與批量插入的返回ID的原理

目錄

  • 背景
  • 底層呼叫方法
  • 單個物件插入
  • 列表批量插入
  • 完成

背景

最近正在整理之前基於mybatis的半ORM框架。原本的框架底層類ORM操作是通過StringBuilder的append拼接的,這次打算用JsqlParser重寫一遍,一來底層不會存在太多的文字拼接,二來基於其他開源包維護難度會小一些,最後還可以整理一下原有的冗餘方法。
這兩天整理insert相關的方法,在將物件插入資料庫後,期望是要返回完整物件,並且包含實際的資料庫id。
基礎相關框架為:spring、mybatis、hikari。

底層呼叫方法

最底層的做法實際上很直白,就是利用mybatis執行最簡單的sql語句,給上程式碼。

@Repository("baseDao")
public class BaseDao extends SqlSessionDaoSupport {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 最大的單次批量插入的數量
     */
    private static final int MAX_BATCH_SIZE = 10000;

    @Override
    @Autowired
    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        super.setSqlSessionFactory(sqlSessionFactory);
    }


    /**
     * 根據sql方法名稱和物件插入資料庫
     */
    public Object insert(String sqlName, Object obj) throws SQLException {
        return getSqlSession().insert(sqlName, obj); // 此處直接執行傳入的xml中對應的sql id,以及引數

    }
}

單個物件插入

java程式碼

    /**
     * 簡單插入實體物件
     *
     * @param entity 實體物件
     * @throws SQLException
     */
    public <T extends BaseEntity> T insertEntity(T entity) throws SQLException {
        Insert insert = new Insert();
        insert.setTable(new Table(entity.getClass().getSimpleName()));
        insert.setColumns(JsqlUtils.getColumnNameFromEntity(entity.getClass()));
        insert.setItemsList(JsqlUtils.getAllColumnValueFromEntity(entity,insert.getColumns()));

        Map<String, Object> param = new HashMap<>();
        param.put("baseSql", insert.toString());
        param.put("entity", entity);
        this.insert("BaseDao.insertEntity", param);

        return entity;
    }

xml程式碼

 <insert id="insertEntity" parameterType="java.util.Map" useGeneratedKeys="true" keyProperty="entity.id">
  ${baseSql}
 </insert>

其他的就不多說了,這裡針對如何返回已經入庫的id給個說明。
在xml的 insert 標籤中,設定 keyProperty 為 對應物件的id欄位,和 insert(sqlName, obj) 這個方法中的 obj 是對應的。
這裡一般有兩種情況:

直接儲存實體的物件作為引數傳入(給虛擬碼示例)

SaveObject saveObject = new SaveObject(); // SaveObject中包含欄位soid,作為自增id
saveObject.setName("my name");
saveObject.setNums(2);

getSqlSession().insert("saveObject.insert",saveObject);

這種情況實際就是傳入了待儲存的物件。這時候我們的xml應該這樣

 <insert id="insert" parameterType="SaveObject " useGeneratedKeys="true" keyProperty="soid">
  insert into save_object (`name`,nums) values (#{names},#{nums})
 </insert>

這裡我們傳入了SaveObject實體物件作為引數,所以我們的 keyProperty 就是parameter的id對應的欄位,在這裡就是 soid 。

多個物件,實體物件作為其中一個物件傳入

        Map<String, Object> param = new HashMap<>();
        param.put("baseSql", insert.toString());
        param.put("entity", entity); // 此處對應實體作為map的第二個引數傳入
        this.insert("BaseDao.insertEntity", param);
 <insert id="insertEntity" parameterType="java.util.Map" useGeneratedKeys="true" keyProperty="entity.id">
  ${baseSql}
 </insert>

這裡也是比較容易理解,當傳入引數是Map時,我們的 keyProperty 對應方式就是先從Map中讀出對應value,再指向 value中的id欄位。

列表批量插入

批量插入資料有兩種做法,一種是多次呼叫單個insert方法,這種效率較低就不說了。另外一種是 insert into table (cols) values (val1),(val2),(val3) 這樣批量插入。
到mybatis中,也是分為兩種

直接儲存實體的物件作為引數傳入(給虛擬碼示例)

SaveObject saveObject1 = new SaveObject(); // SaveObject中包含欄位soid,作為自增id
saveObject1.setName("my name");
saveObject1.setNums(2);

SaveObject saveObject2 = new SaveObject(); // SaveObject中包含欄位soid,作為自增id
saveObject2.setName("my name");
saveObject2.setNums(2);

List<SaveObject> saveObjects = new ArrayList<SaveObject>();
saveObjects.add(saveObjects1);
saveObjects.add(saveObjects2);

getSqlSession().insert("saveObject.insertList",saveObjects);

這種情況實際就是傳入了待儲存的物件。這時候我們的xml應該這樣

 <insert id="insertList" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="soid">
        insert into save_object (`name`,nums) values
        <foreach collection="list" index="index" item="saveObject" separator=",">  
            (#{saveObject.numsnames}, #{saveObject.nums})  
        </foreach>
 </insert>

多個物件,實體物件作為其中一個物件傳入

本文的重點來了,我自己卡在這裡很久,反覆除錯才摸清邏輯。接下來就順著mybatis的思路來講,只會講id生成相關的,其他的流程就不多說了。

先看這個類:org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator (很多程式碼我用...代替了,不是特別重要,放在還佔地方)

  /**
   * 這個方法是在執行完插入語句之後處理的,兩個關鍵引數 
   * 1. MappedStatement ms 裡面包含了我們的 keyProperty
   * 2. Object parameter 就是我們inser方法傳入的引數
   */
  @Override
  public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
    processBatch(ms, stmt, parameter);
  }

  public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
    final String[] keyProperties = ms.getKeyProperties();
    if (keyProperties == null || keyProperties.length == 0) {
      return;
    }
    try (ResultSet rs = stmt.getGeneratedKeys()) {
      final Configuration configuration = ms.getConfiguration();
      if (rs.getMetaData().getColumnCount() >= keyProperties.length) {
        Object soleParam = getSoleParameter(parameter);
        if (soleParam != null) {
          assignKeysToParam(configuration, rs, keyProperties, soleParam);
        } else {
          assignKeysToOneOfParams(configuration, rs, keyProperties, (Map<?, ?>) parameter);
        }
      }
    } catch (Exception e) {
      ...
    }
  }

  protected void assignKeysToOneOfParams(final Configuration configuration, ResultSet rs, final String[] keyProperties,
      Map<?, ?> paramMap) throws SQLException {
    // Assuming 'keyProperty' includes the parameter name. e.g. 'param.id'.
    int firstDot = keyProperties[0].indexOf('.');
    if (firstDot == -1) {
      ...
    }
    String paramName = keyProperties[0].substring(0, firstDot);
    Object param;
    if (paramMap.containsKey(paramName)) {
      param = paramMap.get(paramName);
    } else {
     ...
    }
    ...
    assignKeysToParam(configuration, rs, modifiedKeyProperties, param);
  }

  private void assignKeysToParam(final Configuration configuration, ResultSet rs, final String[] keyProperties,
      Object param)
      throws SQLException {
    final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    final ResultSetMetaData rsmd = rs.getMetaData();
    // Wrap the parameter in Collection to normalize the logic.
    Collection<?> paramAsCollection = null;
    if (param instanceof Object[]) {
      paramAsCollection = Arrays.asList((Object[]) param);
    } else if (!(param instanceof Collection)) {
      paramAsCollection = Arrays.asList(param);
    } else {
      paramAsCollection = (Collection<?>) param;
    }
    TypeHandler<?>[] typeHandlers = null;
    for (Object obj : paramAsCollection) {
      if (!rs.next()) {
        break;
      }
      MetaObject metaParam = configuration.newMetaObject(obj);
      if (typeHandlers == null) {
        typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd);
      }
      populateKeys(rs, metaParam, keyProperties, typeHandlers);
    }
  }

利用這個程式碼先解釋一下上一節 直接儲存實體的物件作為引數傳入 為什麼id會被更新至實體內的soid欄位。
上一節的是 keyProperty="soid"
我們來看19行的程式碼Object soleParam = getSoleParameter(parameter);,當我們傳入的物件是List的時候 soleParam != null,所以 直接執行 assignKeysToParam 方法。
注意64和65行

for (Object obj : paramAsCollection) {
if (!rs.next()) {

paramAsCollection 是將我們傳入的轉換為 Collection 型別,所以這裡是迴圈我們的給定實體列表引數。
rs就是ResultSet,就是插入之後的結果集。 rs.next()就是指標指向下一條記錄,所以實際上這裡是同步迴圈,將結果集中的id直接設定到我們給的實體列表中

我們現在來看看多引數插入是會有什麼問題。
Java方法:

    /**
     * 簡單批量插入實體物件
     *
     * @param entitys
     * @throws SQLException
     */
    public List insertEntityList(List<? extends BaseEntity> entitys) throws SQLException {
        if (entitys == null || entitys.size() == 0) {
            return null;
        }

        Insert insert = new Insert();
        insert.setTable(new Table(entitys.get(0).getClass().getSimpleName()));
        insert.setColumns(JsqlUtils.getColumnNameFromEntity(entitys.get(0).getClass()));
        MultiExpressionList multiExpressionList = new MultiExpressionList();
        entitys.stream().map(e -> JsqlUtils.getAllColumnValueFromEntity(e,insert.getColumns())).forEach(e -> multiExpressionList.addExpressionList(e));
        insert.setItemsList(multiExpressionList);

        Map<String, Object> param = new HashMap<>();
        param.put("baseSql", insert.toString());
        param.put("list", entitys);
        this.insert("BaseDao.insertEntityList", param);
        return entitys;
    }

Xml:

 <insert id="insertEntityList" parameterType="java.util.Map" useGeneratedKeys="true" keyProperty="id">
  ${baseSql}
 </insert>

會有什麼問題??根據這樣的xml,最後的結果是我們傳入的map中會多一個key 叫 “id”,裡面存的是一個插入的實體的id。
因為根據原始碼 Map並非 Collection 型別,所以會做為只有一個元素的陣列傳入,在剛才同步迴圈的地方就只會迴圈一次,把結果集中第一條資料的id放進map中,迴圈就結束了。

怎麼解決呢??
解決的方法就在 assignKeysToOneOfParams 這個方法,方法名其實已經說了,將主鍵賦給其中一個引數,這裡確實也是取了其中的一個引數進行賦值主鍵。所以我們只要能夠跳轉到這個方法就好。所以需要滿足 getSoleParameter(parameter) == null ,點進程式碼看

private Object getSoleParameter(Object parameter) {
    if (!(parameter instanceof ParamMap || parameter instanceof StrictMap)) {
      return parameter;
    }
    Object soleParam = null;
    for (Object paramValue : ((Map<?, ?>) parameter).values()) {
      if (soleParam == null) {
        soleParam = paramValue;
      } else if (soleParam != paramValue) {
        soleParam = null;
        break;
      }
    }
    return soleParam;
  }

要返回null,條件是這樣:

  1. 引數是ParamMap或者 StrictMap
  2. 引數大於兩個,且第一個和後面任意一個不相等

所以解決方案出爐,很簡單,只需要改動程式碼兩個地方即可。

    /**
     * 簡單批量插入實體物件
     *
     * @param entitys
     * @throws SQLException
     */
    public List insertEntityList(List<? extends BaseEntity> entitys) throws SQLException {
        if (entitys == null || entitys.size() == 0) {
            return null;
        }

        Insert insert = new Insert();
        insert.setTable(new Table(entitys.get(0).getClass().getSimpleName()));
        insert.setColumns(JsqlUtils.getColumnNameFromEntity(entitys.get(0).getClass()));
        MultiExpressionList multiExpressionList = new MultiExpressionList();
        entitys.stream().map(e -> JsqlUtils.getAllColumnValueFromEntity(e,insert.getColumns())).forEach(e -> multiExpressionList.addExpressionList(e));
        insert.setItemsList(multiExpressionList);

        Map<String, Object> param = new MapperMethod.ParamMap<>(); // 這裡替換為 MapperMethod.ParamMap 型別
        param.put("baseSql", insert.toString());
        param.put("list", entitys);
        this.insert("BaseDao.insertEntityList", param);
        return entitys;
    }

Xml:

 <insert id="insertEntityList" parameterType="java.util.Map" useGeneratedKeys="true" keyProperty="list.id">  <!-- 這裡是map中的key.實體id -->
  ${baseSql}
 </insert>

完成