Mybatis执行器

mybatis · 浏览次数 : 17

小编点评

MyBatis 是 MyBatis 的核心框架,它负责 SQL 查询、结果映射和事务管理等功能。在 MyBatis 中,执行器(Executor)是用于执行 SQL 语句的核心组件。MyBatis 提供了三种类型的执行器:SimpleExecutor、ReuseExecutor 和 BatchExecutor,它们分别具有不同的特点和用途。 1. **SimpleExecutor**: - 执行器简单,每次执行 SQL 时都会创建新的 PreparedStatement。 - 不缓存查询结果,执行完毕后就会关闭 PreparedStatement。 - 适用于查询操作较少,且不需要缓存的情况。 2. **ReuseExecutor**: - 执行器可以重用 PreparedStatement,减少了数据库连接的开销。 - 缓存查询结果,当下次执行相同的 SQL 时,可以直接使用已缓存的 PreparedStatement。 - 适用于查询操作较多,且需要缓存查询结果的情况。 3. **BatchExecutor**: - 执行器将修改操作(如 INSERT、UPDATE、DELETE)记录在本地,等待程序触发或有下一次查询时才批量执行。 - 适用于需要批量更新操作的情况。 4. **CachingExecutor**: - 执行器是一个装饰类,它本身不执行 SQL,而是将查询请求委托给子类的执行器。 - 缓存查询结果,当查询请求到来时,首先检查缓存中是否有对应的缓存结果。 - 如果有缓存结果,则直接返回;如果没有缓存结果,则执行子类的 query 方法。 - 适用于需要缓存查询结果,并在后续查询中直接使用这些结果的情况。 MyBatis 的执行器通过缓存机制提高了数据库查询的性能,同时也支持了批量操作和事务管理。开发者可以根据具体的业务需求选择合适的执行器。

正文

mybatis执行sql语句的操作是由执行器(Executor)完成的,mybatis中一共提供了3种Executor

类型 名称 功能
REUSE 重用执行器 缓存PreparedStatement,下一次执行相同的sql可重用
BATCH 批量执行器 将修改操作记录在本地,等待程序触发或有下一次查询时才批量执行修改操作
SIMPLE 简单执行器 对每一次执行都生成PreparedStatement,执行完就关闭,不缓存

另外,mybatis 还提供了一个缓存执行器CachingExecutor,该执行器实际上是以上三种执行器的装饰类,用以处理缓存相关操作,实际干活的还是以上三种执行器之一。

Executor的继续结构如下:

image

1. BaseExecutor

BaseExecutor实现了Executor的基本操作,如:

  • 事务的处理:

    • commit(...):处理事务的提交
    • rollback(...):处理事务的回滚
  • 缓存的处理:

    • createCacheKey(...):创建缓存key
    • clearLocalCache(...):清除缓存
  • curd操作:

    • query(...):查询操作
    • update(...):更新操作,插入与删除也是在这里处理
  • 留待子类的实现

    • doUpdate(...):具体的更新操作,留待子类实现
    • doQuery(...):具体的查询操作,留待子类实现

接下来我们关注Executor的实现时,只关注留待子类实现的方法。

2. SimpleExecutor

SimpleExecutor会对每一次执行都生成PreparedStatement,执行完就关闭,不缓存,我们来看看它是怎么实现的,来看看它的doQuery(...)方法:

  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, 
		ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
	Statement stmt = null;
	try {
	  // 获取配置
	  Configuration configuration = ms.getConfiguration();
	  StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, 
			rowBounds, resultHandler, boundSql);
	  // 得到 PrepareStatement
	  stmt = prepareStatement(handler, ms.getStatementLog());
	  // 执行查询
	  return handler.query(stmt, resultHandler);
	} finally {
	  // 关闭 Statement
	  closeStatement(stmt);
	}
  }

获取Statement的方法为SimpleExecutor#prepareStatement

  private Statement prepareStatement(StatementHandler handler, Log statementLog) 
		throws SQLException {
	Statement stmt;
	// 获取数据库连接
	Connection connection = getConnection(statementLog);
	// 获取 Statement
	stmt = handler.prepare(connection, transaction.getTimeout());
	// 处理参数设置
	handler.parameterize(stmt);
	return stmt;
  }

这个方法先是获取了数据库连接,接着获取Statement,然后处理了参数设置。

