MyBatis工作流程-数据访问阶段


MyBatis源码学习系列文章目录



前言

在上一章当中,我们通过一步步分析,通过SqlSession获取mapper接口的实例获取的是一个JDK的代理对象,真正执行的时候会执行org.apache.ibatis.binding.MapperProxy#invoke方法,在该方法中,又会根据当前执行的方法映射为一个MapperMethod对象并做缓存,MapperMethod对象当中包含了对应MappedStatement的主键信息,同时对方法的主要信息进行了解析和包装(返回类型、参数类型等)。如下所示

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
	try {
		if (Object.class.equals(method.getDeclaringClass())) {
			return method.invoke(this, args);
		} else if (isDefaultMethod(method)) {
			return invokeDefaultMethod(proxy, method, args);
		}
	} catch (Throwable t) {
		throw ExceptionUtil.unwrapThrowable(t);
	}
	// 构建Mapper方法并缓存 方法返回参数类型 传入参数个数 类型等信息
	final MapperMethod mapperMethod = cachedMapperMethod(method);
	return mapperMethod.execute(sqlSession, args);
}

所以,数据访问阶段的起点是从org.apache.ibatis.binding.MapperMethod#execute开始的。

MapperMethod执行流程

执行mapper接口的方法其实就是调用MapperMethod的execute方法

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
        case INSERT: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.insert(command.getName(), param));
            break;
        }
        case UPDATE: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.update(command.getName(), param));
            break;
        }
        case DELETE: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.delete(command.getName(), param));
            break;
        }
        case SELECT:
            if (method.returnsVoid() && method.hasResultHandler()) {
                executeWithResultHandler(sqlSession, args);
                result = null;
            } else if (method.returnsMany()) {
                result = executeForMany(sqlSession, args);
            } else if (method.returnsMap()) {
                result = executeForMap(sqlSession, args);
            } else if (method.returnsCursor()) {
                result = executeForCursor(sqlSession, args);
            } else {
                // 将传入的参数转换为执行sql的参数
                Object param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.selectOne(command.getName(), param);
            }
            break;
        case FLUSH:
            // 刷新语句
            result = sqlSession.flushStatements();
            break;
        default:
            throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
        throw new BindingException("Mapper method '" + command.getName()
                + " attempted to return null from a method with a primitive return type (" + method.getReturnType()
                + ").");
    }
    return result;
}

可以看到新增、修改和删除这些操作首先都是将传入的参数进行转化。其实SELECT当中除了hasResultHandler情况也应该是这样的。只是returnsMany、returnsMap、returnsCursor当中都把同样的代码放到了下一层的代码中,这里是可以进行代码调整的。
在这里插入图片描述
这个参数转化是做了什么呢?

City findByState(String state);

比如调用以上的接口传递的删除为‘CA’,经过转化之后为仍然为’CA’
在这里插入图片描述
如果是如下方法(包含了Param注解)

int deleteByState(@Param("state") String state);

在这里插入图片描述
此时调用参数会作为一个Map保存。在后续解析参数的时候(org.apache.ibatis.executor.parameter.ParameterHandler),既可以根据索引值获取参数值,也可以通过map的key值(state)获取参数值。做完参数转化之后,大部分的工作就会交给SqlSession来完成了,当然最后还会做一些结果处理,比如转化为方法返回类型。大致的流程都是一样的,比如executeForMany

private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    1. 进行参数的转化
    Object param = method.convertArgsToSqlCommandParam(args);
    2. 通过sqlSession发起数据库访问 根据是否包含分页参数RowBounds调用不同的接口 
    if (method.hasRowBounds()) {
        RowBounds rowBounds = method.extractRowBounds(args);
        result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
    } else {
        result = sqlSession.<E>selectList(command.getName(), param);
    }
    3. 如果不是目标对象 尝试转成目标对象
    // issue #510 Collections & arrays support 
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
        3.1 转成数组类型
        if (method.getReturnType().isArray()) {
            return convertToArray(result);
        } else {
        3.2 转成目标集合类型 上面返回的是List的类型的 如果目标需要是Set类型的 就需要尝试转换了
            return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
        }
    }
    return result;
}

总结下MapperMethod执行流程:
1. 转换参数
2. 通过sqlSession发起数据库访问
3. 转换结果为方法的返回类型

Executor的三种模式

SqlSession的功能都是基于Executor来实现的,Executor是MyBatis核心接口之一,定义了数据库操作最基本的方法,其内部遵循JDBC规范完成数据库的访问,Executor的继承结构如下所示:
在这里插入图片描述
其中CachingExecutor主要是用于支持二级缓存功能的,主要的数据库访问都是交给内部的delegate来完成的。在默认情况下,都是使用的SimpleExecutor。具体使用哪个是在获取SqlSession时通过executorType来决定的,具体可以查看上一章创建SqlSession的流程。

