MyBatis的Insert操作自增主鍵的實現,Mysql協議與JDBC實現

莊傑森發表於2019-04-27

MyBatis的Insert操作自增主鍵的實現,Mysql協議與JDBC實現

我github部落格地址

背景

Mybatis中配置了Insert 操作時,新增了 useGeneratedKeys = true 的配置,就可以在插入的model完成後獲取到主鍵的值,用於業務
1.有些場景,插入表單完需要返回id作,後續操作
複製程式碼

例子



/**
 * @param
 * @Author: zhuangjiesen
 * @Description:
 * @Date: Created in 2018/6/19
 */
@Mapper
public interface UserMapper {

    @Insert("INSERT INTO `dragsunweb`.`user`(`user_id`, `user_name`, `user_password`) VALUES (#{id}, #{name}, #{password})")
    @Options(useGeneratedKeys = true)
    public Integer insert(User user);

}
複製程式碼

之後 user.getId() , 可以獲取到對應的主鍵

原理/原始碼

Mybatis都是講mappper類掃描後,通過組裝(BeanDefinition)成 MapperFactoryBean.class,
然後最後通過MapperProxy.class(代理) 將mapper例項化,
給人的體驗就是可以直接呼叫mapper運算元據庫
一個insert執行順序(一步步打斷點):
1.UserMapper.insert();
2.MapperProxy.invoke();
3.mapperMethod.execute();
4.DefaultSqlSession.insert();
5.BaseExecutor.update()
6.SimpleExecutor.doUpdate();
7.StatementHandler.update();
8.PreparedStatementHandler.update();
在8中就可以發現執行完sql 後,呼叫的KeyGenerator
複製程式碼

PreparedStatementHandler.update()程式碼片段:


public class PreparedStatementHandler extends BaseStatementHandler {


  @Override
  public int update(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    int rows = ps.getUpdateCount();
    Object parameterObject = boundSql.getParameterObject();
    //呼叫 KeyGenerator
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    return rows;
  }


}

複製程式碼

KeyGenerator.class , 在每個sql初始化時,在mybatis會初始化成 MappedStatement.class例項

/**
 * @author Clinton Begin
 */
public interface KeyGenerator {

  void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);

  void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);

}

複製程式碼

Tips:

這裡有個問題其實 KeyGenerator.class 可以用來作攔截器的工作,或者擴充,
可是在 MappedStatement.class的內部構造類 Builder.class 中,只能預設使用2個類
複製程式碼

其實這個類Mybatis中預設只有2個:

NoKeyGenerator.class (如果不配置useKeyGenerator)

Jdbc3KeyGenerator.class (本次部落格的主角)

複製程式碼

Jdbc3KeyGenerator.class 原始碼解析,核心 processBatch() 方法,也挺簡單的,就是類似攔截器,在prepareStatment執行完作了操作


/**
 * @author Clinton Begin
 * @author Kazuki Shimizu
 */
public class Jdbc3KeyGenerator implements KeyGenerator {

  /**
   * A shared instance.
   * @since 3.4.3
   */
  public static final Jdbc3KeyGenerator INSTANCE = new Jdbc3KeyGenerator();

  @Override
  public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
    // do nothing
  }

  @Override
  public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
    processBatch(ms, stmt, getParameters(parameter));
  }

  public void processBatch(MappedStatement ms, Statement stmt, Collection<Object> parameters) {
    ResultSet rs = null;
    try {
    //這裡獲取到執行的sql結果 , 獲取主鍵的欄位值(實體類的對映),然後set進去很簡單
      rs = stmt.getGeneratedKeys();
      final Configuration configuration = ms.getConfiguration();
      final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
      final String[] keyProperties = ms.getKeyProperties();
      final ResultSetMetaData rsmd = rs.getMetaData();
      TypeHandler<?>[] typeHandlers = null;
      if (keyProperties != null && rsmd.getColumnCount() >= keyProperties.length) {
        for (Object parameter : parameters) {
          // there should be one row for each statement (also one for each parameter)
          if (!rs.next()) {
            break;
          }
          final MetaObject metaParam = configuration.newMetaObject(parameter);
          if (typeHandlers == null) {
            typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd);
          }
          //獲取插入的物件引數,把主鍵值插入回物件屬性
          populateKeys(rs, metaParam, keyProperties, typeHandlers);
        }
      }
    } catch (Exception e) {
      throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
    } finally {
      if (rs != null) {
        try {
          rs.close();
        } catch (Exception e) {
          // ignore
        }
      }
    }
  }

  private Collection<Object> getParameters(Object parameter) {
    Collection<Object> parameters = null;
    if (parameter instanceof Collection) {
      parameters = (Collection) parameter;
    } else if (parameter instanceof Map) {
      Map parameterMap = (Map) parameter;
      if (parameterMap.containsKey("collection")) {
        parameters = (Collection) parameterMap.get("collection");
      } else if (parameterMap.containsKey("list")) {
        parameters = (List) parameterMap.get("list");
      } else if (parameterMap.containsKey("array")) {
        parameters = Arrays.asList((Object[]) parameterMap.get("array"));
      }
    }
    if (parameters == null) {
      parameters = new ArrayList<Object>();
      parameters.add(parameter);
    }
    return parameters;
  }

  private TypeHandler<?>[] getTypeHandlers(TypeHandlerRegistry typeHandlerRegistry, MetaObject metaParam, String[] keyProperties, ResultSetMetaData rsmd) throws SQLException {
    TypeHandler<?>[] typeHandlers = new TypeHandler<?>[keyProperties.length];
    for (int i = 0; i < keyProperties.length; i++) {
      if (metaParam.hasSetter(keyProperties[i])) {
        TypeHandler<?> th;
        try {
          Class<?> keyPropertyType = metaParam.getSetterType(keyProperties[i]);
          th = typeHandlerRegistry.getTypeHandler(keyPropertyType, JdbcType.forCode(rsmd.getColumnType(i + 1)));
        } catch (BindingException e) {
          th = null;
        }
        typeHandlers[i] = th;
      }
    }
    return typeHandlers;
  }

  private void populateKeys(ResultSet rs, MetaObject metaParam, String[] keyProperties, TypeHandler<?>[] typeHandlers) throws SQLException {
    for (int i = 0; i < keyProperties.length; i++) {
      String property = keyProperties[i];
      TypeHandler<?> th = typeHandlers[i];
      if (th != null) {
        Object value = th.getResult(rs, i + 1);
        metaParam.setValue(property, value);
      }
    }
  }

}

