(四)MyBatis源码解析之SqlSession

时间:2021-03-14 17:18:38

上篇博文将完了MyBatis的配置文件解析的过程,从这篇博文开始分析MyBatis的执行流程

解析完配置文件之后,配置文件中的所有信息就存储到了Configuration对象中了,在创建SqlSessionFactory对象时将Configuration做为SqlSessionFactory对象的一个属性,通过调用SqlSessionFactory的openSession(...),openSession()有很多重载方法,创建SqlSession对象只能通过openSession()和这些重载方法

  @Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
下面的代码是实际创建SqlSession的地方,在这里首先要获得解析XML文件时创建的TransactionFacroty对象,其次根据TransactionFactory创建事务对象Transaction,然后通过execType创建Executor,execType的类型决定了创建的Executor对象的类型
  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

executorType有三种类型,分别是BATCH,REUSE,SIMPLE,默认的是SIMPLE类型,这三种类型分别对应着三种executor,BatchExecutor专门用于执行批量sql操作,ReuseExecutor会重用statement执行sql操作,SimpleExecutor就是简单的执行sql操作,除了这三种类型之外Mybatis还支持CacheExecutor,这就是Mybatis内建的缓存机制,在SqlSession级别提供对SQL查询结果的缓存,即:如果你调用了相同的select查询,Mybatis会将放在缓存的结果返回,而不会去再查询数据库,后面具体分析查询过程时会分析Mybatis是如何实现缓存的,在Mybatis中真正负责执行数据库操作的就是这些Executor

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {    
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
最后executor对象是可以被插件拦截的,关于插件后面会有专门的博文进行分析

通过代理获得Mapper

Mybatis建议通过mapper对象访问mybatis,使用mapper和使用普通的方式的区别主要有以下几点

    public int getUserInfo(long userId){
return getSqlSession().selectOne("net.klq.inter.po.TbNsBssUserInfoCn.selectUserInfoCn", userId);
}

1、普通的方式在dao层查询的id和namespace是手写的,和xml中的配置文件有可能会对不上,实际工作中经常会碰到这种状况,而使用mapper的时候每个接口在哪个包下是固定的不会随意改变,省去了我们写包名时候容易导致的错误

2、普通的方式入参只能有一个,如果sql需要多个参数,我们需要先把多个参数封装成一个对象或者是放到map中,而使用mapper的时候可以使用任意多个参数

知道了通过Mapper编程的好处之后,思考一个问题mapper是一个接口,没有实现类怎么可以调用该接口的方法呢?答案就是动态代理,使用sqlSession的getMapper(...)方法,在调用该方法的时候传我们需要的Mapper接口的class对象进去,mybatis就可以为我们创建该对象的实现类了,具体是怎么创建的呢,一起来看看

    MessageMapperDao messageMapper = sqlSession.getMapper(MessageMapperDao.class);
Mybatis会去knownMappers中查找是否为该接口创建了一个代理工厂,在Mybatis中使用MapperProxyFactory作为代理工厂,找到代理工厂后会通过jdk的动态代理功能为我们创建一个动态代理对象然后返回,在应用中用接口来引用这个动态代理对象,接下来我们所有的增删改查都是通过这个动态代理对象进行的,Mybatis使用MapperProxy作为动态代理的对象,而且Mybatis在创建代理对象时将sqlSession的信息给了MapperProxy,这就

  @SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}

到目前为止我们已经找到了Mybatis提供的动态代理类了,但是这个动态代理类帮我们做了什么?怎么就实现了调用接口方法就可以执行xml配置文件里的sql了呢,别着急,让我们来看看Mybatis提供的这个动态代理类,确实到目前为止接口和sql还没有关联,但是仔细一看我们已经为关联做好准备了。methodCache属性具有存储Method和MapperMethod对应关系的功能,而MapperMethod中包含Configuration对象,而Configuration对象包含xml配置文件的所有信息,当然也就包含sql的信息,这就为我们建立接口方法和sql的关联关系做好了准备。再接下来就是具体的增删改查功能了,这是mybatis最最核心的功能,也是最复杂的功能,我们会多篇博文来介绍增删改查

public class MapperProxy<T> implements InvocationHandler, Serializable {

private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;

public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}

private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}

}


在介绍增删改查之前,我们先来看一下为什么通过接口查询可以设置多个参数,而通过普通的方法查询不行,通过普通方法查询时Mybatis是不知道我们到底要传进去多少个参数,每个参数之间的关系是什么样的,所以Mybatis规定只能有一个参数,如果我们想传多个参数有一般是有两种方式,将多个参数封装成一个对象或者将多个参数放到一个map中,这对于开发者来说是很不方便的,既然mybatis规定参数只能是一个为什么通过接口查询就可以是多个参数呢,因为mybatis在内部将我们传进去的多个参数转成了一个map省去我们自己转换的这个步骤,下面看看是如何转换的,jdk的动态代理会将方法的参数转成一个Object[]数组,定义多个参数时必须使用@Param注解Mybatis才可以识别,像下面这样声明多个参数

public List<Message> queryMessageList(@Param(value = "command") String command,@Param(value="description")String description);

在调用增删改查方法时,mybatis通过反射可以知道mybatis有多个参数,如果没有参数下面方法直接返回null,如果有一个参数就返回args[0],如果有多个参数根据@Param的顺序依次放到map中,转换成map之后普通方法执行数据库操作和通过接口操作数据库就没有区别了

public Object convertArgsToSqlCommandParam(Object[] args) {
final int paramCount = params.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasNamedParameters && paramCount == 1) {
return args[params.keySet().iterator().next().intValue()];
} else {
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : params.entrySet()) {
param.put(entry.getValue(), args[entry.getKey().intValue()]);
// issue #71, add param names as param1, param2...but ensure backward compatibility
final String genericParamName = "param" + String.valueOf(i + 1);
if (!param.containsKey(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}