MyBatis的执行器组件是使用模板模式的典型应用,其中的BaseExecutor作为执行器抽象类,实现了Executor的绝大部分方法,主要提供了一级缓存管理和事务管理的能力,其他子类需要实现的抽象方法为:doUpdate、doQuery、doQueryCursor和doFlushStatements。

在DefaultSqlSession中执行insert、update、delete方法最后都是调用Executor的update方法。

@Override
public int insert(String statement) {
    return insert(statement, null);
}

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

@Override
public int update(String statement) {
    return update(statement, null);
}

@Override
public int update(String statement, Object parameter) {
    try {
        dirty = true;
        MappedStatement ms = configuration.getMappedStatement(statement);
        return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

而org.apache.ibatis.executor.BaseExecutor#update方法清空完了一级缓存之后,然后就将任务交给了子类去完成了。

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
	ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
	if (closed) {
		throw new ExecutorException("Executor was closed.");
	}
	// 清空一级缓存
	clearLocalCache();
	// 执行更新操作(包括新增)
	return doUpdate(ms, parameter);
}

首先SimpleExecutor每次执行都会创建一个statement,执行完成之后就会关闭。

@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
	Statement stmt = null;
	try {
		Configuration configuration = ms.getConfiguration();
		1. 创建Statement处理器
		StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null,
				null);
		2. 创建Statement 		
		stmt = prepareStatement(handler, ms.getStatementLog());
		3. 执行Statement 
		return handler.update(stmt);
	} finally {
	    4. 关闭Statement 
		closeStatement(stmt);
	}
}

而其中的

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
	Statement stmt;
	
	1. 获取数据库的连接 在BaseExecutor中定义
	Connection connection = getConnection(statementLog);
	
	2. 通过Statement处理器创建Statement对象 比如connection.prepareStatement(sql)
	stmt = handler.prepare(connection, transaction.getTimeout());
	
	3. 设置参数 比如 ps.setBoolean(i, parameter)
	handler.parameterize(stmt);
	return stmt;
}

而ReuseExecutor会将statement存入到map中,每次尝试从这个map中查找statement,这样就不会重复创建statement了。所以doUpdate的方法与SimpleExecutor基本是一致的,除了没有finally块中关闭Statement。主要的差别是在prepareStatement中。

这里使用一个Map来存放Statement语句,键就是boundSql中的sql语句
private final Map<String, Statement> statementMap = new HashMap<String, Statement>();

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
   
    1. 如果已经存在Statement
    if (hasStatementFor(sql)) {
        直接从缓存中获取Statement 
        stmt = getStatement(sql);
        applyTransactionTimeout(stmt);
    } else {
    
    2. 缓存中不存在Statement 
        Connection connection = getConnection(statementLog);
        stmt = handler.prepare(connection, transaction.getTimeout());
        
        创建完Statement之后 需要缓存下来
        putStatement(sql, stmt);
    }
    handler.parameterize(stmt);
    return stmt;
}

private boolean hasStatementFor(String sql) {
    try {
        return statementMap.keySet().contains(sql) && !statementMap.get(sql).getConnection().isClosed();
    } catch (SQLException e) {
        return false;
    }
}

private Statement getStatement(String s) {
    return statementMap.get(s);
}

private void putStatement(String sql, Statement stmt) {
    statementMap.put(sql, stmt);
}

通过ReuseExecutor可以减少编译语句的次数,但是会增大内存消耗,这个是属于MyBatis的一个优化防范,另外JDBC本身也有一个优化方案,也就是Batch模式,也就是批量执行所有的更新语句(insert、update、delete),本质是基于jdbc的batch操作实现批处理。首先通过java.sql.Statement#addBatch方法将批量的Statement语句添加到列表当中,此时并不会将语句发送到数据库,当需要发送数据库时,再调用一次executeBatch方法完成提交。但是需要注意的是不是所有的数据库都支持批量模式,可以通过java.sql.DatabaseMetaData#supportsBatchUpdates方法查询是否支持批量模式。有的数据库默认情况下不支持批量模式,即使以上方法返回true。开启参数之后会支持批量模式,比如MySQL需要开启参数rewriteBatchedStatements才能真正支持批量模式。而MyBatis的BatchExecutor在使用JDBC的batch之上,还做了一点优化,就是通常批量模式下,执行的语句都是一致的,所以BatchExecutor会记录上一条执行的语句,如果下一次是一样的语句,就会使用上一次创建的Statement。

