Mybatis在执行查询时,其参数设置、结果封装、级联查询、延迟加载,是最基本的功能和用法,我们有必要了解其工作原理,重点阐述级联查询和延迟加载。
1、MetaObject
MetaObject用于反射创建对象、反射从对象中获取属性值、反射给对象设置属性值,参数设置和结果封装,用的都是这个MetaObject提供的功能。
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。
ObjectFactory objectFactory:通过Class对象反射创建对象实例的工厂类,比如创建一个Student对象。
ObjectWrapperFactory :对目标对象进行包装,比如可以将Properties对象包装成为一个Map并返回Map对象。
ReflectorFactory :为了避免多次反射同一个Class对象,ReflectorFactory提供了Class对象的反射结果缓存。
getValue(String name):属性取值。
setValue(String name, Object value):属性赋值。
2、参数设置实现原理
<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")取值。
详情请参见DefaultParameterHandler.java。
3、结果封装实现原理
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"));
问题:
1、Student对象只有studId属性,根本没有STUDID属性;Student对象只有name属性,根本没有NAME属性;Java是大小写敏感的编程语言,我凭什么说原理是这样的?瞎说的吧?
2、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源码。
4、级联查询实现原理
级联查询,主要分为一对一关联查询和一对多集合查询,我们研究一下Mybatis是如何实现的。
1、一对一关联查询实现原理(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查询所需要的所有信息。
2、延迟加载
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属性。
延迟加载原理,就是这么简单。
3、一对多查询原理(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
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;
}
4、嵌套查询原理
上面的一对一、一对多查询,都需要单独发送额外的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 |
5、一对多嵌套查询一的一端去重复原理
<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>();
//...
}
对于上面的6条结果记录,根据
-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个。
“`java
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的参数设置、结果封装、级联查询、延迟加载原理就分析结束了。
> 来源:https://my.oschina.net/zudajun/blog/747283
>
> 作者:祖大俊