文章目录
前言
在上一章当中,我们通过一步步分析,通过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的核心,对于使用者来说屏蔽了各种复杂度,超强的反射机制将复杂的结果映射为目标对象,非常值得学习。