@Override
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;
	1. 查询语句与上一次语句相同 则重复使用上一次的编译结果
	if (sql.equals(currentSql) && ms.equals(currentStatement)) {
		int last = statementList.size() - 1;
		stmt = statementList.get(last);
		applyTransactionTimeout(stmt);
		handler.parameterize(stmt);// fix Issues 322
		
		1.1 直接使用已经创建好的batchResult对象 只需修改parameterObjects
		BatchResult batchResult = batchResultList.get(last);
		batchResult.addParameterObject(parameterObject);
	} else {
	    
	    1.2 语句不相同 获取连接 创建语句 并记录
		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.parameterize(stmt);
	2. 批量模式第一步 添加到本地队列
	handler.batch(stmt);
	return BATCH_UPDATE_RETURN_VALUE;
}

在上面的doUpdate当中出现了batch,但是executeBatch并没有执行,那么这一步是在何时执行的呢?如下所示,是在doFlushStatements方法中实现的。
在这里插入图片描述
在这doFlushStatements是干啥用的?在org.apache.ibatis.executor.SimpleExecutor#doFlushStatements只是直接返回一个空的List,在org.apache.ibatis.executor.ReuseExecutor#doFlushStatements在返回一个空的List之前,会从缓存当中拿出所有的Statement一个一个进行关闭,然后再清空。这个方法是在SqlSession中定义的,可以由外面手动调用。

/**
 * Flushes batch statements.
 * @return BatchResult list of updated records
 * @since 3.0.6
 */
List<BatchResult> flushStatements();

除此之外,在事务提交和回滚时,BaseExecutor也会执行这个逻辑。
在这里插入图片描述

针对于BatchExecutor在执行doQuery也会调用closeStatement,也就是说如果使用的是BATCH模式,在查询时会自动提交前面批量执行的insert、update、delete操作。

总结一下:不同模式的BaseExecutor实现类主要是针对Statement的优化,尽量重用Statement,延迟Statement的关闭。
1. SIMPLE 就是普通的执行器;每次执行完毕就会关闭Statement
2. REUSE 执行器会重用预处理语句(PreparedStatement); 直到事务完成之后才会关闭Statement。
3. BATCH 执行器不仅重用语句还会执行批量更新(一次性提交到服务器减少多次请求服务器,也能减少网络压力)。
但是这一切都是在一个事务内的,因为事务一结束,就会调用flushStatements清空缓存并且关闭查询语句。

Executor的查询流程

在上面我们分析了三种Executor的主要差别,接下来我们分析Executor的查询流程以及此过程中涉及到的三个重要小弟。大体的流程其实都差不多,参考org.apache.ibatis.executor.SimpleExecutor#doQuery代码如下。

@Override
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();
		1. 获取StatementHandler
		StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds,
				resultHandler, boundSql);
		2. 预编译和设置参数
		stmt = prepareStatement(handler, ms.getStatementLog());
		3. 执行查询
		return handler.<E>query(stmt, resultHandler);
	} finally {
		closeStatement(stmt);
	}
}

首先在这里跟上面doUpdate类似,都会获取一个StatementHandler对象,而后续的预编译、执行查询都是由它来完成的,也就是说其实Executor除了处理缓存、事务处理还有针对Statement的优化之外,其他的编译查询语句为Statement、参数设置、结果处理都是由StatementHandler来处理的。通过Configuration获取StatementHandler的源码如下

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
                                            Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject,
            rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

可以看出,其实这里返回的是一个RoutingStatementHandler。而这个RoutingStatementHandler并不是真正处理业务的,仅仅是一个路由器。在实际执行时,会根据MappedStatement的statementType选择不同的StatementHandler。

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
		ResultHandler resultHandler, BoundSql boundSql) {
	// 策略模式获取真正使用的statementHandler
	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());
	}
}

这三个StatementHandler主要是针对不同类型的Statement的,CallableStatementHandler是针对CallableStatement的,而PreparedStatementHandler是针对PreparedStatement,SimpleStatementHandler是针对普通的Statement的。它们之间的继承关系如下所示
在这里插入图片描述

statementType 可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。

这里的StatementHandler的继承结构和上面的Executor是一模一样的,都是采用的模板模式,主要的逻辑是在BaseStatementHandler当中实现的。

protected final ResultSetHandler resultSetHandler;
protected final ParameterHandler parameterHandler;

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();
	// issue #435, get the key before calculating the statement
	if (boundSql == null) {
		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);
}

