Mybatis原始碼分析(五)探究SQL語句的執行過程

清幽之地發表於2019-03-10

一、重溫JDBC

Java Database Connectivity,簡稱JDBC。是Java語言中用來規範客戶端程式如何來訪問資料庫的應用程式介面,提供了諸如查詢和更新資料庫中資料的方法。 隨著Java ORM框架的發展,已經很少有機會再在生產系統中寫JDBC的程式碼來訪問資料庫了,但是基本流程我們還是要熟悉。下面以一個簡單的查詢為例,溫故一下JDBC。

public static void main(String[] args) throws Exception {
	Connection conn = getConnection(); 	
	String sql = "select * from user where 1=1 and id = ?";
	PreparedStatement stmt = conn.prepareStatement(sql);
	stmt.setString(1, "501440165655347200");
	ResultSet rs = stmt.executeQuery();
	while(rs.next()){
		String username = rs.getString("username");
		System.out.print("姓名: " + username);
	}
}
複製程式碼

從上面的程式碼來看,一次簡單的資料庫查詢操作,可以分為幾個步驟。

  • 建立Connection連線

  • 傳入引數化查詢SQL語句構建預編譯物件PreparedStatement

  • 設定引數

  • 執行SQL

  • 從結果集中獲取資料

那麼,我們們的主角Mybatis是怎樣完成這一過程的呢?不著急,我們們一個一個來看。

二、sqlSession

在上一章節的內容中,我們已經看到了在Service層通過@Autowired注入的userMapper是個代理類,在執行方法的時候實際上呼叫的是代理類的invoke通知方法。

public class MapperProxy<T> implements InvocationHandler{
	public Object invoke(Object proxy, Method method, Object[] args)

		final MapperMethod mapperMethod = cachedMapperMethod(method);
		return mapperMethod.execute(sqlSession, args);
	}
}
複製程式碼

1 、建立MapperMethod物件

MapperMethod物件裡面就兩個屬性,SqlCommand和MethodSignature。

SqlCommand包含了執行方法的名稱和方法的型別,比如UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH。 MethodSignature可以簡單理解為方法的簽名資訊。裡面包含:返回值型別、是否void、是否為集合型別、是否為Cursor等,主要還獲取到了方法引數上的@Param註解的名稱,方便下一步獲取引數值。 比如,如果方法上加了@Param的引數: User getUserById(@Param(value="id")String id,@Param(value="password")String password);,引數會被解析成{0=id, 1=password}

2、執行

判斷方法的SQL型別和返回值型別 ,呼叫相應的方法。以方法User getUserById(String id,String password)為例,會呼叫到selectOne()方法。

