0
点赞
收藏
分享

微信扫一扫

mybatis源码之执行insert代码分析

外贸达人小峻先森 2022-01-05 阅读 60

执行insert源码分析

示例代码

mapper xml配置:

<insert id="insertBlog" parameterType="org.net5ijy.mybatis.test.entity.Blog">
  <selectKey resultType="java.lang.Integer" keyProperty="id" order="AFTER" >
    SELECT LAST_INSERT_ID()
  </selectKey>
  insert into blog
  (title, content, create_time, update_time)
  values
  (#{title}, #{content}, now(), now())
</insert>

API示例代码:

Blog blog = new Blog();
blog.setTitle("spring学习");
blog.setContent("spring深入 - 源码分析");
int rows = session.insert("org.net5ijy.mybatis.test.BlogMapper.insertBlog", blog);
// 由于使用sessionFactory.openSession()方法默认非自动提交,所以这里手动提交事务
session.commit();

// 1
System.out.println(rows);
// 由于没有配置selectKey所以是null
System.out.println(blog.getId());

session.insert方法

public int insert(String statement, Object parameter) {
  return update(statement, parameter);
}

public int update(String statement, Object parameter) {
  try {
    dirty = true;
    MappedStatement ms = configuration.getMappedStatement(statement);
    // 数据库更新操作
    // wrapCollection(parameter)在batch insert时就有用了
    return executor.update(ms, wrapCollection(parameter));
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

executor.update方法

此处的executor还是CachingExecutor类的实例。

public int update(MappedStatement ms, Object parameterObject) throws SQLException {
  flushCacheIfRequired(ms);
  return delegate.update(ms, parameterObject);
}

// BaseExecutor.update
public int update(MappedStatement ms, Object parameter) throws SQLException {
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  // 清理一级缓存
  clearLocalCache();
  return doUpdate(ms, parameter);
}

// SimpleExecutor.doUpdate
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
  Statement stmt = null;
  try {
    Configuration configuration = ms.getConfiguration();
    // 这里创建Statement的代码与query时基本一致,不做展开分析
    StatementHandler handler = 
        configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
    stmt = prepareStatement(handler, ms.getStatementLog());
    // 数据库更新操作
    return handler.update(stmt);
  } finally {
    closeStatement(stmt);
  }
}

StatementHandler.update方法

这里底层使用的是PreparedStatementHandler的update方法:

public int update(Statement statement) throws SQLException {
  // 前三行都是JDBC的操作
  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;
}

这里的代码还是比较简单的,执行JDBC更新,获取被影响的行数,后面的KeyGenerator代码是为了给插入数据的主键列赋值。

selectKey和KeyGenerator

selectKey标签

我们的示例SQL中有一段配置:

<selectKey resultType="java.lang.Integer" keyProperty="id" order="AFTER" >
  SELECT LAST_INSERT_ID()
</selectKey>

这个配置用于反查插入数据的主键值,并给参数的主键属性赋值。标签内部的语句在不同的数据库下不同。

selectKey的dtd约束如下:

<!ELEMENT selectKey (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*>
<!ATTLIST selectKey
resultType CDATA #IMPLIED
statementType (STATEMENT|PREPARED|CALLABLE) #IMPLIED
keyProperty CDATA #IMPLIED
keyColumn CDATA #IMPLIED
order (BEFORE|AFTER) #IMPLIED
databaseId CDATA #IMPLIED
>

他支持的子标签与一般的select标签一样,重要属性:

  • resultType - 返回值类型
  • keyProperty - 参数中的主键属性名
  • keyColumn - 主键列名

在mybatis中,会使用与普通查询一样的方式存储selectKey查询。

selectKey的解析

在解析statement的过程中,有一段逻辑是selectKey的解析,在XMLStatementBuilder类的parseStatementNode()方法中,这段代码在初始化阶段做过分析,但是解析selectKey的部分未做展开,这里回顾补充一下:

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

  // 解析parameterType
  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.
  // 解析<selectKey ...>节点
  processSelectKeyNodes(id, parameterTypeClass, langDriver);

  // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
  // 创建KeyGenerator
  KeyGenerator keyGenerator;
  String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
  keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
  if (configuration.hasKeyGenerator(keyStatementId)) {
    keyGenerator = configuration.getKeyGenerator(keyStatementId);
  } else {
    // 解析useGeneratedKeys属性
    keyGenerator = context.getBooleanAttribute(
        "useGeneratedKeys",
        configuration.isUseGeneratedKeys() && 
            SqlCommandType.INSERT.equals(sqlCommandType))
        ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  }

  // 这个非常重要:将xml中的sql转成SqlSource对象,以便后续的进一步解析、
  // 创建preparedstatement及参数赋值
  SqlSource sqlSource = 
      langDriver.createSqlSource(configuration, context, parameterTypeClass);
  // 这个基本就是默认的PREPARED了
  StatementType statementType = 
      StatementType.valueOf(
      context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  Integer fetchSize = context.getIntAttribute("fetchSize");
  Integer timeout = context.getIntAttribute("timeout");
  // 解析parameterMap
  String parameterMap = context.getStringAttribute("parameterMap");
  // 解析resultType
  // 此处有个别名的问题,所以使用的是resolveClass(resultType)方法
  String resultType = context.getStringAttribute("resultType");
  Class<?> resultTypeClass = resolveClass(resultType);
  // 解析resultMap
  String resultMap = context.getStringAttribute("resultMap");
  String resultSetType = context.getStringAttribute("resultSetType");
  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
  if (resultSetTypeEnum == null) {
    resultSetTypeEnum = configuration.getDefaultResultSetType();
  }
  // key对应的属性名
  String keyProperty = context.getStringAttribute("keyProperty");
  // key对应的列名
  String keyColumn = context.getStringAttribute("keyColumn");
  String resultSets = context.getStringAttribute("resultSets");

  // 创建MappedStatement
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered,
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

其中:

// Parse selectKey after includes and remove them.
// 解析<selectKey ...>节点
// 最终会和普通的statement一样,也创建MappedStatement并保存到Configuration中,不做展开分析
processSelectKeyNodes(id, parameterTypeClass, langDriver);

就是用于解析selectKey、并创建SelectKeyGenerator的部分代码。

KeyGenerator的实现

KeyGenerator
  |-- NoKeyGenerator
  |-- Jdbc3KeyGenerator
  |-- SelectKeyGenerator

SelectKeyGenerator类

使用查询获取key的KeyGenerator实现。大概流程如下:

  • 创建Executor
  • 执行selectKey查询
  • 为参数的主键属性赋值

代码有点多,就不记录了。

selectKey标签的替代方式

<insert id="insertBlog" 
        parameterType="org.net5ijy.mybatis.test.entity.Blog" 
        useGeneratedKeys="true" 
        keyProperty="id">
  insert into blog
  (title, content, create_time, update_time)
  values
  (#{title}, #{content}, now(), now())
</insert>

相关属性:

  • useGeneratedKeys - (仅适用于 insert 和 update)MyBatis使用JDBC的getGeneratedKeys方法来取出由数据库内部生成的主键(比如MySQL和SQL Server这样的关系型数据库管理系统的自动递增字段),默认值false
  • keyProperty - 参数中的主键属性名

这里使用的是Jdbc3KeyGenerator实现类。

他的process流程大致如下:

  • 使用stmt.getGeneratedKeys()获取到generatedKeys
  • 给参数的主键属性赋值

不做展开分析了。

KeyGenerator的创建

// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
// 创建KeyGenerator
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
// 这里可以获取到keyGenerator就直接使用,因为上面做了selectKey解析,所以这里执行第一个分支
if (configuration.hasKeyGenerator(keyStatementId)) {
  // 从Configuration获取,这个在之前解析selectKey的时候就创建了
  keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
  // 解析useGeneratedKeys属性
  keyGenerator = context.getBooleanAttribute(
      "useGeneratedKeys",
      configuration.isUseGeneratedKeys() && 
          SqlCommandType.INSERT.equals(sqlCommandType))
      ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}

就是用于获取/创建KeyGenerator的部分代码。

回到StatementHandler.update方法

Object parameterObject = boundSql.getParameterObject();
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);

现在这几行代码的作用就很清晰了。

结束语

至此,mybatis最核心的源码就分析完毕了。

后续会有mapper接口模式(含注解方式)、springboot集成、mybatis-plus核心原理,以及核心的一级缓存、二级缓存、延迟加载、嵌套结果、嵌套查询、多结果集等高级功能的深入源码分析。

举报

相关推荐

0 条评论