查看原文
其他

MyBatis 这小子是怎样拿到 insert 时生成的主键的?

侯树成 Tomcat那些事儿 2021-03-14


SQL 语句执行insert,这谁都会,执行之后,返回结果是影响行数。但是在我们应用开发场景中,有些表的主键采用自增生成。这个时候,你怎么能把这个自增的主键值拿到呢?

你说这还不简单,马上再执行一下 select,就都查出来啦。 你的where 是什么才限定刚好是新增的那一条呢? 你说,那我查主键值最大的那一条呢? 也不灵。毕竟insert 和select 之间是有时间差的,此时可能有其他写入操作。

一些常用的 ORM 框架在执行完 save操作之后,一般都会把新生成的主键自动回填到PO对象中。那它们是怎么做到的呢?

带着这个疑问,咱们一起来看一下,时下活跃在各大应用中的明星MyBatis

MyBatis 是如何取到自增主键值的

我们知道, MyBatis 无论通过注解,还是XML配置的形式,将待执行的SQL包装起来,在执行时通过代理进行解析。
之前的文章中(与MyBatis缠斗的几个小时...),我们分析过MyBatis的大致执行原理,为何只写一个接口就能映射到一个XML或者注解中对应的SQL逻辑上。
其中有个类MappedStatement,我们在XML里配置的各类SQL 都会转成这样一个实例。这个类中包含了大量配置相关的信息。

我们摘一段代码来看看:

public final class MappedStatement {

  private String resource;
  private Configuration configuration;
  private StatementType statementType;
  private ResultSetType resultSetType;
  private SqlSource sqlSource;
  private SqlCommandType sqlCommandType;
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  private String databaseId;
  private Log statementLog;
  private LanguageDriver lang;
  private String[] resultSets;

  MappedStatement() {
    // constructor disabled
  }

  public static class Builder {
    private MappedStatement mappedStatement = new MappedStatement();

    public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
      mappedStatement.configuration = configuration;
      mappedStatement.id = id;
      mappedStatement.sqlSource = sqlSource;
      mappedStatement.statementType = StatementType.PREPARED;
      mappedStatement.parameterMap = new ParameterMap.Builder(configuration, "defaultParameterMap"nullnew ArrayList<ParameterMapping>()).build();
      mappedStatement.resultMaps = new ArrayList<ResultMap>();
      mappedStatement.sqlCommandType = sqlCommandType;
      mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
      String logId = id;
      if (configuration.getLogPrefix() != null) {
        logId = configuration.getLogPrefix() + id;
      }
      mappedStatement.statementLog = LogFactory.getLog(logId);
      mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();
    }

代码比较长,咱们长话短说,重点看这一句

mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;

这里根据配置时指定是否使用UseGeneratedKeys来决定生成的 keyGenerator实例是谁。

咱们使用时,在 XML 里的 insert 一般写成这样:

<insert id="insertUser" parameterType="com.test.entity.User">  
           insert into user( name, password, age, deleteFlag) 
               values(#{name}, #{password}, #{age}, #{deleteFlag})
   </insert>

这个时候, insert 这个 XML 标签包含的一些配置就会使用默认值,其中有一个配置项就是咱们上面在MappedStatement中看到的useGeneratedKeys。它的默认值是false。也就是说,这种配置情况下,这个插入操作的自增主键并不会回写回来。
这个配置主要作用:

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

那咱们把它改成true,同时把自增的列和对应的PO里的属性名写上就可以了。再执行一次操作,在insert 之后, PO 里对应的主键就已经自动填充了。
他是在哪一步做的呢?

MyBatis 如何从 XML的方法映射到 SQLCommand,然后从SQLCommand 的SQLType 再执行不同的操作这些,咱们不细说,具体INSERT,也会执行到PreparedStatementHandler类中的这个update方法。

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

请留意KeyGenerator keyGenerator = mappedStatement.getKeyGenerator(); 这一句,就是咱们在创建mappedStatement时,根据 useKeyGenerator配置生成的。
在 keyGenerator.processAfter方法中,会有一个操作。这里的keyGenerator是根据配置来决定的,如果没有配置useGeneratedKeys=true的话,那这里返回的就是NoKeyGenerator,配置的话,则返回Jdbc3KeyGenerator

位于 Jdbc3KeyGenerator类中, 我们看到,还是继续持有执行SQL的 Statement,然后 通过执行其getGeneratedKeys方法,来拿到一些数据。

public void processBatch(MappedStatement ms, Statement stmt, Collection<Object> parameters) {
    ResultSet rs = null;
    try {
      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
        }
      }
    }
  }

重点就是这一句:rs = stmt.getGeneratedKeys();

Java 文档里描述如下:

Retrieves any auto-generated keys created as a result of executing this Statementobject. If this Statement object did not generate any keys, an empty ResultSetobject is returned.

和我们在前面看到对于useGeneratedKeys的说明描述一致,即通过JDBC API 的方法来拿到自增生成的值,在拿到rs,也就是标准的 JDBC ResultSet 之后,操作和一般的JDBC一样,通过读取columnName来取值。所以我们在前面配置的地方,也需要加上keyColumn

这就是 MyBatis 获取自增主键的全部秘密。

抽奖送书还在进行中,请点击查看 -> 赠送15本springcloud 纸质书籍

相关阅读
与MyBatis缠斗的几个小时...
怎样努力才能成为一名 Java Champion

关注『 Tomcat那些事儿  』 ,发现更多精彩文章!了解各种常见问题背后的原理与答案。深入源码,分析细节,内容原创,欢迎关注。

                       转发是最大的支持,谢谢


更多精彩内容:

一台机器上安装多个Tomcat 的原理(回复001)

监控Tomcat中的各种数据 (回复002)

启动Tomcat的安全机制(回复003)

乱码问题的原理及解决方式(回复007)

Tomcat 日志工作原理及配置(回复011)

web.xml 解析实现(回复 012)

线程池的原理( 回复 014)

Tomcat 的集群搭建原理与实现 (回复 015)

类加载器的原理 (回复 016)

类找不到等问题 (回复 017)

代码的热替换实现(回复 018)

Tomcat 进程自动退出问题 (回复 019)

为什么总是返回404? (回复 020)

...


PS: 对于一些 Tomcat常见问题,在公众号的【常见问题】菜单中,有需要的朋友欢迎关注查看

                                                                                                                                                                                     感觉有用,就点赞、转发支持一把吧,谢谢👇



    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存