public class MapperMethod {
	public Object execute(SqlSession sqlSession, Object[] args) {
		Object result;
		switch (command.getType()) {
		case INSERT: {}
		case UPDATE: {}
		case DELETE: {}
		case SELECT:
		if (method.returnsVoid() && method.hasResultHandler()) {
			//無返回值
		} else if (method.returnsMany()) {
			//返回集合型別
			result = executeForMany(sqlSession, args);
		} else if (method.returnsMap()) {
			//返回Map型別
			result = executeForMap(sqlSession, args);
		} else if (method.returnsCursor()) {
			//返回Cursor
			result = executeForCursor(sqlSession, args);
		} else {
			//將引數args轉換為SQL命令的引數
			//預設會新增一個《param+引數索引》的引數名
			//{password=123456, id=501441819331002368, param1=501441819331002368, param2=123456}
			Object param = method.convertArgsToSqlCommandParam(args);
			result = sqlSession.selectOne(command.getName(), param);
		}
		...
		return result;
	}
}
複製程式碼

可以看到,sqlSession.selectOne就可以獲取到資料庫中的值並完成轉換工作。這裡的sqlSession就是SqlSessionTemplate例項的物件,所以它會呼叫到

public class SqlSessionTemplate{
	public <T> T selectOne(String statement, Object parameter) {
		return this.sqlSessionProxy.<T> selectOne(statement, parameter);
	}
}
複製程式碼

sqlSessionProxy也是個代理物件。關於它的建立我們們上節課也很認真的分析了,總之它實際會呼叫到SqlSessionInterceptor.invoke()

3、建立sqlSession物件

sqlSession我們熟悉呀,它作為MyBatis工作的主要頂層API,表示和資料庫互動的會話,完成必要資料庫增刪改查功能。關於它的建立、執行、提交和資源清理都是在SqlSessionInterceptor的通知方法中完成的。

private class SqlSessionInterceptor implements InvocationHandler {
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {	
		//建立SqlSession物件
		SqlSession sqlSession = getSqlSession(
		SqlSessionTemplate.this.sqlSessionFactory,
		SqlSessionTemplate.this.executorType,
		SqlSessionTemplate.this.exceptionTranslator);
		try {
			//呼叫sqlSession實際方法
			Object result = method.invoke(sqlSession, args);
			return result;
		} catch (Throwable t) {
			....
		} finally {
			if (sqlSession != null) {
				closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
			}
		}
	}
}
複製程式碼

上面的重點就是建立了SqlSession並執行它的方法,它是一個DefaultSqlSession例項的物件,裡面主要有一個通過configuration建立的執行器,在這裡它是SimpleExecutor。

那麼,invoke方法實際呼叫的就是DefaultSqlSession.selectOne()

三、獲取BoundSql物件

DefaultSqlSession中的selectOne()方法最終也會呼叫到selectList()方法。它先從資料大管家configuration中根據請求方法的全名稱拿到對應的MappedStatement物件,然後呼叫執行器的查詢方法。

1、獲取MappedStatement物件

//statement是呼叫方法的全名稱,parameter為引數的Map
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
	//在mapper.xml中每一個SQL節點都會封裝為MappedStatement物件
	//在configuration中就可以通過請求方法的全名稱獲取對應的MappedStatement物件
	MappedStatement ms = configuration.getMappedStatement(statement);
	return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}
複製程式碼

其中有個方法wrapCollection(parameter)我們可以瞭解下,如果引數為集合型別或者陣列型別,它會將引數名稱設定為相應型別的名稱。

private Object wrapCollection(final Object object) {
	if (object instanceof Collection) {
		StrictMap<Object> map = new StrictMap<Object>();
		map.put("collection", object);
		if (object instanceof List) {
		map.put("list", object);
		}
		return map;
	} else if (object != null && object.getClass().isArray()) {
		StrictMap<Object> map = new StrictMap<Object>();
		map.put("array", object);
		return map;
	}
	return object;
}
複製程式碼

2、獲取BoundSql物件

在configuration這個大管家物件中,儲存著mapper.xml裡面所有的SQL節點。每一個節點對應一個MappedStatement物件,而動態生成的各種sqlNode儲存在SqlSource物件,SqlSource物件有一個方法就是getBoundSql()。 我們先來看一下BoundSql類哪有哪些屬性。

public class BoundSql {	
	//動態生成的SQL,解析完畢帶有佔位性的SQL
	private final String sql;
	//每個引數的資訊。比如引數名稱、輸入/輸出型別、對應的JDBC型別等
	private final List<ParameterMapping> parameterMappings;
	//引數
	private final Object parameterObject;
	private final Map<String, Object> additionalParameters;
	private final MetaObject metaParameters;
}
複製程式碼

看到這幾個屬性,也就解釋了BoundSql 的含義。即表示動態生成的SQL語句和相應的引數資訊。

不知大家是否還有印象,不同型別的SQL會生成不同型別的SqlSource物件。比如靜態SQL會生成StaticSqlSource物件,動態SQL會生成DynamicSqlSource物件。

  • 靜態SQL

靜態SQL比較簡單,直接就建立了BoundSql物件並返回。

public class StaticSqlSource implements SqlSource {
	return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
複製程式碼
  • 動態SQL

動態SQL要根據不同的sqlNode節點,呼叫對應的apply方法,有的還要通過Ognl表示式來判斷是否需要新增當前節點,比如IfSqlNode。

public class DynamicSqlSource implements SqlSource {
	public BoundSql getBoundSql(Object parameterObject) {
		DynamicContext context = new DynamicContext(configuration, parameterObject);
		//rootSqlNode為sqlNode節點的最外層封裝,即MixedSqlNode。
		//解析完所有的sqlNode,將sql內容設定到context
		rootSqlNode.apply(context);
		SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
		Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
		//設定引數資訊 將SQL#{}替換為佔位符
		SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
		//建立BoundSql物件
		BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
		for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
			boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
		}
		return boundSql;
	}
}
複製程式碼

