笔者最近研究mybatis比较多,所以打算把最近研究的mybatis的心得写出来,以免以后忘记,算是对这阶段的总结吧
环境:
mybatis-3.2.7
mybatis的懒加载配置什么的我就不详细说了,可以到我的github地址,看我的mybatis-demo
我在这里画了一个图,简单的描述一下懒加载的流程,(画的不好。。。)
画的不好,,,可能大家看不懂,我来简单的给大家说一下流程,其实主要分两个逻辑:
启动懒加载
mybatis初始化返回类型的时候,会返回一个cglib代理对象,这样的话,该对象的关联对象(例如一对多,多对一)相关信息就会在loadpair里边,并且添加到loadmap中,cglib对象会过滤get,set ,is,"equals", "clone", "hashCode", "toString"触发方法,然后才会调用loadpair来加载关联对象的值,这部分的详细源码下面我会带大家看
不启动懒加载
不会返回代理对象,返回原生对象,然后会在一开始的时候就加载关联对象和sql中指定的所有属性
接下来,我会详细跟大家说一下mybatis是怎样实现懒加载的:
首先,懒加载会用到这两个配置
使用懒加载的sql:
/**
* 根据用户id查询用户角色
* @param userId
* @return
*/
@Select("select * from role_main a INNER JOIN user_role b ON a.id = b.role_id WHERE b.user_id=#{userId}")
public List getRolesByUserId(@Param("userId")Integer userId);
/**
* 根据用户id查询用户角色名
* @param userId
* @return
*/
@Select("select a.role_name from role_main a INNER JOIN user_role b ON a.id = b.role_id WHERE b.user_id=#{userId}")
public Set getRoleNamesByUserId(@Param("userId")Integer userId);
/**
* 根据userid查询用户的所有权限
* @param userId
* @return
*/
@Select("SELECT a.permission_name FROM permission_main a INNER JOIN role_permission b ON a.id=b.permission_id WHERE b.role_id IN (SELECT d.role_id from user_main c INNER JOIN user_role d ON c.id = d.user_id WHERE c.id=#{userId})")
public Set getPermissionsByUserId(@Param("userId")Integer userId);
/**
* 通过用户名查询用户信息
* @param username
* @return
*/
@Select("select * from user_main where username=#{username}")
@Results({
@Result(property = "roleNames", column = "id", many = @Many(fetchType=FetchType.LAZY,select = "getRoleNamesByUserId")),
@Result(property = "permissionNames", column = "id", many = @Many(fetchType=FetchType.LAZY,select = "getPermissionsByUserId"))
})
public UserPO getUserByUsername(@Param("username")String username);
方法getUserByUserName就是使用了懒加载的查询,根据上面所描述的,以roleNames和permissionNames为例,在这里,它们俩就是所说的关联对象,如果没有调用它们的get,set ,is,"equals", "clone", "hashCode", "toString"方法,这两个关联的集合对象是不会有值的,只有触发了方法,才会从数据库中查询值
接下来,我带大家看看,mybatis在使用懒加载的时候做了什么,然后又在触发懒加载的时候做了什么
mybatis在使用懒加载的时候,是怎样保留懒加载的信息的?
这里额外的补充一下,在mybatis中,使用了很多动态代理,非常的普遍,使用最多的是jdk proxy,然后就是懒加载用到的cglib(默认)和javassist,关于懒加载这部分,笔者打算只是讲述cglib实现的懒加载,javassist是和cglib差不多的,只是技术不同,这两种懒加载笔者也就不说谁好谁坏了
回到正题,mybatis在懒加载的时候是怎么保留关联对象查询数据库的信息的:
以mybatis执行getUserByUserName的查询为例,在初始化UserPO赋值从数据库中查询的value的时候,因为启用了懒加载,所以USerPO的引用对象会是一个cglib代理对象,代理对象的初始化部分如下CglibProxyFactory,EnhancedResultObjectProxyImpl:
/**
* @author Clinton Begin
*/
public class CglibProxyFactory implements ProxyFactory {
private static final Log log = LogFactory.getLog(CglibProxyFactory.class);
private static final String FINALIZE_METHOD = "finalize";
private static final String WRITE_REPLACE_METHOD = "writeReplace";
/**
* 无参构造函数,初始化CglibProxyFactory
* 加载net.sf.cglib.proxy.Enhancer(cglib)
*
*/
public CglibProxyFactory() {
try {
Resources.classForName("net.sf.cglib.proxy.Enhancer");
} catch (Throwable e) {
throw new IllegalStateException("Cannot enable lazy loading because CGLIB is not available. Add CGLIB to your classpath.", e);
}
}
/**
* 使用内部类EnhancedResultObjectProxyImpl构建target类的代理
*/
public Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List> constructorArgTypes, List
上面是从mybatis中粘出来的源码,大家注意带有注释的crateProxy方法和EnhancedResultObjectProxyImpl中的intercept方法,这两个方法比较重要,crateProxy是初始化返回对象类型的代理对象,这里会默认给代理对象增加一个WriteReplaceInterface接口,这个接口也是用来做懒加载触发的,笔者不是很清楚,不做说明;其实上面说了这么多次的懒加载触发,什么的,实际点说,就是intercept方法,这里我要说一下,代理对象的每个方法调用都会经过intercept方法,大家看intercept方法中的判断lazyLoadTriggerMethods,这个lazyLoadTriggerMethods就是上面一直说的懒加载的默认触发方法,"equals", "clone", "hashCode", "toString",因为这个默认触发方法,笔者还吃了个亏,详细请看,mybatis注解方式懒加载失效分析,这几个默认方法是在configuration中配置的,大家可以根据需要在配置文件中自定义触发方法;除了这几个默认的触发方法,还有PropertyNamer.isProperty(methodName)这段代码,他会判断当前方法是不是代理对象的get,is,set方法,如果是的话,也会触发懒加载来查询数据库。
但是到这里懒加载保存信息部分还是没有完,我们来看,mybatis组装返回对象的时候,是怎么保留懒加载信息的,懒加载信息又是什么:
这里就要看DefaultResultSetHandler了,因为这个类比较多,所以笔者就不贴源码了大家可以对照着源码来看一下这部分,我先给大家看一下这个类中处理查询返回结果的方法时序图:
这个是DefaultResultSetHandler处理从数据库查询的数据的处理流程图,其中懒加载是在getNestedQueryMappingValue方法中的
//初始化resultloader
final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
//如果启用了懒加载,初始化一个loadpair到loadermap里边,如果没有启用,直接获取value并且返回
if (propertyMapping.isLazy()) {
lazyLoader.addLoader(property, metaResultObject, resultLoader);
} else {//直接加载
value = resultLoader.loadResult();
}
其中loadpair记录了关联对象mybatis查询要用到的信息,metaResultObject(返回对象的metaobject),ResultLoader(包含了参数,返回映射,缓存key,配置等),executor(sql执行器)。。。
然后包含这些信息的loadpair就会放到loadmap中了,这部分完成解说了,接下来,就要看mybatis懒加载触发的时候是怎么使用loadpair来查询数据库的。
mybatis触发懒加载使用loadpair查询数据库并且返回组装对象:
说到触发的话,就要回到CglibProxyFactory.EnhancedResultObjectProxyImpl的intercept方法上了,懒加载方法被触发以后会调用lazyLoader.load(property);方法,
这个方法会先从loadmap中将loadpair移除,然后调用loadpair的load方法,
/**
* 执行懒加载查询,获取数据并且set到userObject中返回
* @param userObject
* @throws SQLException
*/
public void load(final Object userObject) throws SQLException {
//合法性校验
if (this.metaResultObject == null || this.resultLoader == null) {
if (this.mappedParameter == null) {
throw new ExecutorException("Property [" + this.property + "] cannot be loaded because "
+ "required parameter of mapped statement ["
+ this.mappedStatement + "] is not serializable.");
}
//获取mappedstatement并且校验
final Configuration config = this.getConfiguration();
final MappedStatement ms = config.getMappedStatement(this.mappedStatement);
if (ms == null) {
throw new ExecutorException("Cannot lazy load property [" + this.property
+ "] of deserialized object [" + userObject.getClass()
+ "] because configuration does not contain statement ["
+ this.mappedStatement + "]");
}
//使用userObject构建metaobject,并且重新构建resultloader对象
this.metaResultObject = config.newMetaObject(userObject);
this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter,
metaResultObject.getSetterType(this.property), null, null);
}
/* We are using a new executor because we may be (and likely are) on a new thread
* and executors aren't thread safe. (Is this sufficient?)
*
* A better approach would be making executors thread safe. */
if (this.serializationCheck == null) {
final ResultLoader old = this.resultLoader;
this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement,
old.parameterObject, old.targetType, old.cacheKey, old.boundSql);
}
//获取数据库查询结果并且set到结果对象返回
this.metaResultObject.setValue(property, this.resultLoader.loadResult());
}
现在懒加载部分的原理已经说完了,当然,里边有很多没有详细讲解的部分,也有很多大家还不详细理解的部分,比如executor,其他的查询结构,mappedstatement,metaobject...等等,笔者打算做一个专题,尽量将自己领悟的mybatis组件原理呈现给大家,,,希望大家能够支持