mybatis获取insert操作自增主键值原理

发布时间 2023-12-20 23:55:41作者: Crazy_Joker

大家好,我是joker,希望你快乐。

上一篇mybatis insert操作获取自增主键中介绍了如何获取主键值,接下来这篇我们将通过跟踪源码的方式进一步探究mybatis是如何获取到主键的。

其实上一篇中,通过官方文档我们可以看出mybatis还是通过 JDBC 的 getGeneratedKeys 方法获取由数据库内部生成的主键。

  • useGeneratedKeys

    (仅适用于 insert 和 update)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段),默认值:false。

  • keyProperty

    (仅适用于 insert 和 update)指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset)。如果生成列不止一个,可以用逗号分隔多个属性名称。

为了对这个过程有一个更加清晰的认识,下面我们开始跟踪主要代码并做简单说明。

  1. 跟踪了一路代理,模板,执行器中的方法后,跟踪到了PreparedStatementHandler.java中的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;
  }
  1. 可以看到getKeyGenerator()获取主键生成器,进一步跟踪,会进入到Jdbc3KeyGenerator.java的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);
    }
  }

可以看到mybatis获取数据库生成的id方法还是通过jdbc的驱动的实现类进行获取的。

  1. 我使用的是mysql,最终会进入到mysql的jdbc驱动中StatementImpl.java实现类。如果使用其他数据也会进入对应jdbc驱动的StatementImpl.java实现类,获取自增主键需要数据库及jdbc驱动支持。
protected ResultSetInternalMethods getGeneratedKeysInternal(long numKeys) throws SQLException {
    synchronized (checkClosed().getConnectionMutex()) {
        String encoding = this.session.getServerSession().getCharsetSettings().getMetadataEncoding();
        int collationIndex = this.session.getServerSession().getCharsetSettings().getMetadataCollationIndex();
        Field[] fields = new Field[1];
        fields[0] = new Field("", "GENERATED_KEY", collationIndex, encoding, MysqlType.BIGINT_UNSIGNED, 20);

        ArrayList<Row> rowSet = new ArrayList<>();

        long beginAt = getLastInsertID();

        if (this.results != null) {
            String serverInfo = this.results.getServerInfo();

            //
            // Only parse server info messages for 'REPLACE' queries
            //
            if ((numKeys > 0) && (this.results.getFirstCharOfQuery() == 'R') && (serverInfo != null) && (serverInfo.length() > 0)) {
                numKeys = getRecordCountFromInfo(serverInfo);
            }

            if ((beginAt != 0 /* BIGINT UNSIGNED can wrap the protocol representation */) && (numKeys > 0)) {
                for (int i = 0; i < numKeys; i++) {
                    byte[][] row = new byte[1][];
                    if (beginAt > 0) {
                        row[0] = StringUtils.getBytes(Long.toString(beginAt));
                    } else {
                        byte[] asBytes = new byte[8];
                        asBytes[7] = (byte) (beginAt & 0xff);
                        asBytes[6] = (byte) (beginAt >>> 8);
                        asBytes[5] = (byte) (beginAt >>> 16);
                        asBytes[4] = (byte) (beginAt >>> 24);
                        asBytes[3] = (byte) (beginAt >>> 32);
                        asBytes[2] = (byte) (beginAt >>> 40);
                        asBytes[1] = (byte) (beginAt >>> 48);
                        asBytes[0] = (byte) (beginAt >>> 56);

                        BigInteger val = new BigInteger(1, asBytes);

                        row[0] = val.toString().getBytes();
                    }
                    rowSet.add(new ByteArrayRow(row, getExceptionInterceptor()));
                    beginAt += this.connection.getAutoIncrementIncrement();
                }
            }
        }

        ResultSetImpl gkRs = this.resultSetFactory.createFromResultsetRows(ResultSet.CONCUR_READ_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE,
                new ResultsetRowsStatic(rowSet, new DefaultColumnDefinition(fields)));

        return gkRs;
    }
}

可以看到getLastInsertID()方法获取了最后一次插入的id,这个方法只是读取了最后的自增主键,通过注释可以看出select LAST_INSERT_ID()使用这种方式有并发问题,那具体是如何为这个lastInsertId赋值的我们接着说。

/**
 * getLastInsertID returns the value of the auto_incremented key after an
 * executeQuery() or excute() call.
 * 
 * <p>
 * This gets around the un-threadsafe behavior of "select LAST_INSERT_ID()" which is tied to the Connection that created this Statement, and therefore could
 * have had many INSERTS performed before one gets a chance to call "select LAST_INSERT_ID()".
 * </p>
 * 
 * @return the last update ID.
 */
public long getLastInsertID() {
    synchronized (checkClosed().getConnectionMutex()) {
        return this.lastInsertId;
    }
}

至此,我们验证了MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键。

在JDBC中是通过mysql协议进行通讯获取的,在这里做一个简单说明,有兴趣可以去看代码。

MySQL: Client/Server Protocol

MySQL协议分析 - davygeek - 博客园 (cnblogs.com)