rootSqlNode.apply(context)是一個迭代呼叫的過程。最後生成的內容儲存在DynamicContext物件,比如select * from user WHERE uid=#{uid}

然後呼叫SqlSourceBuilder.parse()方法。它主要做了兩件事:

1、將SQL語句中的#{}替換為佔位符 2、將#{}裡面的欄位封裝成ParameterMapping物件,新增到parameterMappings。

ParameterMapping物件儲存的就是引數的型別資訊,如果沒有配置則為null。 ParameterMapping{property='uid', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}

最後返回的BoundSql物件就包含一個帶有佔位符的SQL和引數的具體資訊。

四、執行SQL

建立完BoundSql物件,呼叫query方法,來到CachingExecutor.query()。這個方法的前面是二級快取的判斷,如果開啟了二級快取且快取中有資料,就返回。

1、快取

public class CachingExecutor implements Executor {
	public <E> List<E> query(MappedStatement ms, Object parameterObject, 
		RowBounds rowBounds, ResultHandler resultHandler, 
		CacheKey key, BoundSql boundSql)throws SQLException {
		//二級快取的應用
		//如果配置</cache>則走入這個流程
		Cache cache = ms.getCache();
		if (cache != null) {
			flushCacheIfRequired(ms);
			if (ms.isUseCache() && resultHandler == null) {
				//從快取中獲取資料
				List<E> list = (List<E>) tcm.getObject(cache, key);
				if (list == null) {
					list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
					tcm.putObject(cache, key, list); // issue #578 and #116
				}
				return list;
			}
		}
		return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
	}
}
複製程式碼

接著看query方法,建立PreparedStatement預編譯物件,執行SQL並獲取返回集合。

public class SimpleExecutor extends BaseExecutor {
	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();
			//獲取Statement的型別,即預設的PreparedStatementHandler
			//需要注意,在這裡如果配置了外掛,則StatementHandler可能返回的是一個代理
			StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
			//建立PreparedStatement物件,並設定引數值
			stmt = prepareStatement(handler, ms.getStatementLog());
			//執行execute 並返回結果集
			return handler.<E>query(stmt, resultHandler);
		} finally {
			closeStatement(stmt);
		}
	}
}
複製程式碼

prepareStatement方法獲取資料庫連線並構建Statement物件設定SQL引數。

1、建立PreparedStatement

public class SimpleExecutor extends BaseExecutor {
	private Statement prepareStatement(StatementHandler handler, Log statementLog) {
		Statement stmt;
		Connection connection = getConnection(statementLog);
		stmt = handler.prepare(connection, transaction.getTimeout());
		handler.parameterize(stmt);
		return stmt;
	}
}
複製程式碼
  • 獲取Connection連線

我們看到getConnection方法就是獲取Connection連線的地方。但這個Connection也是一個代理物件,它的呼叫程式處理器為ConnectionLogger。顯然,它是為了更方便的列印日誌。

public abstract class BaseExecutor implements Executor {
	protected Connection getConnection(Log statementLog) throws SQLException {
		//從c3p0連線池中獲取一個連線
		Connection connection = transaction.getConnection();
		//如果日誌級別為Debug,則為這個連線生成代理物件返回
		//它的處理類為ConnectionLogger
		if (statementLog.isDebugEnabled()) {
			return ConnectionLogger.newInstance(connection, statementLog, queryStack);
		} else {
			return connection;
		}
	}
}
複製程式碼
  • 執行預編譯

這個跟我們的JDBC程式碼是一樣的,拿到SQL,呼叫Connection連線的prepareStatement(sql)。但由於connection是一個代理物件,似乎又沒那麼簡單。

public class PreparedStatementHandler
	protected Statement instantiateStatement(Connection connection) throws SQLException {
		String sql = boundSql.getSql();
		return connection.prepareStatement(sql);
	}
}
複製程式碼

所以,在執行的onnection.prepareStatement(sql)的時候,實際呼叫的是ConnectionLogger類的invoke()。

