一、查询执行大致流程
在MyBatis中,查询执行的大致流程如下:
对应的时序图如下:
二、MapperProxy绑定MappedStatement
在MyBatis Mapper Bean初始化深度解析中说过,mapper bean就是MapperProxy通过jdk动态代理实现的,所以,执行mapper bean中的方法时,就是执行MapperProxy中的invoke方法。执行查询时,其debug信息如下:
在org.apache.ibatis.binding.MapperProxy#cachedMapperMethod方法中,会判断是否缓存了方法对应的MapperMethod,如果缓存了,就直接返回缓存的MapperMethod。MapperMethod中的SqlCommand就是需要执行的sql语句的封装,在SqlCommand会根据statementId(mapper的interfacename+"."+methodname)去查找对应的MappedStatement,然后返回这个查找到的MappedStatement。代码如下:
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
if (ms == null) {
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
String statementId = mapperInterface.getName() + "." + methodName;
if (configuration.hasStatement(statementId)) {
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
return null;
}
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
}
这里的statementId就是interfacename+"."+methodname,也是mapper中的namespace+"."+id。
三、SimpleExecutor设置查询参数与缓存设置
1、查询参数设置
跟随代码一步一步debug,会到达SimpleExecutor的query方法(其实是父类BaseExecutor的query方法),其代码如下:
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
这里的queryFromDatabase方法会调用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();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
这里会获取connection,并且会获取statement,从connection中获取statement的代码如下:
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
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) {
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
} else {
return connection.prepareStatement(sql);
}
}
可以看出,这里获取的是PrepareStatement,这样可以防止sql注入。在handler.parameterize(stmt)中,会调用typeHandler设置查询参数,具体如下:
这样以后,查询语句就变成如下sql了:
SELECT * FROM CNT_USER WHERE name ='zhangsan'
2、缓存设置
查询结果返回后,会将查询结果保存在一级缓存中,以便同一个会话中的后续相同查询使用。
缓存key的生产规则:
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
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);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
缓存key有MappedStatement的id、查询sql、查询参数、环境等组成。样例值如下:
461375566:-715040544:com.iwill.mybatis.dao.mapper.ext.UserMapperExt.findUserListByName:0:2147483647:SELECT * FROM CNT_USER WHERE name =?:zhangsan:SqlSessionFactoryBean
缓存查询:
将查询结果放入缓存中:
四、PreparedStatementHandler执行查询
在方法org.apache.ibatis.executor.statement.PreparedStatementHandler#query中,会调用PreparedStatement的execute方法来执行查询。
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.<E> handleResultSets(ps);
}
五、DefaultResultSetHandler进行查询结果处理
在查询结果返回时,会将SQL查询的结果映射到resultMap制定的类型。例如findUserListByName:
<resultMap id="BaseResultMap" type="com.iwill.mybatis.dao.model.UserDTO">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="age" jdbcType="INTEGER" property="age" />
</resultMap>
<select id="findUserListByName" parameterType="java.lang.String" resultMap="BaseResultMap">
SELECT * FROM CNT_USER WHERE name =#{name}
</select>
这里的返回类型就是UserDTO,查询结果会映射到UserDTO,查询结果如下:
映射时,使用java的反射机制来将对应的值设置到对应的属性中。具体的代码如下:
查询的整个流程就是这样的了,里面涉及很多细节:一级缓存、二级缓存、链接管理等,值得深入学习。