关于数据库连接的获取,我们在分析配置文件的解析时,数据源的配置最终会转化成PooledDataSourceUnpooledDataSource对象,数据库连接就是从数据源来的。

至于Statement的生成,PreparedStatement的实例化操作方法为PreparedStatementHandler#instantiateStatement,这些都是常规的jdbc操作,就不细看了。

处理sql的执行方法为PreparedStatementHandler#query

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
	PreparedStatement ps = (PreparedStatement) statement;
	// 执行
	ps.execute();
	return resultSetHandler.handleResultSets(ps);
  }

SimpleExecutor#doQuery(...)的执行流程如下:

  • 获取数据库连接
  • 获取PrepareStatement
  • 执行查询
  • 关闭PrepareStatement

SimpleExecutor的操作就是常规的jdbc操作。

3. ReuseExecutor

ReuseExecutor会缓存PreparedStatement,下一次执行相同的sql可重用。

我们依然分析doQuery(...)方法:

  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, 
		ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
	Configuration configuration = ms.getConfiguration();
	StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, 
			rowBounds, resultHandler, boundSql);
	// 获取 Statement
	Statement stmt = prepareStatement(handler, ms.getStatementLog());
	// 处理查询操作
	return handler.query(stmt, resultHandler);
  }

SimpleExecutor相比,ReuseExecutordoQuery(...)方法并没关闭Statement.我们来看看Statement的获取操作:

private Statement prepareStatement(StatementHandler handler, Log statementLog) 
		throws SQLException {
	Statement stmt;
	BoundSql boundSql = handler.getBoundSql();
	String sql = boundSql.getSql();
	// 根据sql语句判断是否有Statement缓存
	if (hasStatementFor(sql)) {
	  // 有缓存,直接使用
	  stmt = getStatement(sql);
	  applyTransactionTimeout(stmt);
	} else {
	  // 没缓存,获取数据库连接,再获取 Statement
	  Connection connection = getConnection(statementLog);
	  stmt = handler.prepare(connection, transaction.getTimeout());
	  // 缓存 Statement
	  putStatement(sql, stmt);
	}
	// 处理参数
	handler.parameterize(stmt);
	return stmt;
}

可以看到,ReuseExecutor获取Statement时,会先从缓存里获取,缓存里没有才会新建一个Statement,然后将新建的Statement添加到缓存中。从这里可以看出,ReuseExecutorReuse,复用的是Statement

我们再来看看缓存Statement的结构:

public class ReuseExecutor extends BaseExecutor {
  private final Map<String, Statement> statementMap = new HashMap<>();
  ...
  private Statement getStatement(String s) {
	return statementMap.get(s);
  }
  private void putStatement(String sql, Statement stmt) {
	statementMap.put(sql, stmt);
  }
}

由些可见,缓存Statement的是一个Mapkeysql语句,valueStatement.

4. BatchExecutor

BatchExecutor会将修改操作记录在本地,等待程序触发或有下一次查询时才批量执行修改操作,即:

  • 进行修改操作(insertupdatedelete)时,并不会立即执行,而是会缓存到本地
  • 进行查询操作(select)时,会先处理缓存到本地的修改操作,再进行查询操作
  • 也可行触发修改操作

从以上内容来看,这种方式似乎有大坑,列举几点如下:

  • 修改操作缓存到本地后,如果执行前遇到意外重启,缓存的记录会不会丢失?
  • 分布式环境下,多机共同协作,更新在A机上执行,查询在B机上执行,B机是不是不能查到B机的更新记录(B机的更新操作还在缓存中,并未执行)?

我们来看下BatchExecutor的更新操作,进入doUpdate(...)方法:

  public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
	final Configuration configuration = ms.getConfiguration();
	final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, 
			RowBounds.DEFAULT, null, null);
	final BoundSql boundSql = handler.getBoundSql();
	final String sql = boundSql.getSql();
	final Statement stmt;
	// 如果传入的sql是当前保存的 sql,直接使用
	if (sql.equals(currentSql) && ms.equals(currentStatement)) {
	  int last = statementList.size() - 1;
	  stmt = statementList.get(last);
	  applyTransactionTimeout(stmt);
	  handler.parameterize(stmt);// fix Issues 322
	  BatchResult batchResult = batchResultList.get(last);
	  batchResult.addParameterObject(parameterObject);
	} else {
	  // 创建连接,获取 Statement
	  Connection connection = getConnection(ms.getStatementLog());
	  stmt = handler.prepare(connection, transaction.getTimeout());
	  handler.parameterize(stmt);    // fix Issues 322
	  currentSql = sql;
	  currentStatement = ms;
	  statementList.add(stmt);
	  batchResultList.add(new BatchResult(ms, sql, parameterObject));
	}
	// 保存,等待之后批量执行
	handler.batch(stmt);
	return BATCH_UPDATE_RETURN_VALUE;
  }

