• mybatis源码(五)-获取自动插入的主键


      有些时候我们希望插入的自增主键会自动赋值给bean 实体的相关属性,这就需要获取到插入之后的属性。JDBC提供了相关的API,研究mybatis 的操作过程。

    1. JDBC获取自动插入的主键

    1. API 查看

    1. java.sql.Connection#prepareStatement(java.lang.String, int) 生成预编译PreparedStatement 对象如下:(也就是参数为1的时候是获取key,其他是不获取自增的key)

        /**
         * Creates a default <code>PreparedStatement</code> object that has
         * the capability to retrieve auto-generated keys. The given constant
         * tells the driver whether it should make auto-generated keys
         * available for retrieval.  This parameter is ignored if the SQL statement
         * is not an <code>INSERT</code> statement, or an SQL statement able to return
         * auto-generated keys (the list of such statements is vendor-specific).
         * <P>
         * <B>Note:</B> This method is optimized for handling
         * parametric SQL statements that benefit from precompilation. If
         * the driver supports precompilation,
         * the method <code>prepareStatement</code> will send
         * the statement to the database for precompilation. Some drivers
         * may not support precompilation. In this case, the statement may
         * not be sent to the database until the <code>PreparedStatement</code>
         * object is executed.  This has no direct effect on users; however, it does
         * affect which methods throw certain SQLExceptions.
         * <P>
         * Result sets created using the returned <code>PreparedStatement</code>
         * object will by default be type <code>TYPE_FORWARD_ONLY</code>
         * and have a concurrency level of <code>CONCUR_READ_ONLY</code>.
         * The holdability of the created result sets can be determined by
         * calling {@link #getHoldability}.
         *
         * @param sql an SQL statement that may contain one or more '?' IN
         *        parameter placeholders
         * @param autoGeneratedKeys a flag indicating whether auto-generated keys
         *        should be returned; one of
         *        <code>Statement.RETURN_GENERATED_KEYS</code> or
         *        <code>Statement.NO_GENERATED_KEYS</code>
         * @return a new <code>PreparedStatement</code> object, containing the
         *         pre-compiled SQL statement, that will have the capability of
         *         returning auto-generated keys
         * @exception SQLException if a database access error occurs, this
         *  method is called on a closed connection
         *         or the given parameter is not a <code>Statement</code>
         *         constant indicating whether auto-generated keys should be
         *         returned
         * @exception SQLFeatureNotSupportedException if the JDBC driver does not support
         * this method with a constant of Statement.RETURN_GENERATED_KEYS
         * @since 1.4
         */
        PreparedStatement prepareStatement(String sql, int autoGeneratedKeys)
            throws SQLException;
    View Code

    2. 同理Statement 对象也提供了相关的方法:java.sql.Statement#execute(java.lang.String, int)

        /**
         * Executes the given SQL statement and signals the driver with the
         * given flag about whether the
         * auto-generated keys produced by this <code>Statement</code> object
         * should be made available for retrieval.  The driver will ignore the
         * flag if the SQL statement
         * is not an <code>INSERT</code> statement, or an SQL statement able to return
         * auto-generated keys (the list of such statements is vendor-specific).
         *<p>
         * <strong>Note:</strong>This method cannot be called on a
         * <code>PreparedStatement</code> or <code>CallableStatement</code>.
         * @param sql an SQL Data Manipulation Language (DML) statement, such as <code>INSERT</code>, <code>UPDATE</code> or
         * <code>DELETE</code>; or an SQL statement that returns nothing,
         * such as a DDL statement.
         *
         * @param autoGeneratedKeys a flag indicating whether auto-generated keys
         *        should be made available for retrieval;
         *         one of the following constants:
         *         <code>Statement.RETURN_GENERATED_KEYS</code>
         *         <code>Statement.NO_GENERATED_KEYS</code>
         * @return either (1) the row count for SQL Data Manipulation Language (DML) statements
         *         or (2) 0 for SQL statements that return nothing
         *
         * @exception SQLException if a database access error occurs,
         *  this method is called on a closed <code>Statement</code>, the given
         *            SQL statement returns a <code>ResultSet</code> object,
         *            the given constant is not one of those allowed, the method is called on a
         * <code>PreparedStatement</code> or <code>CallableStatement</code>
         * @exception SQLFeatureNotSupportedException if the JDBC driver does not support
         * this method with a constant of Statement.RETURN_GENERATED_KEYS
         * @throws SQLTimeoutException when the driver has determined that the
         * timeout value that was specified by the {@code setQueryTimeout}
         * method has been exceeded and has at least attempted to cancel
         * the currently running {@code Statement}
         * @since 1.4
         */
        int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException;
    View Code

    2. JDBC获取插入的主键测试

    1. 单条插入:

      首先基于JDBC可以获取自动插入的主键。

    import java.sql.*;
    
    public class Client {
    
        public static void main(String[] args) throws Exception {
            //加载MySql的驱动类
            Class.forName("com.mysql.jdbc.Driver");
            Connection root = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
            // 第二个参数可以指定是否返回主键。
            PreparedStatement preparedStatement = root.prepareStatement("insert into test(name) values(?)", Statement.RETURN_GENERATED_KEYS);
            preparedStatement.setString(1, "张三");
            preparedStatement.executeUpdate();
    
            ResultSet generatedKeys = preparedStatement.getGeneratedKeys();
            while (generatedKeys.next()) {
                Object anInt = generatedKeys.getObject(1);
                System.out.println(anInt);
            }
    
            generatedKeys.close();
            preparedStatement.close();
            root.close();
        }
    }

    如上代码可以获取插入的主键。

    2. 多条插入: values(), () 这种语法依次插入多条。 这种类似于一次将数据发给mysql

        public static void main(String[] args) throws Exception {
            //加载MySql的驱动类
            Class.forName("com.mysql.jdbc.Driver");
            Connection root = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
            // 第二个参数可以指定是否返回主键。
            PreparedStatement preparedStatement = root.prepareStatement("insert into test(name) values(?), (?)", Statement.RETURN_GENERATED_KEYS);
            preparedStatement.setString(1, "张三1");
            preparedStatement.setString(2, "张三2");
            preparedStatement.executeUpdate();
    
            ResultSet generatedKeys = preparedStatement.getGeneratedKeys();
            while (generatedKeys.next()) {
                Object anInt = generatedKeys.getObject(1);
                System.out.println(anInt);
            }
    
            generatedKeys.close();
            preparedStatement.close();
            root.close();
        }

    结果:

    9
    10

    3. Batch 批处理 

    (1) PreparedStatement 批处理

      对同一个PreparedStatement反复设置参数并调用addBatch(),这样就相当于给一个SQL加上了多组参数,相当于变成了“多行”SQL。

      第二个不同点是调用的不是executeUpdate(),而是executeBatch(),因为我们设置了多组参数,相应地,返回结果也是多个int值,因此返回类型是int[],循环int[]数组即可获取每组参数执行后影响的结果数量。

        public static void main(String[] args) throws Exception {
            //加载MySql的驱动类
            Class.forName("com.mysql.jdbc.Driver");
            Connection root = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
            // 第二个参数可以指定是否返回主键。
            PreparedStatement preparedStatement = root.prepareStatement("insert into test(name) values(?)", Statement.RETURN_GENERATED_KEYS);
    
            // 模拟总共插入10条,每批插入3条
            for (int i = 0; i < 11; i++) {
                preparedStatement.setString(1, "batch name " + i);
                preparedStatement.addBatch();
    
                if (i % 3 == 0) {
                    System.out.println("======第 " + i / 3 + " 批次");
                    int[] ints = preparedStatement.executeBatch();
                    System.out.println(Arrays.toString(ints));
                    preparedStatement.clearBatch();
                    ResultSet generatedKeys = preparedStatement.getGeneratedKeys();
                    while (generatedKeys.next()) {
                        Object anInt = generatedKeys.getObject(1);
                        System.out.println(anInt);
                    }
                }
            }
    
            // 将剩下的未处理命令发送给数据库
            System.out.println("======执行剩下的批次");
            int[] ints = preparedStatement.executeBatch();
            if (ints != null && ints.length > 0) {
                System.out.println(Arrays.toString(ints));
                ResultSet generatedKeys = preparedStatement.getGeneratedKeys();
                while (generatedKeys.next()) {
                    Object anInt = generatedKeys.getObject(1);
                    System.out.println(anInt);
                }
                generatedKeys.close();
            }
    
            preparedStatement.close();
            root.close();
        }

    结果:

    ======第 0 批次
    [1]
    113
    ======第 1 批次
    [1, 1, 1]
    114
    115
    116
    ======第 2 批次
    [1, 1, 1]
    117
    118
    119
    ======第 3 批次
    [1, 1, 1]
    120
    121
    122
    ======执行剩下的批次
    [1]
    123

    (2) Statement 批处理

        public static void main(String[] args) throws Exception {
            //加载MySql的驱动类
            Class.forName("com.mysql.jdbc.Driver");
            Connection root = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
            // 第二个参数可以指定是否返回主键。
            Statement statement = root.createStatement();
            statement.addBatch("insert into test(name) values("zs1")");
            statement.addBatch("insert into test(name) values("zs2")");
            statement.addBatch("insert into test(name) values("zs3")");
            statement.addBatch("insert into test(name) values("zs4")");
            statement.addBatch("insert into test(name) values("zs7")");
            statement.addBatch("insert into test(name) values("zs7")");
            statement.addBatch("delete from test where name = "zs7"");
            int[] ints = statement.executeBatch();
            System.out.println(Arrays.toString(ints));
    
            statement.close();
            root.close();
        }

    结果:

    [1, 1, 1, 1, 1, 1, 2]

    3. mybatis 获取自动插入的主键

    1. mybatis 获取自动插入的主键

    xml 方式

      <insert id="insertTab1" useGeneratedKeys="true" keyProperty="id">
        insert into tab1(name)
        values (#{name})
      </insert>

    注解方式:

      @Insert("insert into tab1(name) values(#{name})")
      @Options(useGeneratedKeys = true, keyProperty = "id")
      int insertTab1(Tab1 tab1);

    2. 查看源码:

    我们断点设置在debug上可以看到调用链如下:

     看几个重要的方法:

    (1) 对于上面sql,在创建 RoutingStatementHandler 时会选择 PreparedStatementHandler。org.apache.ibatis.executor.statement.RoutingStatementHandler#RoutingStatementHandler

      public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    
        switch (ms.getStatementType()) {
          case STATEMENT:
            delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
            break;
          case PREPARED:
            delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
            break;
          case CALLABLE:
            delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
            break;
          default:
            throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
        }
    
      }

      所以创建PreparedStatementHandler

    (2) 执行插入会调用到: org.apache.ibatis.executor.statement.PreparedStatementHandler#update

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

    (3) org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator#processBatch

      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 ResultSetMetaData rsmd = rs.getMetaData();
          final Configuration configuration = ms.getConfiguration();
          if (rsmd.getColumnCount() < keyProperties.length) {
            // Error?
          } else {
            assignKeys(configuration, rs, rsmd, keyProperties, parameter);
          }
        } catch (Exception e) {
          throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
        }
      }

      这里会获取到我们设置的 id, 如果 keyProperties 为空的话则不走后面的代码。

      stmt.getGeneratedKeys() 实际和上面JDBC 一样,获取另一个ResultSet, 然后拿里面的属性值。

    (4) org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.KeyAssigner#assign

        protected void assign(ResultSet rs, Object param) {
          if (paramName != null) {
            // If paramName is set, param is ParamMap
            param = ((ParamMap<?>) param).get(paramName);
          }
          MetaObject metaParam = configuration.newMetaObject(param);
          try {
            if (typeHandler == null) {
              if (metaParam.hasSetter(propertyName)) {
                Class<?> propertyType = metaParam.getSetterType(propertyName);
                typeHandler = typeHandlerRegistry.getTypeHandler(propertyType,
                    JdbcType.forCode(rsmd.getColumnType(columnPosition)));
              } else {
                throw new ExecutorException("No setter found for the keyProperty '" + propertyName + "' in '"
                    + metaParam.getOriginalObject().getClass().getName() + "'.");
              }
            }
            if (typeHandler == null) {
              // Error?
            } else {
              Object value = typeHandler.getResult(rs, columnPosition);
              metaParam.setValue(propertyName, value);
            }
          } catch (SQLException e) {
            throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e,
                e);
          }
        }

    (5) org.apache.ibatis.reflection.MetaObject#setValue

      public void setValue(String name, Object value) {
        PropertyTokenizer prop = new PropertyTokenizer(name);
        if (prop.hasNext()) {
          MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
          if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
            if (value == null) {
              // don't instantiate child path if value is null
              return;
            } else {
              metaValue = objectWrapper.instantiatePropertyValue(name, prop, objectFactory);
            }
          }
          metaValue.setValue(prop.getChildren(), value);
        } else {
          objectWrapper.set(prop, value);
        }
      }

      到之后的逻辑和查询到数据之后反射生成bean 属性一样。

    4. mybatis 获取自动插入的主键方式二

      mybatis 提供了selectKey 来在insert 或者 update 执行前后进行一些处理。

    1. 后置处理获取插入的主键

      <insert id="insertTab1">
        <selectKey keyProperty="id" order="AFTER" resultType="int">
          select LAST_INSERT_ID();
        </selectKey>
        insert into tab1(name)
        values (#{name})
      </insert>

      这个逻辑就是插入完成之后调用selectKey 中的语句,然后赋值给keyProperty 指定的列。debug 查看其调用链:

       可以看到和上面注解方式一样,走的是一个后置处理。只不过区别是这次用的KeyGenerator 是SelectKeyGenerator, 也就是走的是查询生成器。org.apache.ibatis.executor.keygen.SelectKeyGenerator#processGeneratedKeys 源码如下:

      private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
        try {
          if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {
            String[] keyProperties = keyStatement.getKeyProperties();
            final Configuration configuration = ms.getConfiguration();
            final MetaObject metaParam = configuration.newMetaObject(parameter);
            // Do not close keyExecutor.
            // The transaction will be closed by parent executor.
            Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
            List<Object> values = keyExecutor.query(keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
            if (values.size() == 0) {
              throw new ExecutorException("SelectKey returned no data.");
            } else if (values.size() > 1) {
              throw new ExecutorException("SelectKey returned more than one value.");
            } else {
              MetaObject metaResult = configuration.newMetaObject(values.get(0));
              if (keyProperties.length == 1) {
                if (metaResult.hasGetter(keyProperties[0])) {
                  setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));
                } else {
                  // no getter for the property - maybe just a single value object
                  // so try that
                  setValue(metaParam, keyProperties[0], values.get(0));
                }
              } else {
                handleMultipleProperties(keyProperties, metaParam, metaResult);
              }
            }
          }
        } catch (ExecutorException e) {
          throw e;
        } catch (Exception e) {
          throw new ExecutorException("Error selecting key or setting result to parameter object. Cause: " + e, e);
        }
      }

      可以看到这里重新创建keyExecutor, 然后执行 keyStatement(也就是SelectKey 对应的MappedStatement 对象), 然后走setValue 进行赋值。

      当然也可以指定多个,比如下面这种写法:(下面这种写法只是模拟可以实现插入完成之后通过查询给不同的列赋值)

      <insert id="insertTab1">
        <selectKey keyProperty="id,name" keyColumn="id,name" order="AFTER" resultType="map">
          select * from tab1 where id = 13
        </selectKey>
        insert into tab1(name)
        values (#{name})
      </insert>

    2. 前置处理器的使用

        这里也可以使用前置处理从数据库生成一个唯一ID赋值给指定的列,这里模拟其调用过程。

    比如:

      <insert id="insertTab1">
        <selectKey keyProperty="name" resultType="string" order="BEFORE">
          select name from tab1 where id = 13
        </selectKey>
        insert into tab1(name)
        values (#{name})
      </insert>

    我们将断点下在setName 方法,查看调用链:

     重要方法讲解:

    (1) org.apache.ibatis.executor.SimpleExecutor#doUpdate

      public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
        Statement stmt = null;
        try {
          Configuration configuration = ms.getConfiguration();
          StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
          stmt = prepareStatement(handler, ms.getStatementLog());
          return handler.update(stmt);
        } finally {
          closeStatement(stmt);
        }
      }

      从上面调用链可以看到在configuration.newStatementHandler 就走BEFORE 处理。然后拿到相关属性赋值完再走真正的insert 语句。

    (2) 创建StatementHandler 会调用到 configuration.newStatementHandler:

      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);
      }

      这里走generateKeys(parameterObject); 

    (3) org.apache.ibatis.executor.statement.BaseStatementHandler#generateKeys

      protected void generateKeys(Object parameter) {
        KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
        ErrorContext.instance().store();
        keyGenerator.processBefore(executor, mappedStatement, null, parameter);
        ErrorContext.instance().recall();
      }

      接下来后续也是走SelectKeyGenerator 生成器。

    (4) org.apache.ibatis.executor.keygen.SelectKeyGenerator#processBefore

      @Override
      public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
        if (executeBefore) {
          processGeneratedKeys(executor, ms, parameter);
        }
      }

    (5) org.apache.ibatis.executor.keygen.SelectKeyGenerator#processGeneratedKeys

      private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
        try {
          if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {
            String[] keyProperties = keyStatement.getKeyProperties();
            final Configuration configuration = ms.getConfiguration();
            final MetaObject metaParam = configuration.newMetaObject(parameter);
            // Do not close keyExecutor.
            // The transaction will be closed by parent executor.
            Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
            List<Object> values = keyExecutor.query(keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
            if (values.size() == 0) {
              throw new ExecutorException("SelectKey returned no data.");
            } else if (values.size() > 1) {
              throw new ExecutorException("SelectKey returned more than one value.");
            } else {
              MetaObject metaResult = configuration.newMetaObject(values.get(0));
              if (keyProperties.length == 1) {
                if (metaResult.hasGetter(keyProperties[0])) {
                  setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));
                } else {
                  // no getter for the property - maybe just a single value object
                  // so try that
                  setValue(metaParam, keyProperties[0], values.get(0));
                }
              } else {
                handleMultipleProperties(keyProperties, metaParam, metaResult);
              }
            }
          }
        } catch (ExecutorException e) {
          throw e;
        } catch (Exception e) {
          throw new ExecutorException("Error selecting key or setting result to parameter object. Cause: " + e, e);
        }
      }

      到这里流程和上面一样,重新创建新的Executor、然后获取到KeyStatament对象之后调用,获取到结果之后进行反射赋值给特定的属性。

      注意:selectKey 只能有一个,不能写多个,多个在构造MappedStatement 过程中会报错。

    补充: KeyGenerator的选择以及调用机制

    1. 首先KeyGenerator 是一个接口

    package org.apache.ibatis.executor.keygen;
    
    import java.sql.Statement;
    
    import org.apache.ibatis.executor.Executor;
    import org.apache.ibatis.mapping.MappedStatement;
    
    /**
     * @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);
    
    }

    查看其调用时机:

    (1) processBefore 调用如下:
    org.apache.ibatis.executor.statement.BaseStatementHandler#BaseStatementHandler 构造 -》 org.apache.ibatis.executor.statement.BaseStatementHandler#generateKeys -》 keyGenerator.processBefore

    (2) processAfter 调用如下:以PreparedStatementHandler 为例子

    org.apache.ibatis.executor.statement.PreparedStatementHandler#update -》 org.apache.ibatis.executor.keygen.KeyGenerator#processAfter

    2. 具体的实现类有下面三个

    org.apache.ibatis.executor.keygen.NoKeyGenerator

    public class NoKeyGenerator implements KeyGenerator {
    
      /**
       * A shared instance.
       *
       * @since 3.4.3
       */
      public static final NoKeyGenerator INSTANCE = new NoKeyGenerator();
    
      @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) {
        // Do Nothing
      }
    
    }
    View Code

    org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator:

    public class Jdbc3KeyGenerator implements KeyGenerator {
    
      private static final String SECOND_GENERIC_PARAM_NAME = ParamNameResolver.GENERIC_NAME_PREFIX + "2";
    
      /**
       * A shared instance.
       *
       * @since 3.4.3
       */
      public static final Jdbc3KeyGenerator INSTANCE = new Jdbc3KeyGenerator();
    
      private static final String MSG_TOO_MANY_KEYS = "Too many keys are generated. There are only %d target objects. "
          + "You either specified a wrong 'keyProperty' or encountered a driver bug like #1523.";
    
      @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, 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 ResultSetMetaData rsmd = rs.getMetaData();
          final Configuration configuration = ms.getConfiguration();
          if (rsmd.getColumnCount() < keyProperties.length) {
            // Error?
          } else {
            assignKeys(configuration, rs, rsmd, keyProperties, parameter);
          }
        } catch (Exception e) {
          throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
        }
      }
    
      @SuppressWarnings("unchecked")
      private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties,
          Object parameter) throws SQLException {
        if (parameter instanceof ParamMap || parameter instanceof StrictMap) {
          // Multi-param or single param with @Param
          assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map<String, ?>) parameter);
        } else if (parameter instanceof ArrayList && !((ArrayList<?>) parameter).isEmpty()
            && ((ArrayList<?>) parameter).get(0) instanceof ParamMap) {
          // Multi-param or single param with @Param in batch operation
          assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, (ArrayList<ParamMap<?>>) parameter);
        } else {
          // Single param without @Param
          assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);
        }
      }
    
      private void assignKeysToParam(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
          String[] keyProperties, Object parameter) throws SQLException {
        Collection<?> params = collectionize(parameter);
        if (params.isEmpty()) {
          return;
        }
        List<KeyAssigner> assignerList = new ArrayList<>();
        for (int i = 0; i < keyProperties.length; i++) {
          assignerList.add(new KeyAssigner(configuration, rsmd, i + 1, null, keyProperties[i]));
        }
        Iterator<?> iterator = params.iterator();
        while (rs.next()) {
          if (!iterator.hasNext()) {
            throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, params.size()));
          }
          Object param = iterator.next();
          assignerList.forEach(x -> x.assign(rs, param));
        }
      }
    
      private void assignKeysToParamMapList(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
          String[] keyProperties, ArrayList<ParamMap<?>> paramMapList) throws SQLException {
        Iterator<ParamMap<?>> iterator = paramMapList.iterator();
        List<KeyAssigner> assignerList = new ArrayList<>();
        long counter = 0;
        while (rs.next()) {
          if (!iterator.hasNext()) {
            throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, counter));
          }
          ParamMap<?> paramMap = iterator.next();
          if (assignerList.isEmpty()) {
            for (int i = 0; i < keyProperties.length; i++) {
              assignerList
                  .add(getAssignerForParamMap(configuration, rsmd, i + 1, paramMap, keyProperties[i], keyProperties, false)
                      .getValue());
            }
          }
          assignerList.forEach(x -> x.assign(rs, paramMap));
          counter++;
        }
      }
    
      private void assignKeysToParamMap(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
          String[] keyProperties, Map<String, ?> paramMap) throws SQLException {
        if (paramMap.isEmpty()) {
          return;
        }
        Map<String, Entry<Iterator<?>, List<KeyAssigner>>> assignerMap = new HashMap<>();
        for (int i = 0; i < keyProperties.length; i++) {
          Entry<String, KeyAssigner> entry = getAssignerForParamMap(configuration, rsmd, i + 1, paramMap, keyProperties[i],
              keyProperties, true);
          Entry<Iterator<?>, List<KeyAssigner>> iteratorPair = MapUtil.computeIfAbsent(assignerMap, entry.getKey(),
              k -> MapUtil.entry(collectionize(paramMap.get(k)).iterator(), new ArrayList<>()));
          iteratorPair.getValue().add(entry.getValue());
        }
        long counter = 0;
        while (rs.next()) {
          for (Entry<Iterator<?>, List<KeyAssigner>> pair : assignerMap.values()) {
            if (!pair.getKey().hasNext()) {
              throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, counter));
            }
            Object param = pair.getKey().next();
            pair.getValue().forEach(x -> x.assign(rs, param));
          }
          counter++;
        }
      }
    
      private Entry<String, KeyAssigner> getAssignerForParamMap(Configuration config, ResultSetMetaData rsmd,
          int columnPosition, Map<String, ?> paramMap, String keyProperty, String[] keyProperties, boolean omitParamName) {
        Set<String> keySet = paramMap.keySet();
        // A caveat : if the only parameter has {@code @Param("param2")} on it,
        // it must be referenced with param name e.g. 'param2.x'.
        boolean singleParam = !keySet.contains(SECOND_GENERIC_PARAM_NAME);
        int firstDot = keyProperty.indexOf('.');
        if (firstDot == -1) {
          if (singleParam) {
            return getAssignerForSingleParam(config, rsmd, columnPosition, paramMap, keyProperty, omitParamName);
          }
          throw new ExecutorException("Could not determine which parameter to assign generated keys to. "
              + "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
              + "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
              + keySet);
        }
        String paramName = keyProperty.substring(0, firstDot);
        if (keySet.contains(paramName)) {
          String argParamName = omitParamName ? null : paramName;
          String argKeyProperty = keyProperty.substring(firstDot + 1);
          return MapUtil.entry(paramName, new KeyAssigner(config, rsmd, columnPosition, argParamName, argKeyProperty));
        } else if (singleParam) {
          return getAssignerForSingleParam(config, rsmd, columnPosition, paramMap, keyProperty, omitParamName);
        } else {
          throw new ExecutorException("Could not find parameter '" + paramName + "'. "
              + "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
              + "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
              + keySet);
        }
      }
    
      private Entry<String, KeyAssigner> getAssignerForSingleParam(Configuration config, ResultSetMetaData rsmd,
          int columnPosition, Map<String, ?> paramMap, String keyProperty, boolean omitParamName) {
        // Assume 'keyProperty' to be a property of the single param.
        String singleParamName = nameOfSingleParam(paramMap);
        String argParamName = omitParamName ? null : singleParamName;
        return MapUtil.entry(singleParamName, new KeyAssigner(config, rsmd, columnPosition, argParamName, keyProperty));
      }
    
      private static String nameOfSingleParam(Map<String, ?> paramMap) {
        // There is virtually one parameter, so any key works.
        return paramMap.keySet().iterator().next();
      }
    
      private static Collection<?> collectionize(Object param) {
        if (param instanceof Collection) {
          return (Collection<?>) param;
        } else if (param instanceof Object[]) {
          return Arrays.asList((Object[]) param);
        } else {
          return Arrays.asList(param);
        }
      }
    
      private class KeyAssigner {
        private final Configuration configuration;
        private final ResultSetMetaData rsmd;
        private final TypeHandlerRegistry typeHandlerRegistry;
        private final int columnPosition;
        private final String paramName;
        private final String propertyName;
        private TypeHandler<?> typeHandler;
    
        protected KeyAssigner(Configuration configuration, ResultSetMetaData rsmd, int columnPosition, String paramName,
            String propertyName) {
          super();
          this.configuration = configuration;
          this.rsmd = rsmd;
          this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
          this.columnPosition = columnPosition;
          this.paramName = paramName;
          this.propertyName = propertyName;
        }
    
        protected void assign(ResultSet rs, Object param) {
          if (paramName != null) {
            // If paramName is set, param is ParamMap
            param = ((ParamMap<?>) param).get(paramName);
          }
          MetaObject metaParam = configuration.newMetaObject(param);
          try {
            if (typeHandler == null) {
              if (metaParam.hasSetter(propertyName)) {
                Class<?> propertyType = metaParam.getSetterType(propertyName);
                typeHandler = typeHandlerRegistry.getTypeHandler(propertyType,
                    JdbcType.forCode(rsmd.getColumnType(columnPosition)));
              } else {
                throw new ExecutorException("No setter found for the keyProperty '" + propertyName + "' in '"
                    + metaParam.getOriginalObject().getClass().getName() + "'.");
              }
            }
            if (typeHandler == null) {
              // Error?
            } else {
              Object value = typeHandler.getResult(rs, columnPosition);
              metaParam.setValue(propertyName, value);
            }
          } catch (SQLException e) {
            throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e,
                e);
          }
        }
      }
    }
    View Code

    org.apache.ibatis.executor.keygen.SelectKeyGenerator:

    import java.sql.Statement;
    import java.util.List;
    
    import org.apache.ibatis.executor.Executor;
    import org.apache.ibatis.executor.ExecutorException;
    import org.apache.ibatis.mapping.MappedStatement;
    import org.apache.ibatis.reflection.MetaObject;
    import org.apache.ibatis.session.Configuration;
    import org.apache.ibatis.session.ExecutorType;
    import org.apache.ibatis.session.RowBounds;
    
    /**
     * @author Clinton Begin
     * @author Jeff Butler
     */
    public class SelectKeyGenerator implements KeyGenerator {
    
      public static final String SELECT_KEY_SUFFIX = "!selectKey";
      private final boolean executeBefore;
      private final MappedStatement keyStatement;
    
      public SelectKeyGenerator(MappedStatement keyStatement, boolean executeBefore) {
        this.executeBefore = executeBefore;
        this.keyStatement = keyStatement;
      }
    
      @Override
      public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
        if (executeBefore) {
          processGeneratedKeys(executor, ms, parameter);
        }
      }
    
      @Override
      public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
        if (!executeBefore) {
          processGeneratedKeys(executor, ms, parameter);
        }
      }
    
      private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
        try {
          if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {
            String[] keyProperties = keyStatement.getKeyProperties();
            final Configuration configuration = ms.getConfiguration();
            final MetaObject metaParam = configuration.newMetaObject(parameter);
            // Do not close keyExecutor.
            // The transaction will be closed by parent executor.
            Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
            List<Object> values = keyExecutor.query(keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
            if (values.size() == 0) {
              throw new ExecutorException("SelectKey returned no data.");
            } else if (values.size() > 1) {
              throw new ExecutorException("SelectKey returned more than one value.");
            } else {
              MetaObject metaResult = configuration.newMetaObject(values.get(0));
              if (keyProperties.length == 1) {
                if (metaResult.hasGetter(keyProperties[0])) {
                  setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));
                } else {
                  // no getter for the property - maybe just a single value object
                  // so try that
                  setValue(metaParam, keyProperties[0], values.get(0));
                }
              } else {
                handleMultipleProperties(keyProperties, metaParam, metaResult);
              }
            }
          }
        } catch (ExecutorException e) {
          throw e;
        } catch (Exception e) {
          throw new ExecutorException("Error selecting key or setting result to parameter object. Cause: " + e, e);
        }
      }
    
      private void handleMultipleProperties(String[] keyProperties,
          MetaObject metaParam, MetaObject metaResult) {
        String[] keyColumns = keyStatement.getKeyColumns();
    
        if (keyColumns == null || keyColumns.length == 0) {
          // no key columns specified, just use the property names
          for (String keyProperty : keyProperties) {
            setValue(metaParam, keyProperty, metaResult.getValue(keyProperty));
          }
        } else {
          if (keyColumns.length != keyProperties.length) {
            throw new ExecutorException("If SelectKey has key columns, the number must match the number of key properties.");
          }
          for (int i = 0; i < keyProperties.length; i++) {
            setValue(metaParam, keyProperties[i], metaResult.getValue(keyColumns[i]));
          }
        }
      }
    
      private void setValue(MetaObject metaParam, String property, Object value) {
        if (metaParam.hasSetter(property)) {
          metaParam.setValue(property, value);
        } else {
          throw new ExecutorException("No setter found for the keyProperty '" + property + "' in " + metaParam.getOriginalObject().getClass().getName() + ".");
        }
      }
    }
    View Code

      这里会根据内部的executeBefore 属性判断能否执行。

    3. 其选择策略以及相关处理逻辑如下:(以xml 解析一条insert 标签为例子)

    org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode 解析方法如下:

      public void parseStatementNode() {
        String id = context.getStringAttribute("id");
        String databaseId = context.getStringAttribute("databaseId");
    
        if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
          return;
        }
    
        String nodeName = context.getNode().getNodeName();
        SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
        boolean useCache = context.getBooleanAttribute("useCache", isSelect);
        boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    
        // Include Fragments before parsing
        XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
        includeParser.applyIncludes(context.getNode());
    
        String parameterType = context.getStringAttribute("parameterType");
        Class<?> parameterTypeClass = resolveClass(parameterType);
    
        String lang = context.getStringAttribute("lang");
        LanguageDriver langDriver = getLanguageDriver(lang);
    
        // Parse selectKey after includes and remove them.
        processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
        // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
        KeyGenerator keyGenerator;
        String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
        keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
        if (configuration.hasKeyGenerator(keyStatementId)) {
          keyGenerator = configuration.getKeyGenerator(keyStatementId);
        } else {
          keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
              configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
              ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        }
    
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
        StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
        Integer fetchSize = context.getIntAttribute("fetchSize");
        Integer timeout = context.getIntAttribute("timeout");
        String parameterMap = context.getStringAttribute("parameterMap");
        String resultType = context.getStringAttribute("resultType");
        Class<?> resultTypeClass = resolveClass(resultType);
        String resultMap = context.getStringAttribute("resultMap");
        String resultSetType = context.getStringAttribute("resultSetType");
        ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
        if (resultSetTypeEnum == null) {
          resultSetTypeEnum = configuration.getDefaultResultSetType();
        }
        String keyProperty = context.getStringAttribute("keyProperty");
        String keyColumn = context.getStringAttribute("keyColumn");
        String resultSets = context.getStringAttribute("resultSets");
    
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered,
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
      }

    (1) processSelectKeyNodes(id, parameterTypeClass, langDriver); 处理selectKey 节点

    org.apache.ibatis.builder.xml.XMLStatementBuilder#processSelectKeyNodes

      private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
        List<XNode> selectKeyNodes = context.evalNodes("selectKey");
        if (configuration.getDatabaseId() != null) {
          parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
        }
        parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
        removeSelectKeyNodes(selectKeyNodes);
      }

    这里可以看到是先解析selectKeyNodes, 然后删除掉selectKeyNodes, 也就是从原来xml 中的sql 语句中删除。

    1》解析节点如下:

    org.apache.ibatis.builder.xml.XMLStatementBuilder#parseSelectKeyNodes

      private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
        for (XNode nodeToHandle : list) {
          String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
          String databaseId = nodeToHandle.getStringAttribute("databaseId");
          if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
            parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
          }
        }
      }

    第一步生成新的statementId, 生成规则是父ID加固定后缀 "!selectKey"

    第二步:调用org.apache.ibatis.builder.xml.XMLStatementBuilder#parseSelectKeyNode 解析标签

      private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
        String resultType = nodeToHandle.getStringAttribute("resultType");
        Class<?> resultTypeClass = resolveClass(resultType);
        StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
        String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
        String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
        boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));
    
        // defaults
        boolean useCache = false;
        boolean resultOrdered = false;
        KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
        Integer fetchSize = null;
        Integer timeout = null;
        boolean flushCache = false;
        String parameterMap = null;
        String resultMap = null;
        ResultSetType resultSetTypeEnum = null;
    
        SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
        SqlCommandType sqlCommandType = SqlCommandType.SELECT;
    
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered,
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);
    
        id = builderAssistant.applyCurrentNamespace(id, false);
    
        MappedStatement keyStatement = configuration.getMappedStatement(id, false);
        configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
      }

      首先解析标签的一些属性, 其中order顺序可以看到是大写进行判断的。

           然后生成一个新的MappedStatement 对象,并且缓存起来, 新的MappedStatement 使用的都是一些基础的配置。比如keyGenerator 是 NoKeyGenerator。

      然后创建一个SelectKeyGenerator 对象添加到Configuration 的缓存Map 中。供后面使用。

    2》删除nodes方法如下:org.apache.ibatis.builder.xml.XMLStatementBuilder#removeSelectKeyNodes

      private void removeSelectKeyNodes(List<XNode> selectKeyNodes) {
        for (XNode nodeToHandle : selectKeyNodes) {
          nodeToHandle.getParent().getNode().removeChild(nodeToHandle.getNode());
        }
      }

    (2) 接下来解析移除掉include 和 selectKey 标签的sql

    1》生成keyGenerator

        KeyGenerator keyGenerator;
        String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
        keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
        if (configuration.hasKeyGenerator(keyStatementId)) {
          keyGenerator = configuration.getKeyGenerator(keyStatementId);
        } else {
          keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
              configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
              ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        }

      可以看到其规则是先从configuration 获取,如果获取到就使用keyGenerator; 否则走else 使用Jdbc3KeyGenerator.INSTANCE 或者 NoKeyGenerator.INSTANCE。

    2》 解析其他属性。。。

    3》builderAssistant.addMappedStatement 生成MappedStatement 对象,然后存到Configuration 中。 

        这里就完成了selectKey 对应的MappedStatemtn 对象的生成 以及 外面Insert或者Update 标签对应的MappedStatement 对象的生成。

    【当你用心写完每一篇博客之后,你会发现它比你用代码实现功能更有成就感!】
  • 相关阅读:
    《游戏引擎架构》笔记十二
    《游戏引擎架构》笔记十一
    《游戏引擎架构》笔记十
    《游戏引擎架构》笔记九
    《游戏引擎架构》笔记八
    《游戏引擎架构》笔记七
    2000行代码实现软渲染引擎
    C++ 构造函数或析构函数调用虚函数
    C++ protected访问权限思考
    堆排序
  • 原文地址:https://www.cnblogs.com/qlqwjy/p/15441058.html
Copyright © 2020-2023  润新知