複製程式碼

問題

這裡就會發現其實insert操作完獲取主鍵直接可以通過 ResultSet 獲取到。而 KeyGenerator.class其實就是個設計模式封裝而已。

通過斷點發現JDBC 的 ResultSetImpl.class (ResultSet.class 介面實現) ,其實是有欄位 resultId 、 updateId (mysql註釋: /** Value generated for AUTO_INCREMENT columns */ -> 就是自增主鍵的值 )

還有個欄位是 updateCount (mysql註釋: /** How many rows were affected by UPDATE/INSERT/DELETE? */ -> 就是影響行數 )

複製程式碼

由此推斷不是mybatis實現的獲取自增主鍵的功能,而是JDBC原生實現了這個功能

然後就用過原始的JDBC程式測試:


    public static void main(String[] args ) throws Exception {
        Connection conn = getConn();
        int i = 0;
        String sql = " INSERT INTO `dragsunweb`.`user` ( `user_id`, `user_name`, `user_password` ) VALUES ( null , 'hahaha',  '2233' )";
        PreparedStatement pstmt;
        try {
            pstmt = (PreparedStatement) conn.prepareStatement(sql);
            long id = 0L;
        
            //這裡可以獲取執行行數
            i = pstmt.executeUpdate();
            //這裡可以檢視到最後插入的主鍵值
            id = pstmt.getLastInsertID();
            System.out.println(String.format("1. id : %d , rows : %d " , id  , i));

            i = pstmt.executeUpdate();
            id = pstmt.getLastInsertID();
            System.out.println(String.format("2. id : %d , rows : %d " , id  , i));
            pstmt.close();
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }


------
結果就是如我所說,列印出了對應的主鍵值
複製程式碼

結果就是如我所說,列印出了對應的主鍵值,驗證了我所說,mybatis只是封裝了這個功能

現在的困惑是:

1.是1條sql(insert)就可以獲取到主鍵的值嗎,平時在視覺化工具中執行的時候只能獲取影響行數;
2.通過mysql配置或是多條語句
複製程式碼

通過打JDBC底層實現的程式碼的斷點

程式碼就不貼了,很抽象
步驟是
1.序列化引數
2.序列化sql
3.格式化mysql 通訊協議
4.獲取mysql 連線
5.傳送mysql請求
6.獲取返回報文

----以下是原始碼執行順序-----
PreparedStatement.execute();
//搜這個關鍵程式碼塊
Buffer sendPacket = fillSendPacket();
fillSendPacket();//用來封裝傳送的mysql報文協議
executeInternal();方法中傳送了請求
ConnectionImpl.execSQL()
//MysqlIO這裡有mysql的協議封裝 
MysqlIO.sqlQueryDirect();//傳送請求
MysqlIO.readAllResults()//解析返回值報文

複製程式碼

結論:

mysql的協議中,伺服器響應報文是對於insert/update/delete是有返回主鍵值的,只是應用層中沒有給出介面


4.3.1 OK 響應報文
客戶端的命令執行正確時,伺服器會返回OK響應報文。
MySQL 4.0 及之前的版本
位元組	說明
1	OK報文,值恆為0x00
1-9	受影響行數(Length Coded Binary)
1-9	索引ID值(Length Coded Binary)
2	伺服器狀態
n	伺服器訊息(字串到達訊息尾部時結束,無結束符)
複製程式碼

mysql協議部落格

MySQL協議分析

mysql網路協議官網

相關文章