这里出现了两个从未出现的类ParameterHandler和ResultSetHandler,而且获取的方式也是从configuration中创建的。
在这里插入图片描述
ParameterHandler、ResultSetHandler和StatementHandler的创建过程是极其相似的,都是简单的对象传递,同时它们也都是支持插件。ParameterHandler是在XMLLanguageDriver#createParameterHandler中创建的,为DefaultParameterHandler类型,而ResultSetHandler则为DefaultResultSetHandler类型,都是默认实现类。其实这三个对象作为Executor的小弟,是真正干活的,首先StatementHandler是用来处理Statement的,而ParameterHandler用于设置参数的,而ResultSetHandler在执行数据库操作之后处理返回结果的。
在默认情况下,使用的为PreparedStatementHandler,我们看一下这个这三个类在查询过程中的具体流程。

1. prepare预编译

创建Statement语句的主体逻辑是在父类方法BaseStatementHandler#prepare中实现的。

@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
	ErrorContext.instance().sql(boundSql.getSql());
	Statement statement = null;
	try {
		1. 执行查询语句的编译
		statement = instantiateStatement(connection);
		
		2. 设置QueryTimeout时间 JDBC规范
		对应的配置优先级 mappedStatement > configuration , 具体还要和transactionTimeout比较大小 取值小者
		setStatementTimeout(statement, transactionTimeout);
		
		4. 设置FetchSize JDBC规范 当此Statement生成的ResultSet对象需要更多行时,向JDBC驱动程序提供提示,提示应从数据库中获取的行数。 如果指定的值为零,则忽略提示。 默认值为零。
		对应的配置优先级为 mappedStatement > configuration
		setFetchSize(statement);
		return statement;
	} catch (SQLException e) {
		closeStatement(statement);
		throw e;
	} catch (Exception e) {
		closeStatement(statement);
		throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
	}
}

protected abstract Statement instantiateStatement(Connection connection) throws SQLException;

setQueryTimeout 将驱动程序等待Statement对象执行的秒数设置为给定的秒数。 默认情况下,对运行中的语句完成的时间没有限制。 如果超出限制,则抛出SQLTimeoutException。 JDBC驱动程序必须将此限制应用于execute,executeQuery和executeUpdate方法。
注意:JDBC驱动程序实现也可以将此限制应用于ResultSet方法(有关详细信息,请咨询驱动程序供应商文档)。
注意:在进行语句批处理的情况下,实现的定义是确定是否将超时应用于通过addBatch方法添加的单个SQL命令,还是应用于executeBatch方法调用的整批SQL命令(请咨询驱动程序供应商文档) 有关详细信息)。

主要的instantiateStatement是在子类中实现的

@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
	String sql = boundSql.getSql();
	1. 包含KeyGenerator 需要返回关键列的值 仅支持insert方法
	if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
		String[] keyColumnNames = mappedStatement.getKeyColumns();
		if (keyColumnNames == null) {
			return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
		} else {
			return connection.prepareStatement(sql, keyColumnNames);
		}
	} else if (mappedStatement.getResultSetType() != null) {
	2. 设置resultSetType和resultSetConcurrency参数
		return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(),
				ResultSet.CONCUR_READ_ONLY);
	} else {
	3. 普通预编译模式
		return connection.prepareStatement(sql);
	}
}

这里的java.sql.Connection#prepareStatement(java.lang.String)方法接收可能包含一个或多个“?”的SQL语句IN参数占位符,创建一个PreparedStatement对象,用于将参数化的SQL语句发送到数据库。可以预编译带有或不带有IN参数的SQL语句,并将其存储在PreparedStatement对象中。然后可以使用该对象多次有效地执行该语句。

注意:此方法已优化用于处理受益于预编译的参数SQL语句。如果驱动程序支持预编译,则prepareStatement方法会将语句发送到数据库以进行预编译。某些驱动程序可能不支持预编译。在这种情况下,在执行PreparedStatement对象之前,该语句可能不会发送到数据库。这对用户没有直接影响;但是,它确实会影响哪些方法抛出某些SQLException对象。默认情况下,使用返回的PreparedStatement对象创建的结果集的类型为TYPE_FORWARD_ONLY,并发级别为CONCUR_READ_ONLY。可以通过调用getHoldability来确定创建的结果集的可保存性。

PreparedStatement prepareStatement(String sql, int autoGeneratedKeys)
    throws SQLException;