public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {

	public Object invoke(Object proxy, Method method, Object[] params)throws Throwable {
		try {
			if ("prepareStatement".equals(method.getName())) {
				if (isDebugEnabled()) {
					debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
				}        
				//呼叫connection.prepareStatement
				PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
				//又為stmt建立了代理物件,通知類為PreparedStatementLogger
				stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
				return stmt;
			}
		} 
	}
}

public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) {
	InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack);
	ClassLoader cl = PreparedStatement.class.getClassLoader();
	return (PreparedStatement) Proxy.newProxyInstance(cl, 
			new Class[]{PreparedStatement.class, CallableStatement.class}, handler);
}
複製程式碼

果然沒那麼簡單,最後返回的PreparedStatement又是個代理物件。

  • 設定引數

我們知道,在設定引數的時候,你有很多可選項,比如stmt.setString()、stmt.setInt()、stmt.setFloat()等,或者粗暴一點就stmt.setObject()

當然了,Mybatis作為一個優秀的ORM框架,不可能這麼粗暴。它先是根據引數的Java型別獲取所有JDBC型別的處理器,再根據JDBC的型別獲取對應的處理器。在這裡我們沒有配置JDBC型別,所以就是它的型別為NULL,最後返回的就是StringTypeHandler。

關於型別處理器的匹配和查詢規則,我們們在Mybatis原始碼分析(三)通過例項來看typeHandlers已經詳細分析過,就不再細看。

public class StringTypeHandler extends BaseTypeHandler<String> {
	public void setNonNullParameter(PreparedStatement ps, int i, 
			String parameter, JdbcType jdbcType)throws SQLException {
		ps.setString(i, parameter);
	}
}
複製程式碼

2、執行

在SQL預編譯完成之後,呼叫execute()執行。

public class PreparedStatementHandler{
	public <E> List<E> query(Statement statement, ResultHandler resultHandler) {
		PreparedStatement ps = (PreparedStatement) statement;
		ps.execute();
		return resultSetHandler.<E> handleResultSets(ps);
	}
}
複製程式碼

這裡的PreparedStatement物件也是個代理類,在呼叫通知類PreparedStatementLogger,執行execute的時候,只是列印了引數的值。即Parameters: 501868995461251072(String)

五、處理返回值

上面的方法我們看到SQL已經提交給資料庫執行,那麼最後一步就是獲取返回值。

public class DefaultResultSetHandler implements ResultSetHandler {
	public List<Object> handleResultSets(Statement stmt) throws SQLException {
		final List<Object> multipleResults = new ArrayList<Object>();
		int resultSetCount = 0;
		//將ResultSet封裝成ResultSetWrapper物件
		ResultSetWrapper rsw = getFirstResultSet(stmt);
		//返回mapper.xml中配置的rsultMap 實際上我們沒有配置,但會有預設的一個
		List<ResultMap> resultMaps = mappedStatement.getResultMaps();
		int resultMapCount = resultMaps.size();
		//處理資料庫的返回值,最後加入到multipleResults
		while (rsw != null && resultMapCount > resultSetCount) {
			ResultMap resultMap = resultMaps.get(resultSetCount);
			handleResultSet(rsw, resultMap, multipleResults, null);
			resultSetCount++;
		}
		//返回
		return collapseSingleResultList(multipleResults);
	}
}
複製程式碼

1、ResultSetWrapper物件

上面的程式碼我們看到,第一步就把ResultSet物件封裝成了ResultSetWrapper物件,關於它還需要具體來看。

public class ResultSetWrapper {
	public ResultSetWrapper(ResultSet rs, Configuration configuration) throws SQLException {
		//所有已註冊的型別處理器
		this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
		//ResultSet物件
		this.resultSet = rs;
		//後設資料 列名、列型別等資訊
		final ResultSetMetaData metaData = rs.getMetaData();
		final int columnCount = metaData.getColumnCount();
		//迴圈列,將列名、列對應的JDBC型別和列對應的Java型別都獲取到
		for (int i = 1; i <= columnCount; i++) {
			columnNames.add(metaData.getColumnName(i));
			jdbcTypes.add(JdbcType.forCode(metaData.getColumnType(i)));
			classNames.add(metaData.getColumnClassName(i));
		}
	}
}
複製程式碼

