Mybatis3.4.x技術內幕(二十一):參數設置、結果封裝、級聯查詢、延遲加載原理分析

Mybatis在執行查詢時,其參數設置、結果封裝、級聯查詢、延遲加載,是最基本的功能和用法,咱們有必要了解其工做原理,重點闡述級聯查詢和延遲加載。java

一、MetaObject

MetaObject用於反射建立對象、反射從對象中獲取屬性值、反射給對象設置屬性值,參數設置和結果封裝,用的都是這個MetaObject提供的功能。sql

public static MetaObject forObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {
    if (object == null) {
      return SystemMetaObject.NULL_META_OBJECT;
    } else {
      return new MetaObject(object, objectFactory, objectWrapperFactory, reflectorFactory);
    }
  }
public Object getValue(String name) {
//...
}
public void setValue(String name, Object value) {
// ...
}

Object object:要反射的對象,好比Student。apache

ObjectFactory objectFactory:經過Class對象反射建立對象實例的工廠類,好比建立一個Student對象。編程

ObjectWrapperFactory :對目標對象進行包裝,好比能夠將Properties對象包裝成爲一個Map並返回Map對象。緩存

ReflectorFactory :爲了不屢次反射同一個Class對象,ReflectorFactory提供了Class對象的反射結果緩存。網絡

getValue(String name):屬性取值。mybatis

setValue(String name, Object value):屬性賦值。app

二、參數設置實現原理