java.sql.Connection#prepareStatement(java.lang.String, int)中的第二个参数autoGeneratedKeys用于暗示自动生成的主键值是否需要返回,比如为Statement.RETURN_GENERATED_KEYS或者Statement.NO_GENERATED_KEYS这两个值中的一个。如果SQL语句不是INSERT语句,也不是能够返回自动生成的键的SQL语句(此类语句的列表是特定于供应商的),则忽略此参数。定义如下

/**
 * The constant indicating that generated keys should be made
 * available for retrieval.
 *
 * @since 1.4
 */
int RETURN_GENERATED_KEYS = 1;

/**
 * The constant indicating that generated keys should not be made
 * available for retrieval.
 *
 * @since 1.4
 */
int NO_GENERATED_KEYS = 2;

需要注意的是如果对应的数据库驱动不支持RETURN_GENERATED_KEYS模式,会抛出SQLFeatureNotSupportedException异常。

PreparedStatement prepareStatement(String sql, int autoGeneratedKeys)
    throws SQLException;

java.sql.Connection#prepareStatement(java.lang.String, java.lang.String[])可以通过第二个columnNames指定需要返回的行值。如果SQL语句不是INSERT语句,也不是能够返回自动生成的键的SQL语句(此类语句的列表是特定于供应商的),则忽略此参数。

PreparedStatement prepareStatement(String sql, int resultSetType,
                                   int resultSetConcurrency)  throws SQLException;

这种形式的prepareStatement则用于指定resultSetType和resultSetConcurrency,如果对应的JDBC驱动不支持这些参数,则会抛出SQLFeatureNotSupportedException异常。对应的定义在java.sql.ResultSet这个类当中。如下所示

/**
 * The constant indicating the type for a <code>ResultSet</code> object
 * whose cursor may move only forward.
 * @since 1.2
 */
int TYPE_FORWARD_ONLY = 1003;

/**
 * The constant indicating the type for a <code>ResultSet</code> object
 * that is scrollable but generally not sensitive to changes to the data
 * that underlies the <code>ResultSet</code>.
 * @since 1.2
 */
int TYPE_SCROLL_INSENSITIVE = 1004;

/**
 * The constant indicating the type for a <code>ResultSet</code> object
 * that is scrollable and generally sensitive to changes to the data
 * that underlies the <code>ResultSet</code>.
 * @since 1.2
 */
int TYPE_SCROLL_SENSITIVE = 1005;
/**
 * The constant indicating the concurrency mode for a
 * <code>ResultSet</code> object that may NOT be updated.
 * @since 1.2
 */
int CONCUR_READ_ONLY = 1007;

/**
 * The constant indicating the concurrency mode for a
 * <code>ResultSet</code> object that may be updated.
 * @since 1.2
 */
int CONCUR_UPDATABLE = 1008;

从以上分析不难看出,其实PreparedStatementHandler#instantiateStatement仅仅是针对JDBC中的不同预编译方法做选择而已。

2. parameterize设置参数

通过上一个步骤,完成了Statement的创建,接下来是设置参数

@Override
public void parameterize(Statement statement) throws SQLException {
	parameterHandler.setParameters((PreparedStatement) statement);
}

这个过程是交给parameterHandler完成的。设置参数最终是通过PreparedStatement的相关setXXX方法来完成的。比如java.sql.PreparedStatement#setBoolean方法,要选择对方法必须获取到参数的类型以及参数的值,所以在setParameters方法中绝大部分的逻辑用于匹配传入的参数值。如果传入的没有参数和单个参数都很好处理,直接将传入的参数作为value即可。但是如果是多个参数的话,只有三种方式,第一种是通过Map,第二中是通过自定义对象,第三种就是通过@Param注解。

String propertyName = parameterMapping.getProperty();
// issue #448 ask first for additional params
if (boundSql.hasAdditionalParameter(propertyName)) {
	value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
	value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
	value = parameterObject;
} else {
	MetaObject metaObject = configuration.newMetaObject(parameterObject);
	value = metaObject.getValue(propertyName);
}

在以上的这段解析值的过程中,以上三种方式都只会进入到最后一个分支中(Map不存在对应的TypeHandler,数据库不支持Map格式的吧!)。而在前面介绍MapperMethod中转换参数时,对于@Param注解的参数也是转成了Map包装的。而转成了metaObject形式,通过传入对应的属性名称反射获取,因为@Param中定义的就是对应parameterMapping的属性名称(在替换sql语句中占位符为?的时候保存的)。如果传递的是具体的对象,通过属性名称获取值更不是问题了。
解析出对应属性的值之后,再通过TypeHandler设置参数就不是什么大问题了。

TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
	jdbcType = configuration.getJdbcTypeForNull();
}
try {
	// 设置参数
	typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
	throw new TypeException(
			"Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
	throw new TypeException(
			"Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}

3. query执行查询

设置完参数之后,就可以执行语句了。

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
	PreparedStatement ps = (PreparedStatement) statement;
	
	1. 执行数据库操作
	ps.execute(); 
	
	2. 处理查询结果
	return resultSetHandler.<E>handleResultSets(ps);
}

执行数据库操作之后,通过resultSetHandler来处理接下来的工作了。对应的实现在DefaultResultSetHandler#handleResultSets方法当中。由于这个方法的实现非常复杂,这里使用一个案例来讲解,比如现在的查询方法如下

List<City> findAll();
<resultMap id="BaseResultMap" type="sample.mybatis.domain.City">
    <id property="id" column="city_id" jdbcType="INTEGER"/>
    <result property="name" column="city_name" jdbcType="VARCHAR"/>
    <result property="state" column="state" jdbcType="VARCHAR"/>
    <association property="country" javaType="sample.mybatis.domain.Country">
        <id property="id" column="country_id" jdbcType="INTEGER"/>
        <result property="name" column="country_name" jdbcType="VARCHAR"/>
        <result property="continent" column="continent" jdbcType="VARCHAR"/>
    </association>
    <collection property="hotels" ofType="sample.mybatis.domain.Hotel" columnPrefix="hotel_">
        <id property="id" column="id" javaType="long" jdbcType="BIGINT"
            typeHandler="org.apache.ibatis.type.LongTypeHandler"/>
        <result property="name" column="name" jdbcType="VARCHAR"/>
        <result property="address" column="address" jdbcType="VARCHAR"/>
        <result property="zip" column="zip" jdbcType="VARCHAR"/>
    </collection>
</resultMap>

<sql id="baseStatement">
    ci.city_id, ci.name as city_name, ci.state,
    co.country_id,co.name as country_name,co.continent,
    ho.hotel_id,ho.name as hotel_name, ho.address as hotel_address,ho.zip as hotel_zip
</sql>

<select id="findAll" resultMap="BaseResultMap">
    select
    <include refid="baseStatement"/>
    from city ci
    inner join country co on co.country_id = ci.country_id
    inner join hotel ho on ho.city_id = ci.city_id
</select>

假设数据库的结果如下

Columns: CITY_ID, CITY_NAME, STATE, COUNTRY_ID, COUNTRY_NAME, CONTINENT, HOTEL_ID, HOTEL_NAME, HOTEL_ADDRESS, HOTEL_ZIP
Row: 1, San Francisco, CA, 23, US, North America, 10001, Conrad Treasury Place, William & George Streets, 4001
Row: 1, San Francisco, CA, 23, US, North America, 10002, The Ritz-Carlton, 600 Stockton Street, 4001
Row: 2, Atlanta, GA, 23, US, North America, 20001, Jw Marriott, 3300 Lenox Road NE, 30322
Row: 2, Atlanta, GA, 23, US, North America, 20002, Zoek, Downtown City Center, 30322
Row: 2, Atlanta, GA, 23, US, North America, 20003, St. Regis, William & George Streets, 30322
Row: 3, Norfolk, VA, 23, US, North America, 30004, Candlewood, City Center, 4001
Row: 3, Norfolk, VA, 23, US, North America, 30005, Staybridage, Virginia Beach, 4001

首先这里有几个难点,第一个就是返回结果肯定是List<City>类型的,那如何反射创建这个对象呢?在DefaultResultSetHandler中有一个resultHandler属性,在返回结果如果为多个的情况下,这个属性的实现类为DefaultResultHandler,对应的实现如下

public class DefaultResultHandler implements ResultHandler<Object> {

    private final List<Object> list;

    public DefaultResultHandler() {
        list = new ArrayList<Object>();
    }

    @SuppressWarnings("unchecked")
    public DefaultResultHandler(ObjectFactory objectFactory) {
        list = objectFactory.create(List.class);
    }

    @Override
    public void handleResult(ResultContext<? extends Object> context) {
        list.add(context.getResultObject());
    }

    public List<Object> getResultList() {
        return list;
    }
}

这里面有一个list属性,其实MyBatis在解析返回数据的时候,一行一行数据处理,然后将结果放到这个list当中,处理完成之后,直接将这个list作为ResultList返回了。所以不需要反射出一个List<City>对象。

然后就是第二个问题,在上面返回的数据当中,CITY_ID相同的应该算同一条对象,而不是作为多个对象。所以在DefaultResultSetHandler存在如下一个属性nestedResultObjects,其实就是一个缓存。

// nested resultmaps 全局有效
private final Map<CacheKey, Object> nestedResultObjects = new HashMap<CacheKey, Object>();

每次在获取一个结果集的时候,会根据ResultMap的id、主键列名称、对应的值构建CacheKey,如下所示
在这里插入图片描述
如果在nestedResultObjects缓存当中存在相同的CacheKey,则直接使用缓存中创建好的对象就好了。
在这里插入图片描述
如果主键值不同,则继续解析,解析完了,缓存起来。
在这里插入图片描述
解决了数据主键重复的问题,还有另一个问题,就是同一行数据中可能需要解析为多个属性对象,比如上面就涉及到首先解析City,然后解析country,然后再解析Hotel。而且还需要将country和Hotel设置到City当中,怎样才能保证不会将country设置到错误的City当中呢?
当然了这里其实是有一个前提的,country和Hotel在resultMap的关系必须要正确。在前面解析resultMap的过程中,country和Hotel对应的是两个内嵌的resultMap,而在City的resultMap中又是对应的两个ResultMapping属性,所以在解析的过程中,根据resultMap的resultMappings属性列表一个一个的解析,当遇到内嵌的resultMap则递归解析,所以对于一条返回结果,

Columns: CITY_ID, CITY_NAME, STATE, COUNTRY_ID, COUNTRY_NAME, CONTINENT, HOTEL_ID, HOTEL_NAME, HOTEL_ADDRESS, HOTEL_ZIP
Row: 1, San Francisco, CA, 23, US, North America, 10001, Conrad Treasury Place, William & George Streets, 4001

肯定首先是解析City(1, San Francisco, CA),然后解析Country(23, US, North America),并设置到正在解析流程中的City中,然后再解析Hotel(10001, Conrad Treasury Place, William & George Streets, 4001)并设置到正在解析流程中的City中。所以在DefaultResultSetHandler中存在如下这个属性

// 解析子对象属性时有效
private final Map<String, Object> ancestorObjects = new HashMap<String, Object>();

所以在每次解析内嵌属性的时候,就会保存一个值,解析完了,再移除,免得影响下一次结果。比如在.DefaultResultSetHandler#getRowValue方法中存在以下片段
在这里插入图片描述
对应过程中的缓存结果
在这里插入图片描述
最后一个比较重要的就是ResultSetWrapper类,这是对数据库返回结果集的一个封装类。

private ResultSetWrapper getFirstResultSet(Statement stmt) throws SQLException {
    ResultSet rs = stmt.getResultSet();
    while (rs == null) {
        // move forward to get the first resultset in case the driver
        // doesn't return the resultset as the first result (HSQLDB 2.1)
        if (stmt.getMoreResults()) {
            rs = stmt.getResultSet();
        } else {
            if (stmt.getUpdateCount() == -1) {
                // no more results. Must be no resultset
                break;
            }
        }
    }
    return rs != null ? new ResultSetWrapper(rs, configuration) : null;
}

通过ResultSetWrapper封装结果集、以及结果集元数据、列的个数,并且获取列的名称、类型以及对象的类名称,这些在查找TypeHandler,并根据TypeHandler将数据库中的值转成目标对象中的属性值的时候尤其重要。所以在这里ResultSetWrapper把一些基础工作都完成了。

public ResultSetWrapper(ResultSet rs, Configuration configuration) throws SQLException {
	super();
	this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
	this.resultSet = rs;
	// 获取查询结果元数据
	final ResultSetMetaData metaData = rs.getMetaData();
	// 获取查询结果元数据列的个数
	final int columnCount = metaData.getColumnCount();
	// 编译查询结果元数据
	for (int i = 1; i <= columnCount; i++) {
		columnNames.add(configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i));
		jdbcTypes.add(JdbcType.forCode(metaData.getColumnType(i)));
		classNames.add(metaData.getColumnClassName(i));
	}
}

针对结果集封装之后,接下来就是要根据ResultMap对象一个一个值的处理了。

// 获取ResultMap 与结果集映射
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
    1. 处理结果映射
    ResultMap resultMap = resultMaps.get(resultSetCount);
    handleResultSet(rsw, resultMap, multipleResults, null);
    2. 有的数据库支持多个结果集
    rsw = getNextResultSet(stmt);
    3.  清空缓存nestedResultObjects
    cleanUpAfterHandlingResultSet();
    resultSetCount++;
}