上面的重點是拿到資料庫列上的資訊,在解析的時候會用到。

2、處理返回值

handleResultSet方法最後呼叫到DefaultResultSetHandler.handleRowValuesForSimpleResultMap()

public class DefaultResultSetHandler implements ResultSetHandler {
	private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, 
			ResultMap resultMap, ResultHandler<?> resultHandler, 
			RowBounds rowBounds, ResultMapping parentMapping)throws SQLException {
			
		DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
		//跳過行 Mybatis的RowBounds分頁功能
		skipRows(rsw.getResultSet(), rowBounds);
		while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
			ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
			Object rowValue = getRowValue(rsw, discriminatedResultMap);
			storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
		}
	}
}
複製程式碼

在這個地方涉及到Mybatis中分頁的一個物件RowBounds。但實際上,我們基本不會用到它。因為它是一個邏輯分頁,而非物理分頁。

  • RowBounds

RowBounds物件中有兩個屬性控制著分頁:offset、limit。offset是說分頁從第幾條資料開始,limit是說一共取多少條資料。因為我們沒有配置它,所以它預設是offset從0開始,limit取Int的最大值。

public class RowBounds {
	public static final int NO_ROW_OFFSET = 0;
	public static final int NO_ROW_LIMIT = Integer.MAX_VALUE;
	public RowBounds() {
		this.offset = NO_ROW_OFFSET;
		this.limit = NO_ROW_LIMIT;
	}
}
複製程式碼

skipRows方法就是來跳過offset,它的實現也比較簡單。

private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
	for (int i = 0; i < rowBounds.getOffset(); i++) {
		rs.next();
	}
}
複製程式碼

offset跳過之後,怎麼控制Limit的呢?這就要看上面的while迴圈了。

while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
	//處理資料
}
複製程式碼

關鍵在於shouldProcessMoreRows()方法,它其實是個簡單的判斷。

private boolean shouldProcessMoreRows(ResultContext<?> context, 
							RowBounds rowBounds) throws SQLException {
	//就是看已經取到的資料是否小與Limit
	return context.getResultCount() < rowBounds.getLimit();
}
複製程式碼
  • 獲取

while迴圈獲取ResultSet的每一行資料,然後通過rs.getxxx()獲取資料。

public class DefaultResultSetHandler implements ResultSetHandler {
	private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
		final ResultLoaderMap lazyLoader = new ResultLoaderMap();
		//建立返回值型別,比如我們返回的是User實體類
		Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
		if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
			final MetaObject metaObject = configuration.newMetaObject(rowValue);
			boolean foundValues = this.useConstructorMappings;
			if (shouldApplyAutomaticMappings(resultMap, false)) {
			//自動對映
			foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
		}
		//這個處理配置的ResultMap,就是手動配置資料庫列名與Java實體類欄位的對映
		foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
		foundValues = lazyLoader.size() > 0 || foundValues;
		rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
		}
		return rowValue;
	}
}
複製程式碼

1、獲取返回值型別

第一步是獲取返回值型別,過程就是拿到Class物件,然後獲取構造器,設定可訪問並返回例項。

private  <T> T instantiateClass(Class<T> type, 
			List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
	Constructor<T> constructor;
	if (constructorArgTypes == null || constructorArgs == null) {
		//獲取構造器
		constructor = type.getDeclaredConstructor();
		if (!constructor.isAccessible()) {
			//設定可訪問
			constructor.setAccessible(true);
		}
		//返回例項
		return constructor.newInstance();
	}
}
複製程式碼

返回後,又把它包裝成了MetaObject物件。Mybatis會根據返回值型別的不同,包裝成不同的Wrapper物件。本例中,由於是一個實體類,會返回BeanWrapper。

private MetaObject(Object object, ObjectFactory objectFactory, 
			ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {
	this.originalObject = object;
	this.objectFactory = objectFactory;
	this.objectWrapperFactory = objectWrapperFactory;
	this.reflectorFactory = reflectorFactory;

	if (object instanceof ObjectWrapper) {
		this.objectWrapper = (ObjectWrapper) object;
	} else if (objectWrapperFactory.hasWrapperFor(object)) {
		this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object);
	} else if (object instanceof Map) {
		this.objectWrapper = new MapWrapper(this, (Map) object);
	} else if (object instanceof Collection) {
		this.objectWrapper = new CollectionWrapper(this, (Collection) object);
	} else {
		this.objectWrapper = new BeanWrapper(this, object);
	}
}
複製程式碼