<insert id="insertStudent" parameterType="Student" >
		INSERT INTO
		STUDENTS(STUD_ID, NAME, EMAIL, DOB, PHONE)
		VALUES(#{studId}, #{name},
		#{email}, #{dob}, #{phone})
</insert>

Mybatis解析後,上面的#{studId}, #{name}佔位符都會被替換爲?號佔位符,而後給?號設置參數值,Mybatis經過一個反射工具類MetaObject,從Student對象中,反射獲取studId、name屬性值,並賦值給?號參數。編程語言

若是是佔位符是#{item.studId},也是同樣,經過getValue("item.studId")取值。ide

詳情請參見DefaultParameterHandler.java。

三、結果封裝實現原理

Mybatis的結果封裝,分爲兩種,一種是有ResultMap映射表,明肯定義告終果集列名與對象屬性名的配對關係,另一種是對象類型,沒有明肯定義結果集列名與對象屬性名的配對關係,如resultType是Student對象。

<resultMap type="Teacher" id="TeacherResult">
		<id property="id" column="t_id"/>
		<result property="name" column="t_name" />
	</resultMap>
	
	<select id="findAllTeachers" resultMap="TeacherResult">
		SELECT t_id, t_name FROM TEACHERS
	</select>

原理很是簡單:使用ObjectFactory ,建立一個Teacher對象實例。

teacher.setId(resultSet.getInt("t_id"));

teacher.setName(resultSet.getString("t_name"));

若是是對象類型,如Student對象相似,原理也很是簡單。

<select id="findStudentById" parameterType="int" resultType="Student">
		SELECT STUD_ID AS STUDID, NAME, EMAIL, DOB, PHONE
		FROM STUDENTS WHERE STUD_ID = #{Id}
</select>

原理:使用ObjectFactory ,建立一個Student對象實例。

student.setStudId(resultSet.getInt("STUDID"));

student.setName(resultSet.getString("NAME"));

問題:

一、Student對象只有studId屬性,根本沒有STUDID屬性;Student對象只有name屬性,根本沒有NAME屬性;Java是大小寫敏感的編程語言,我憑什麼說原理是這樣的?瞎說的吧?

二、resultSet.getInt("STUDID"),resultSet.getString("NAME"),我怎麼知道一個是Integer,一個是String?

好的博客文章,價值就體如今這些地方,下面咱們就來解開謎團。

未映射的結果集列名爲[STUDID, NAME, EMAIL, DOB, PHONE]。

public class Reflector {
  private Map<String, String> caseInsensitivePropertyMap = new HashMap<String, String>();
//...
  caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
//...
}

因而caseInsensitivePropertyMap = {STUDID=studId, DOB=dob, PHONE=phone, EMAIL=email, NAME=name}。

因而,resultSet結果集列名和對象屬性名之間,就創建起了一對一對應關係。所以,哪怕你把列名寫成NaME、pHoNe,它均可以「智能」找到對象屬性名,進行賦值操做,Mybatis不愧是一款偉大的開源產品。

caseInsensitive的含義就是忽略結果集列名大小寫。

正確找到對象屬性名以後,反射獲取屬性studId的java類型,獲得Integer類型,反射獲取屬性name的java類型獲得String類型,Integer類型對應IntegerTypeHandler,String類型對應StringTypeHandler。

因而,resultSet.getInt("STUDID"),resultSet.getString("NAME")就是這麼肯定的。

詳情請參看DefaultResultSetHandler.java源碼。

四、級聯查詢實現原理

級聯查詢,主要分爲一對一關聯查詢和一對多集合查詢,咱們研究一下Mybatis是如何實現的。

一、一對一關聯查詢實現原理(association)

一對一,一個Studen對應一個班級。

舉例:假設一個Student對應一個Teacher,以下:

<resultMap id="studentResult" type="Student">
		<association property="teacher" column="teacher_id"
			javaType="Teacher" select="selectTeacher" />
	</resultMap>
	
	<select id="selectStudent" parameterType="int" resultMap="studentResult">
		SELECT STUD_ID AS STUDID, NAME, EMAIL, DOB, PHONE, TEACHER_ID FROM STUDENTS WHERE STUD_ID = #{id}
	</select>
	
	<select id="selectTeacher" parameterType="int" resultType="Teacher">
		SELECT * FROM TEACHERS WHERE ID = #{id}
	</select>

首先查詢Student對象,想要得到該Student對象的屬性Teacher teacher對象,那麼須要該Student對象的teacher_id值,做爲查詢Teacher對象的參數,這個語意是易懂的,因此,上面的一對一關聯查詢,應該很容易看得懂。

做爲resultMap標籤,其下面的association標籤,也會被解析爲一個ResultMapping對象。

public class ResultMapping {
  private String property;
  private String column;
  private String nestedQueryId;
//...
}

對於xml配置,ResultMapping={property=teacher, column=teacher_id, nestedQueryId=selectTeacher},其屬性nestedQueryId就是用來存儲另一個select查詢的id值的。

org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getPropertyMappingValue(ResultSet, MetaObject, ResultMapping, ResultLoaderMap, String)源碼。

private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
      throws SQLException {
    if (propertyMapping.getNestedQueryId() != null) {
      // 執行另一個select查詢,把查詢結果賦值給屬性值,好比Student對象的teacher屬性。
      return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
    } else if (propertyMapping.getResultSet() != null) {
      addPendingChildRelation(rs, metaResultObject, propertyMapping);   // TODO is that OK?
      return DEFERED;
    } else {
      final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
      final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
      return typeHandler.getResult(rs, column);
    }
  }

org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getNestedQueryMappingValue(ResultSet, MetaObject, ResultMapping, ResultLoaderMap, String)源碼。

private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
      throws SQLException {
    final String nestedQueryId = propertyMapping.getNestedQueryId();
    final String property = propertyMapping.getProperty();
    final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
    final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
    final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);
    Object value = null;
    if (nestedQueryParameterObject != null) {
      final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
      final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
      final Class<?> targetType = propertyMapping.getJavaType();
      if (executor.isCached(nestedQuery, key)) {
        executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
        value = DEFERED;
      } else {
        // ResultLoader保存了關聯查詢所須要的全部信息
        final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
        if (propertyMapping.isLazy()) {
          // 執行延遲加載
          // 語意:resultLoader的查詢結果將賦值給metaResultObject源對象的property屬性,resultLoader的查詢參數值來自於metaResultObject源對象屬性中。
          // 舉例:查詢Teacher,賦值給Student的teacher屬性,參數來自於查詢Student的ResultSet的teacher_id列的值。
          // 因爲須要執行延遲加載,將查詢相關信息放入緩存,但不執行查詢,使用該屬性時,自動觸發加載操做。
          lazyLoader.addLoader(property, metaResultObject, resultLoader);
          value = DEFERED;
        } else {
          // 不執行延遲加載,當即查詢並賦值
          value = resultLoader.loadResult();
        }
      }
    }
    return value;
  }
public ResultLoader(Configuration config, Executor executor, MappedStatement mappedStatement, Object parameterObject, Class<?> targetType, CacheKey cacheKey, BoundSql boundSql) {}

看看ResultLoader的構造函數,它保存了執行一個select查詢所須要的全部信息。

二、延遲加載

mybatis-config.xml內全局配置。

<setting name="lazyLoadingEnabled" value="false|true" />
public class ResultLoaderMap {

  private final Map<String, LoadPair> loaderMap = new HashMap<String, LoadPair>();
}

private LoadPair(final String property, MetaObject metaResultObject, ResultLoader resultLoader) {
//...
}

ResultLoader保存了一個select查詢所須要的全部信息,那麼,將查詢結果賦值給metaResultObject源對象的property屬性,這些基本信息都緩存至loaderMap內,這就是語意。

舉例:查詢Teacher,賦值給Student的teacher屬性。爲了實現延遲加載,產生了一個loaderMap緩存,緩存了查詢所須要的全部信息,若是lazyLoadingEnabled=true,先不執行查詢。若是lazyLoadingEnabled=false,那麼當即執行查詢。