这里拷出来支持多个返回结果集的情况,一般都是一个。

public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler,
                            RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    if (resultMap.hasNestedResultMaps()) {
        // 含有Collection元素不支持分页
        ensureNoRowBounds();
        checkResultHandler();
        handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
        handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
}

这里区分是否存在内嵌的ResultMap,走不同的逻辑,因为不包含的话,只需要一个一个属性处理即可,而含有内嵌的还要考虑缓存的问题。

private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap,
                                               ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    final DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
    1. 根据RowBounds参数做逻辑分页
    skipRows(rsw.getResultSet(), rowBounds);
    Object rowValue = previousRowValue;
    2. 考虑RowBounds逻辑分页 满足分页条件或者不存在分页条件 resultSet中还有值
    while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
        3. 根据鉴别器处理返回值
        final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
        4. 此处构造主键 会通过resultSet读取主键行
        final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
        5. 如果是存在相同的主键 则直接取缓存的对象
        Object partialObject = nestedResultObjects.get(rowKey);
        // issue #577 && #542
        if (mappedStatement.isResultOrdered()) {
           
            if (partialObject == null && rowValue != null) {
                nestedResultObjects.clear();
                storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
            }
            rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
        } else {
            6. 将当前行数据解析为对象
            rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
            if (partialObject == null) {
                 
                 7. 如果不存在缓存 而且上一次解析对象不为空 则要考虑将结果保存到resultHandler的List当中
                storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
            }
        }
    }
    if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {
        storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
        previousRowValue = null;
    } else if (rowValue != null) {
        previousRowValue = rowValue;
    }
}