BatchExecutor有成员变量会记录上一次执行的sqlMappedStatement,如果本次执行的sqlMappedStatement与上一次执行的相同,则直接使用上一次的Statement,否则就新建连接、获取Statement.

得到Statement后,会调用PreparedStatementHandler#batch方法:

  public void batch(Statement statement) throws SQLException {
	PreparedStatement ps = (PreparedStatement) statement;
	ps.addBatch();
  }

这个方法并没有执行,只是调用PreparedStatement#addBatch方法,将当前statement保存了起来。

PreparedStatement#addBatch方法如何使用呢?简单示意下:

// 获取连接
Connection connection = getConnection();
// 预编译sql
String sql = "xxx";
PreparedStatement statement = connection.prepareStatement(sql);   
//记录1
statement.setInt(1, 1);
statement.setString(2, "one");
statement.addBatch();   
//记录2
statement.setInt(1, 2);
statement.setString(2, "two");
statement.addBatch();   
//记录3
statement.setInt(1, 3);
statement.setString(2, "three");
statement.addBatch();   
//批量执行
int[] counts = statement.executeBatch();
// 关闭statment,关闭连接
...

BatchExecutordoUpdate(...)方法并没有执行sql语句,我们再来看看doQuery(...)方法:

  public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, 
		ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
	Statement stmt = null;
	try {
	  // 处理缓存中的 statements
	  flushStatements();
	  Configuration configuration = ms.getConfiguration();
	  StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, 
			rowBounds, resultHandler, boundSql);
	  // 获取连接,获取Statement,处理参数
	  Connection connection = getConnection(ms.getStatementLog());
	  stmt = handler.prepare(connection, transaction.getTimeout());
	  handler.parameterize(stmt);
	  // 执行查询
	  return handler.query(stmt, resultHandler);
	} finally {
	  // 关闭 Statement
	  closeStatement(stmt);
	}
  }

doQuery(...)方法会先调用flushStatements()方法,然后再处理查询操作,整个过程基本同SimpleExecutor一致,即”获取数据库连接-获取Statement-处理查询-关闭Statement“等几步。我们重点来看flushStatements()方法的流程.

flushStatements()方法最终调用的是BatchExecutor#doFlushStatements方法,代码如下:

  public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
	try {
	  List<BatchResult> results = new ArrayList<>();
	  if (isRollback) {
		return Collections.emptyList();
	  }
	  // 遍历的statementList,statementList就是缓存statement的结构
	  for (int i = 0, n = statementList.size(); i < n; i++) {
		Statement stmt = statementList.get(i);
		applyTransactionTimeout(stmt);
		BatchResult batchResult = batchResultList.get(i);
		try {
		  // 关键代码:stmt.executeBatch(),批量执行sql
		  batchResult.setUpdateCounts(stmt.executeBatch());
		  ...
		} catch (BatchUpdateException e) {
		  ...
		}
		results.add(batchResult);
	  }
	  return results;
	} finally {
	  ...
	}
  }

BatchExecutor#doFlushStatements方法的关键代码就是batchResult.setUpdateCounts(stmt.executeBatch());了 ,其中的stmt.executeBatch()就是批量执行更新操作了。

从以上分析可知,BatchExecutor#doUpdate(...)方法不会执行sql语句,只是把sql语句转换为Statement然后缓存起来,在执行BatchExecutor#doQuery(...)方法时,会先执行缓存起来的Statement,然后再执行查询操作,当然也可以手动调用BatchExecutor#flushStatements方法执行缓存的Statement

5. CachingExecutor

CachingExecutor不同于以上3种执行器,它是一个装饰类,可以从缓存中获取数据,实际干活的还是以上三种执行器之一:

public class CachingExecutor implements Executor {
  // 具体的执行器
  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();
  public CachingExecutor(Executor delegate) {
	this.delegate = delegate;
	delegate.setExecutorWrapper(this);
  }
  ...
}