咱們看看lazyLoadingEnabled=true時的工做原理。

private static class EnhancedResultObjectProxyImpl implements MethodHandler {
    @Override
    public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
      final String methodName = method.getName();
      try {
        synchronized (lazyLoader) {
          if (WRITE_REPLACE_METHOD.equals(methodName)) {
            Object original = null;
            if (constructorArgTypes.isEmpty()) {
              original = objectFactory.create(type);
            } else {
              original = objectFactory.create(type, constructorArgTypes, constructorArgs);
            }
            PropertyCopier.copyBeanProperties(type, enhanced, original);
            if (lazyLoader.size() > 0) {
              return new JavassistSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
            } else {
              return original;
            }
          } else {
            // 此處完成延遲加載功能
            if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
              if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
                lazyLoader.loadAll();
              } else if (PropertyNamer.isProperty(methodName)) {
                final String property = PropertyNamer.methodToProperty(methodName);
                if (lazyLoader.hasLoader(property)) {
                  lazyLoader.load(property);
                }
              }
            }
          }
        }
        return methodProxy.invoke(enhanced, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
}

JavassistProxyFactory會使用CGLib建立一個Student代理對象,全部調用Student對象方法,都會通過EnhancedResultObjectProxyImpl.invoke()方法的攔截。

因而當調用Student.getTeacher()方法時,才真正去執行查詢Teacher的動做並把結果賦值給Student的teacher屬性。

若是lazyLoadingEnabled=false,壓根就不會建立Student代理對象,直接就是Student對象,並當即執行Teacher查詢,而後賦值給Student的teacher屬性。

延遲加載原理,就是這麼簡單。

三、一對多查詢原理(collection)

<resultMap type="Teacher" id="TeacherResult">
		<collection property="students" javaType="ArrayList" column="id" ofType="Student" select="selectStudents"/>
	</resultMap>
	
	<select id="findTeacherById" parameterType="int" resultMap="TeacherResult">
		SELECT * FROM TEACHERS where ID = #{ID}
	</select>
	
	<select id="selectStudents" parameterType="int" resultType="Student">
		SELECT STUD_ID AS STUDID, NAME, EMAIL, DOB, PHONE FROM STUDENTS WHERE TEACHER_ID = #{id}
	</select>

一對多查詢原理,和一對一查詢原理是如出一轍的,都是將結果以List的形式返回,若是是一對一查詢,就取List的第0個元素,若是是一對多查詢,就直接返回List。

org.apache.ibatis.executor.loader.ResultLoader.loadResult()源碼。

public Object loadResult() throws SQLException {
    List<Object> list = selectList();
    resultObject = resultExtractor.extractObjectFromList(list, targetType);
    return resultObject;
  }

org.apache.ibatis.executor.ResultExtractor.extractObjectFromList(List<Object>, Class<?>)源碼。

public Object extractObjectFromList(List<Object> list, Class<?> targetType) {
    Object value = null;
    if (targetType != null && targetType.isAssignableFrom(list.getClass())) {
      value = list;
    } else if (targetType != null && objectFactory.isCollection(targetType)) {
      value = objectFactory.create(targetType);
      MetaObject metaObject = configuration.newMetaObject(value);
      metaObject.addAll(list);
    } else if (targetType != null && targetType.isArray()) {
      Class<?> arrayComponentType = targetType.getComponentType();
      Object array = Array.newInstance(arrayComponentType, list.size());
      if (arrayComponentType.isPrimitive()) {
        for (int i = 0; i < list.size(); i++) {
          Array.set(array, i, list.get(i));
        }
        value = array;
      } else {
        value = list.toArray((Object[])array);
      }
    } else {
      if (list != null && list.size() > 1) {
        throw new ExecutorException("Statement returned more than one row, where no more than one was expected.");
      } else if (list != null && list.size() == 1) {
        value = list.get(0);
      }
    }
    return value;
  }

四、嵌套查詢原理

上面的一對1、一對多查詢,都須要單獨發送額外的sql進行關聯對象查詢操做,那麼嵌套查詢,解決的是隻須要一個sql,就能夠將關聯對象也查詢出來。

<resultMap id="studentResult" type="Student">
		<id property="studId" column="stud_id" />
		<association property="teacher" column="teacher_id"
			javaType="Teacher">
			<id property="id" column="teacher_id" />
			<result property="name" column="T_NAME" />
		</association>
	</resultMap>

	<select id="selectStudent" parameterType="int" resultMap="studentResult">
		SELECT
			s.STUD_ID
			,s.TEACHER_ID
			,t.NAME AS T_NAME
		FROM STUDENTS s
			LEFT JOIN TEACHERS t ON s.TEACHER_ID = t.ID
		WHERE s.STUD_ID = #{id}
	</select>

 ResultMapping的屬性nestedResultMapId就是用來作這個的。