解析行数据就是与根据对应的ResultMap一一映射即可。

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix,
                           Object partialObject) throws SQLException {
    final String resultMapId = resultMap.getId();
    Object rowValue = partialObject;
    1. 根据主键已经找到解析好的对象 存在相同的主键值
    if (rowValue != null) {
    
        2.1 存在相同的主键值 只需要解析内嵌结果值 
        final MetaObject metaObject = configuration.newMetaObject(rowValue);
        putAncestor(rowValue, resultMapId);
        处理内嵌结果
        applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);
        ancestorObjects.remove(resultMapId);
    } else {
        final ResultLoaderMap lazyLoader = new ResultLoaderMap();
        2.2 不存在相同的主键值 则创建一个空结果对象
        rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
        if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
            final MetaObject metaObject = configuration.newMetaObject(rowValue);
            boolean foundValues = this.useConstructorMappings;
            if (shouldApplyAutomaticMappings(resultMap, true)) {
                foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
            }
            2.3 一个一个设置属性值
            foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix)
                    || foundValues;
            putAncestor(rowValue, resultMapId);
            2.4 处理内嵌结果
            foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true)
                    || foundValues;
            ancestorObjects.remove(resultMapId);
            foundValues = lazyLoader.size() > 0 || foundValues;
            rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
        }
        if (combinedKey != CacheKey.NULL_CACHE_KEY) {
            nestedResultObjects.put(combinedKey, rowValue);
        }
    }
    return rowValue;
}

对应的属性值映射

/**
 * 属性值映射
 *
 * @param rsw          结果集包装对象
 * @param resultMap    用户定义的结果映射对象
 * @param metaObject   结果元数据对象
 * @param lazyLoader
 * @param columnPrefix 列前缀
 * @return
 * @throws SQLException
 */
private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,
                                      ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
    final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
    boolean foundValues = false;
    final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
    for (ResultMapping propertyMapping : propertyMappings) {
        String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
        if (propertyMapping.getNestedResultMapId() != null) {
            // the user added a column attribute to a nested result map, ignore it
            column = null;
        }
        if (propertyMapping.isCompositeResult()
                || (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))
                || propertyMapping.getResultSet() != null) {
            Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader,
                    columnPrefix);
            // issue #541 make property optional
            final String property = propertyMapping.getProperty();
            if (property == null) {
                continue;
            } else if (value == DEFERED) {
                foundValues = true;
                continue;
            }
            if (value != null) {
                foundValues = true;
            }
            if (value != null || (configuration.isCallSettersOnNulls()
                    && !metaObject.getSetterType(property).isPrimitive())) {
                // gcode issue #377, call setter on nulls (value is not 'found') 反射设置值
                metaObject.setValue(property, value);
            }
        }
    }
    return foundValues;
}

可以看到属性值的映射无非就是通过反射来进行的,关于DefaultResultSetHandler的逻辑就介绍到这里了,这里的代码量比较多,但真正复杂的无非就是几个缓存以及内嵌ResultMap的处理了。

总结

MyBatis数据访问阶段涉及的信息量非常多,也是整个MyBatis的核心,对于使用者来说屏蔽了各种复杂度,超强的反射机制将复杂的结果映射为目标对象,非常值得学习。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lang20150928

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值