从代码来看,它是Executor的子类,其中有一个成员变量delegate,它的类型为Executor,由构造方法传入。也就是说,在创建CachingExecutor时,会传入以上3种执行器之一,CachingExecutor会把它保存到成员变量delegate中。

CachingExecutor的query(...)方法如下:

  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, 
		ResultHandler resultHandler) throws SQLException {
	BoundSql boundSql = ms.getBoundSql(parameterObject);
	// 创建缓存key
	CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
	return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, 
		ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
	  throws SQLException {
	Cache cache = ms.getCache();
	// 操作缓存
	if (cache != null) {
	  flushCacheIfRequired(ms);
	  if (ms.isUseCache() && resultHandler ** null) {
		ensureNoOutParams(ms, boundSql);
		@SuppressWarnings("unchecked")
		// 从缓存中获取  
		List<E> list = (List<E>) tcm.getObject(cache, key);
		if (list ** null) {
		  // 实际处理查询的操作  
		  list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
		  // 添加到缓存中
		  tcm.putObject(cache, key, list); // issue #578 and #116
		}
		return list;
	  }
	}
	return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

从代码上来看,CachingExecutor在处理查询时,会先从缓存中获取,当缓存中不存在时,就执行具体执行器的query(xxx)方法。

与Mybatis执行器相似的内容:

Mybatis执行器

mybatis执行sql语句的操作是由执行器(Executor)完成的,mybatis中一共提供了3种Executor: 类型 名称 功能 REUSE 重用执行器 缓存PreparedStatement,下一次执行相同的sql可重用 BATCH 批量执行器 将修改操作记录在本地,等待程序触发或有下一

Mybatis源码解析之执行SQL语句

作者:郑志杰 mybatis 操作数据库的过程 // 第一步:读取mybatis-config.xml配置文件 InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); // 第二步:构建SqlSes

MyBatis源码之MyBatis中SQL语句执行过程

本文是MyBatis源码之MyBatis中SQL语句执行过程,使用图文并茂的方式,讲解了SQL语句执行过程,调用了哪些方法,和这些方法是如何调用的。

面试必会 --> MyBatis篇

什么是MyBatis Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直接编写原生态sql,可以严格控制sql执行性能,灵活度高。 MyBatis 可以使用 XM

『手写Mybatis』实现映射器的注册和使用

前言 如何面对复杂系统的设计? 我们可以把 Spring、MyBatis、Dubbo 这样的大型框架或者一些公司内部的较核心的项目,都可以称为复杂的系统。 这样的工程也不在是初学编程手里的玩具项目,没有所谓的 CRUD,更多时候要面对的都是对系统分层的结构设计和聚合逻辑功能的实现,再通过层层转换进行

MyBatis 的缓存机制

1. MyBatis 的缓存机制 @目录1. MyBatis 的缓存机制2. 准备工作3. MyBatis 的一级缓存3.1 一级缓存失效情况/条件4. MyBatis 的二级缓存5. MyBatis 集成 EhCache 第三方缓存6. 总结:7. 最后: 缓存(Cache) 缓存的作用:通过减少

MyBatis的逆向工程详细步骤操作

1. MyBatis的逆向工程详细步骤操作 @目录1. MyBatis的逆向工程详细步骤操作2. 逆向工程配置与生成2.1 MyBatis3Simple:基础版,只有基本的增删改查2.1.1 第一步:在pom.xml 中添加逆向工程插件2.1.2 第二步:配置 generatorConfig.xml

MyBatis 关于查询语句上配置的详细内容

1. MyBatis 关于查询语句上配置的详细内容 @目录1. MyBatis 关于查询语句上配置的详细内容2. 准备工作3. SQL查询结果,返回为POJO实体类型4. SQL查询结果,返回为List 集合类型5. SQL查询结果,返回为Map 集合6. SQL查询结果,返回为List

MyBatis 的在使用上的注意事项及其辨析

1. MyBatis 的在使用上的注意事项及其辨析 @目录1. MyBatis 的在使用上的注意事项及其辨析2. 准备工作3. #{ } 与 ${ } 的区别和使用{}3.1 什么情况下必须使用 $3.1.1 拼接表名3.1.2 批量删除3.1.3 模糊查询3.1.3.1 使用 ${ }的方式3.1

【简写Mybatis-02】注册机的实现以及SqlSession处理

学习源码一定一定不要太关注代码的编写,而是注意代码实现思想:通过设问方式来体现代码中的思想;方法:5W+1H