2、applyAutomaticMappings

在mapper.xml中我們可以宣告一個resultMap節點,將資料庫中列的名稱和Java中欄位名稱對應起來,應用到SQL節點的resultMap中。也可以不配置它,直接利用resultType返回一個Bean即可。但是這兩種方式會對應兩種解析方法。

private boolean applyAutomaticMappings(ResultSetWrapper rsw, 
		ResultMap resultMap, MetaObject metaObject, 
		String columnPrefix) throws SQLException {	
	//獲取相應欄位的型別處理器
	List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
	boolean foundValues = false;
	if (!autoMapping.isEmpty()) {
		for (UnMappedColumnAutoMapping mapping : autoMapping) {
			final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
			if (value != null) {
				foundValues = true;
			}
			if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
				//因為返回值型別是一個BeanWapper,通過反射把值設定到JavaBean中。
				metaObject.setValue(mapping.property, value);
			}
		}
	}
	return foundValues;
}
複製程式碼
  • 獲取

上面程式碼的重點是獲取對應欄位的型別處理器,呼叫對應型別處理器的getResult方法從ResultSet中拿到資料的值。

//type是Java欄位的型別 jdbcType是資料庫列的JDBC型別
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
	//先從所有的處理器中獲取Java型別的處理器
	Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
	TypeHandler<?> handler = null;
	if (jdbcHandlerMap != null) {
		//再根據JDBC的型別獲取實際的處理器
		handler = jdbcHandlerMap.get(jdbcType);
	}
	return (TypeHandler<T>) handler;
}
複製程式碼

以ID為例,在Java中是String型別,在資料庫中是VARCHAR,最後返回的型別處理器是StringTypeHandler。呼叫的時候,就很簡單了。

return rs.getString(columnName);
複製程式碼
  • 設定

通過rs.getString()拿到值之後,然後向返回值型別中設定。因為我們返回的是一個JavaBean,對應的是BeanWapper物件,方法中其實就是反射呼叫。

public class BeanWrapper extends BaseWrapper {
	private void setBeanProperty(PropertyTokenizer prop, Object object, Object value) {
		try {
			Invoker method = metaClass.getSetInvoker(prop.getName());
			Object[] params = {value};
			try {
				method.invoke(object, params);
			} catch (Throwable t) {
			throw ExceptionUtil.unwrapThrowable(t);
			}
		} catch (Throwable t) {
			throw new ReflectionException("Could not set property '" + prop.getName() + "' of '" + object.getClass() + "' with value '" + value + "' Cause: " + t.toString(), t);
		}
	}
}
複製程式碼

把所有的列都解析完,返回指定的Bean。最後加入到list,整個方法返回。在selectOne方法中,取List的第一條資料。如果資料記錄大於1,就是出錯了。

public class DefaultSqlSession implements SqlSession {
	public <T> T selectOne(String statement, Object parameter) {
		List<T> list = this.<T>selectList(statement, parameter);
		if (list.size() == 1) {
			return list.get(0);
		} else if (list.size() > 1) {
			throw new TooManyResultsException("Expected one result (or null) 
                          to be returned by selectOne(), but found: " + list.size());
		} else {
			return null;
		}
	}
}
複製程式碼

六、總結

關於Mybatis執行方法的整個過程,我們簡單歸納一下。

  • 獲取SqlSession,根據方法的返回值型別呼叫不同的方法。比如selectOne

  • 獲取BoundSql物件,根據傳遞的引數生成SQL語句

  • 從資料庫連線池獲取Connection物件,併為它建立代理,以便列印日誌

  • 從Connection中獲取PreparedStatement預編譯物件,併為它建立代理

  • 預編譯SQL,並設定引數

  • 執行、返回資料集合

  • 將資料集轉換為Java物件

看到這裡,再回憶下我們開頭的JDBC例項的步驟,可以看到它們兩者之間的主流程都是一樣的。Mybatis只是在此基礎上做了一些封裝,更好的服務於我們的應用。

相關文章