public class ResultMapping {
  private String nestedResultMapId;
//..
}

org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(ResultSetWrapper, ResultMap, CacheKey, String, Object)源碼。

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix, Object partialObject) throws SQLException {
    final String resultMapId = resultMap.getId();
    Object resultObject = partialObject;
    if (resultObject != null) {
      final MetaObject metaObject = configuration.newMetaObject(resultObject);
      putAncestor(resultObject, resultMapId, columnPrefix);
      applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);
      ancestorObjects.remove(resultMapId);
    } else {
      final ResultLoaderMap lazyLoader = new ResultLoaderMap();
      resultObject = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
      if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
        final MetaObject metaObject = configuration.newMetaObject(resultObject);
        boolean foundValues = !resultMap.getConstructorResultMappings().isEmpty();
        if (shouldApplyAutomaticMappings(resultMap, true)) {
          foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
        }
        foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
        putAncestor(resultObject, resultMapId, columnPrefix);
        // 解析NestedResultMappings並封裝結果,賦值給源對象的關聯查詢屬性上
        foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true) || foundValues;
        ancestorObjects.remove(resultMapId);
        foundValues = lazyLoader.size() > 0 || foundValues;
        resultObject = foundValues ? resultObject : null;
      }
      if (combinedKey != CacheKey.NULL_CACHE_KEY) {
        nestedResultObjects.put(combinedKey, resultObject);
      }
    }
    return resultObject;
  }

applyNestedResultMappings()方法負責從ResultSet結果集中,封裝association映射爲指定對象,賦值給metaObject源對象的屬性對象上。

一對多嵌套查詢。

<resultMap id="teacherResult" type="Teacher">
		<id property="id" column="TEACHER_ID" />
		<result property="name" column="TEACHER_NAME" />
		<collection property="students" ofType="Student">
			<id property="studId" column="STUD_ID" />
		</collection>
	</resultMap>

	<select id="findTeacherById" parameterType="int" resultMap="teacherResult">
		SELECT
			s.STUD_ID
			,t.ID AS TEACHER_ID
			,t.NAME AS TEACHER_NAME
		FROM TEACHERS t
		LEFT JOIN STUDENTS s ON s.TEACHER_ID = t.ID
		WHERE t.ID = #{id}
	</select>

原理和一對一嵌套查詢是同樣的。

問題:left join查詢,一對一沒問題,可是,一對多時,返回記錄像下面這樣,也就是說一的一端其實也是N條記錄,可是它表明的是一個Teacher對象,Mybatis是如何去重的呢?下面的記錄,表明1個老師有6個學生,而不是6個老師6個學生。

|          1 | teacher      |      38 |
|          1 | teacher      |      39 |
|          1 | teacher      |      40 |
|          1 | teacher      |      41 |
|          1 | teacher      |      42 |
|          1 | teacher      |      43 |

五、一對多嵌套查詢一的一端去重複原理

<id property="studId" column="stud_id" />
public class ResultMap {
  private List<ResultMapping> idResultMappings;
//...
}
public class DefaultResultSetHandler implements ResultSetHandler {
  private final Map<CacheKey, Object> nestedResultObjects = new HashMap<CacheKey, Object>();
//...
}

<id>和<result>標籤的區別就在於此,<id>表示惟一標識一條記錄的屬性,能夠有多個<id>標籤,表明聯合主鍵。Map<CacheKey, Object> nestedResultObjects就是用來緩存嵌套查詢中,記錄去重複功能的。

對於上面的6條結果記錄,根據<id>標籤生成的CacheKey是相同的,相似下面的值:

-540526625:-2232742192:com.mybatis3.mappers.TeacherMapper.teacherResult:TEACHER_ID:1
-540526625:-2232742192:com.mybatis3.mappers.TeacherMapper.teacherResult:TEACHER_ID:1

每次遍歷結果集ResultSet時,獲取到的Teacher對象,都是第一次生成的Teacher對象,因此,Teacher是同一個,Student則是6個。

private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    //...
    while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
      //...
      Object partialObject = nestedResultObjects.get(rowKey);
        rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject); 
}

詳情請參看DefaultResultSetHandler.java。

至此,Mybatis的參數設置、結果封裝、級聯查詢、延遲加載原理就分析結束了。

版權提示:文章出自開源中國社區,若對文章感興趣,可關注個人開源中國社區博客(http://my.oschina.net/zudajun)。(通過網絡爬蟲或轉載的文章,常常丟失流程圖、時序圖,格式錯亂等,仍是看原版的比較好)

相關文章
相關標籤/搜索