MyBatis作为一款优秀的持久层框架,现在已经被越来越多的公司在用了。面对这个我们每天都在使用的框架,不好好读读它的源码怎么行呢?笔者花了几天的时间阅读和调试MyBatis源码,现在把我的一些理解分享给大家,如有错误,还望指正。
MyBatis源码版本:3.5.8-SNAPSHOT,源码地址:https://github.com/mybatis/mybatis-3.git
1. 前言
MyBatis整体框架大致如下图所示,其中每个模块都可以单拎出来写一篇文章。篇幅原因,本篇文章只会介绍【查询】的全流程,且不会对细节介绍太多。例如:xml解析、参数映射、缓存等会一笔带过,其中的细节会在后面的文章单独介绍。

本篇文章只讨论MyBatis,也只分析MyBatis的源码,因此不会和Spring做集成。
2. 示例程序
【需求】
使用MyBatis完成一次数据库查询,根据主键ID查询用户记录。
1、编写mybatis-config.xml配置文件。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--环境配置-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!--Mapper配置-->
<mappers>
<mapper resource="mappers/UserMapper.xml"/>
</mappers>
</configuration>
2、编写用户表对应的DO类。
public class User {
private Long id;
private String userName;
private String pwd;
private String nickName;
private String phone;
private LocalDateTime createTime;
// 省略 Getter,Setter方法
}
3、编写UserMapper接口。
public interface UserMapper {
// 根据ID查询用户
User getById(@Param("id") Long id);
}
4、编写UserMapper.xml文件。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.apache.mapper.UserMapper">
<select id="getById" useCache="true" flushCache="false" resultType="org.apache.domain.User">
select
id,user_name as userName,pwd,nick_name as nickName,phone,create_time as createTime
from user
where id = #{id}
</select>
</mapper>
5、编写测试程序。
public class Demo {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
// 读取配置文件,获取输入流
InputStream inputStream = Resources.getResourceAsStream(resource);
// 从输入流中构建回话工厂
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 打开一个新的回话
SqlSession session = sessionFactory.openSession(true);
// 获取UserMapper代理类对象
UserMapper mapper = session.getMapper(UserMapper.class);
// 查询ID=1的用户信息,并输出到控制台
System.err.println(mapper.getById(1L));
}
}
至此,示例程序全部结束,运行示例程序,控制台会输出ID=1的用户信息。基于此程序,我们来一步步分析MyBatis是如何仅凭一个接口和xml文件就实现数据库查询到Java Bean的映射。
3. 源码分析
3.1 SqlSessionFactory的构建
SqlSessionFactory是MyBatis管理回话的工厂,它的职责就是打开一个SqlSession,有了SqlSession我们就可以对数据库进行增删改查的操作了。
SqlSessionFactory是一个接口,默认的实现类是DefaultSqlSessionFactory。
这里贴出部分代码:
public interface SqlSessionFactory {
// 打开一个新的回话
SqlSession openSession()
/**
* 打开一个新的回话
* @param autoCommit 是否自动提交
* @return
*/
SqlSession openSession(boolean autoCommit);
/**
* 以指定的事务隔离级别 打开一个新的回话
* @param level 事务隔离级别
* @return
*/
SqlSession openSession(TransactionIsolationLevel level);
// 获取全局配置
Configuration getConfiguration();
}
SqlSessionFactory采用【建造者模式】进行构建,对应的类为SqlSessionFactoryBuilder,build()构建方法有多个重载,你可以通过字符流Reader来构建,也可以通过字节流InputStream来构建,也可以通过全局配置对象Configuration来构建。其实不论是哪种方式,最终都是通过Configuration对象构建,前两种构建方式MyBatis只不过是从字符流/字节流中将config.xml配置文件解析成Configuration对象了。
public class SqlSessionFactoryBuilder {
// 通过字符流构建
public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}
// 通过字节流构建
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
// 通过配置对象来构建,最终都是通过该方法构建
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
3.2 Configuration的构建
Configuration是MyBatis的全局配置类,它可以看作是mybatis-config.xml配置文件在Java中的描述对象。这个类非常重要,也非常庞大,这里不展开细说,后面会专门写文章记录。
构建SqlSessionFactory需要Configuration,那Configuration对象是怎么来的呢?其实就两种方式,一种是你手动创建Configuration对象,还有一种就是你编写好xml文件,让MyBatis自己去解析。
一般都是采用配置文件的方式,因此我们重点看XMLConfigBuilder.parse()。
// 从xml配置文件中解析Configuration
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 从根节点<configuration>开始解析
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
// 从根节点开始依次解析
private void parseConfiguration(XNode root) {
try {
/**
* 下面就是依次解析各类标签了
* 1.解析properties标签,读取属性
* 2.解析settings标签,读取设置项
* 3.解析类的别名
* 4.解析插件配置
* 5.解析对象工厂配置
* 6.解析对象包装工厂配置
* 7.解析运行环境,多数据源配置
* 8.解析databaseIdProvider,多数据库支持
* 9.解析typeHandlers,类型处理器
* 10.解析mappers,注册Mapper接口
*/
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
3.3 SqlSession
解析配置文件生成Configuration对象,有了Configuration就可以构建SqlSessionFactory,有了SqlSessionFactory就可以获得SqlSession对象。
SqlSession是MyBatis最重要的接口之一,它是MyBatis提供给开发者操作数据库的唯一接口,极大的简化了数据库的操作。
先来看看接口定义,我们就知道它具有哪些能力,我只贴部分代码。
public interface SqlSession extends Closeable {
// 查询一条记录
<T> T selectOne(String statement);
// 查询多条记录
<E> List<E> selectList(String statement);
// 查询Map
<K, V> Map<K, V> selectMap(String statement, String mapKey);
// 游标查询
<T> Cursor<T> selectCursor(String statement);
// 插入数据
int insert(String statement);
// 修改数据
int update(String statement);
// 删除数据
int delete(String statement);
// 提交事务
void commit();
// 回滚事务
void rollback();
// 获取Mapper接口的代理类
<T> T getMapper(Class<T> type);
// 获取SqlSession关联的数据库连接
Connection getConnection();
}
通过接口就可以看出来,只要有了SqlSession对象,我们就可以对数据库进行【增删改查】以及对事务的操作,而且它还会自动帮我们出来结果集和Java对象的映射,非常的方便。
3.4 生成Mapper代理对象
一般我们很少会直接通过SqlSession去操作数据库,而是会新建一个接口,通过MyBatis生成的代理对象去操作,因此我们重点关注getMapper()方法。
SqlSession的默认实现类是DefaultSqlSession,直接看它就好了。
/**
* 获取Mapper接口代理对象:MapperProxy
* @see MapperProxy
* @param type Mapper接口类
* @param <T>
* @return
*/
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
当我们通过SqlSession去获取Mapper的代理对象时,它会转交给Configuration对象去完成。因为MyBatis解析配置文件时,会一同解析Mapper.xml文件,并把解析结果注册到MapperRegistry中。MapperRegistry是Mapper接口的注册器,它将Mapper接口类Class和对应的MapperProxyFactory放到一个Map容器中,你可以向里面注册Mapper接口,以及获取Mapper接口的代理对象。
public class MapperRegistry {
// 全局配置
private final Configuration config;
// Mapper接口和MapperProxyFactory的映射关系
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
// 获取Mapper接口的代理对象
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);
}
}
// 注册Mapper接口
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
knownMappers.put(type, new MapperProxyFactory<>(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
}
向MapperRegistry注册Mapper接口时,它会将Mapper接口封装成MapperProxyFactory,因为需要依赖它来为Mapper接口创建代理对象。创建代理对象的逻辑也很简单,因为Mapper都是接口,因此直接使用JDK动态代理就可以了,生成的代理对象的MapperProxy。
/**
* Mapper接口代理类工厂
* 作用:生成Mapper接口代理对象
* @param <T>
*/
public class MapperProxyFactory<T> {
// Mapper接口类
private final Class<T> mapperInterface;
// 方法缓存,通过代理对象调用方法时,会先将Method解析成MapperMethodInvoker
private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();
@SuppressWarnings("unchecked")
// 创建新实例,通过JDK动态代理创建代理对象
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
// 省略部分代码...
}
3.5 MapperProxy
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.getById(1L);
UserMapper是接口,接口不能实例化,那mapper对象是哪里来的呢?
当我们调用第一行代码时,实际上MyBatis通过JDK动态代理为我们生成了一个代理对象MapperProxy。当我们调用第二行代码时,实际上执行的是MapperProxy.invoke()方法。
代理对象的invoke()如下,如果调用的是Object类的方法,如:hashCode、equals等,则直接通过代理对象本身调用即可,不需要操作数据库。否则为其生成MapperMethodInvoker对象,调用其invoke方法。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
// 如果是继承自Object类的方法,则直接通过代理对象本身调用即可
return method.invoke(this, args);
} else {
/**
自定义方法才操作数据库
1.先从缓存中获取方法对应的MapperMethodInvoker
2.执行
非default犯法,直接看:org.apache.ibatis.binding.MapperProxy.PlainMethodInvoker.invoke()
@see MapperMethod#execute(org.apache.ibatis.session.SqlSession, java.lang.Object[])
*/
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
MapperMethodInvoker是MyBatis对Mapper接口中Method的包装类,它有两个实现类:DefaultMethodInvoker和PlainMethodInvoker。
DefaultMethodInvoker是对Mapper接口中default方法的包装,也是不需要操作数据库的,直接获取方法的句柄对象MethodHandle调用即可。
如果是用户自定义的非default方法,如getById(),这种需要去操作数据库的,才会为Method生成PlainMethodInvoker对象。
// 从缓存中获取Method对应的MapperMethodInvoker对象
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
return MapUtil.computeIfAbsent(methodCache, method, m -> {
if (m.isDefault()) {
// 如果调用的是Interface中的default方法,找到方法句柄MethodHandle,通过反射调用。
// 不是重点,了解即可。
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
// 非default方法,需要操作数据库的,生成PlainMethodInvoker对象。
// 它会通过sqlSession去操作数据库。
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
PlainMethodInvoker需要一个MapperMethod对象,对于需要操作数据库的方法,它会转交给MapperMethod完成,它会去调用MapperMethod.execute()方法。
3.6 MapperMethod
MapperMethod是MyBatis对Mapper接口中需要操作数据库的方法的包装类,先看下它的属性:
/**
* 执行的SQL命令的封装类
* name:接口全限定名+方法名,定位要执行的唯一的sql
* type:SQL命令类型(增删改查)
*/
private final SqlCommand command;
/**
* 方法签名的描述类
* 方法的返回值的什么?
* 返回单个对象还是集合对象?
* 是否返回Map?
* 等等...
*/
private final MethodSignature method;
SqlCommand是对方法要执行的SQL命令的描述,它记录了要执行SQL的唯一标识Statement,以及SQL命令的类型。
public static class SqlCommand {
// 接口全限定名+方法名,定位要执行的唯一的sql
private final String name;
// SQL命令类型:增删改查等
private final SqlCommandType type;
}
MethodSignature是对方法签名的描述,它记录了方法的参数、返回值、分页、ResultHandler等信息。
public static class MethodSignature {
// 是否返回多个对象?如List
private final boolean returnsMany;
// 是否返回Map?
private final boolean returnsMap;
// 是否返回Void?
private final boolean returnsVoid;
// 是否返回Cursor游标对象?
private final boolean returnsCursor;
// 是否返回Optional对象?
private final boolean returnsOptional;
// 返回类型Class
private final Class<?> returnType;
// 解析方法上@MapKey注解的值
private final String mapKey;
// ResultHandler所在参数列表的下标(对查询结果做处理)
private final Integer resultHandlerIndex;
// RowBounds所在参数列表的下标(限制返回的结果数量)
private final Integer rowBoundsIndex;
// 参数名称解析器,对于加了@Param注解的参数,xml中可以直接使用
private final ParamNameResolver paramNameResolver;
// 省略部分代码... ...
}
当我们执行Mapper接口去操作数据库时,MapperProxy代理对象会执行MapperMethod.execute()方法,这里会完成数据库的【增删改查】操作,需要重点关注。
篇幅原因,这里只看查询。当调用userMapper.getById(1L)时,它首先会判断方法是否返回void且参数中包含ResultHandler,如果是就不返回结果了,将查询结果交给ResultHandler去处理。
然后会判断是否返回多条结果?是否返回Map?是否返回游标?很明显,这里都不满足,因此直接进最后一个else。
执行普通查询时,它首先需要将方法参数解析为ParamMap,本质还是一个HashMap,Key是参数Name,Value是参数值,参数解析的目的是你可以在xml中使用#{id}/${id}来使用参数,从而实现动态SQL。
参数解析完成后,它会调用SqlSession.selectOne()查询单条结果。看到没,还是通过SqlSession去操作数据库,只是我们很少直接用它,都是通过代理对象去操作的。
查询完成后,判断返回类型是否是Optional,如果是它会将结果使用Optional进行包装,最后返回结果。
/**
* 执行Mapper方法:执行SQL
* @param sqlSession
* @param args
* @return
*/
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {// 插入操作
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {// 更新操作
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {// 删除操作
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:// 查询操作
if (method.returnsVoid() && method.hasResultHandler()) {
/**
方法返回值为Void,且参数中有ResultHandler,将查询结果交给ResultHandler处理,直接返回null
@see ResultHandler
*/
executeWithResultHandler(sqlSession, args);
result = null;
} 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 {
/*
将方法实参转换为SQL语句需要用到的参数映射,这样你就可以在xml中通过#{param}来使用参数了。
一般返回的是一个Map结构:例如
"id":1,
"param1":1
*/
Object param = method.convertArgsToSqlCommandParam(args);
/*
执行SQL查询
1.根据statement定位到MappedStatement
2.SQL语句的处理,参数绑定
3.StatementHandler创建对应的Statement
4.执行SQL
5.结果集处理,ResultSet转换成Java Bean
*/
result = sqlSession.selectOne(command.getName(), param);
// 如果返回结果为Optional,则自动将结果进行包装。
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
3.7 ParamNameResolver
ParamNameResolver是MyBatis提供的【参数名称解析器】,它会处理方法的形参,以及加了@Param注解的参数,将它们转换成ParamMap。
先看属性:
public class ParamNameResolver {
// 生成的Name前缀
public static final String GENERIC_NAME_PREFIX = "param";
// 是否用实际的参数名,可能得到arg0,arg1这种无意义的名称
private final boolean useActualParamName;
// 参数下标-对应的参数名
private final SortedMap<Integer, String> names;
// 是否存在@Param注解
private boolean hasParamAnnotation;
}
MethodSignature对象被创建时,就会创建对应的ParamNameResolver。
ParamNameResolver会在构造函数中解析好参数名称及其对应的下标,并存放到names中。解析的逻辑是:优先取@Param注解指定的名称,如果没有注解则判断是否取实际的参数名,否则以参数下标当做Name。
这里补充一下,Java反射是可以获取参数名称的,前提是必须JDK8版本,且编译时加了–parameters参数才行,否则得到的是arg0,arg1这种无意义的参数名。
/**
* 1.优先获取@Param注解的值
* 2.通过反射获取参数名
* 3.使用参数下标
*/
public ParamNameResolver(Configuration config, Method method) {
this.useActualParamName = config.isUseActualParamName();
final Class<?>[] paramTypes = method.getParameterTypes();
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
final SortedMap<Integer, String> map = new TreeMap<>();
int paramCount = paramAnnotations.length;
// get names from @Param annotations
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
if (isSpecialParameter(paramTypes[paramIndex])) {
// 跳过RowBounds和ResultHandler参数
continue;
}
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
// 如果加了@Param注解,则取注解的值
hasParamAnnotation = true;
name = ((Param) annotation).value();
break;
}
}
if (name == null) {
// @Param was not specified.
if (useActualParamName) {
// 如果使用实际的参数名,则通过反射获取参数名。
// JDK8编译类加–parameters参数可以保留参数名称,否则得到的是arg0,arg1这种无意义的参数名
name = getActualParamName(method, paramIndex);
}
if (name == null) {
// 如果名称还是为空,则可以使用下标来获取参数:#{param1},#{param2}...
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}
// 使其不可变
names = Collections.unmodifiableSortedMap(map);
}
构造函数只负责解析参数下标对应的参数名称,要想获取参数名对应的参数值,还需要调用getNamedParams()方法。
因为开发者可能没加注解,反射可能获取不到参数名,由于存在种种不确定性,因此MyBatis额外提供了一种确定性方案。它会在解析参数时,除了使用已知的参数名,还会自动根据参数下标按照#{param下标}自动生成记录。
例如示例程序中的参数id,会解析成如下:
"id" > 1
"param1" > 1
代码如下:
/**
* 获取参数名对应的参数值
* 一般是Map结构,当参数只有1个时,直接返回,xml中写任意值都可以匹配到
* @param args
* @return
*/
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
// 无参情况
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
// 没有@Param注解,且参数只有一个的情况
Object value = args[names.firstKey()];
return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
} else {
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
// 还会额外生成 param1,param2...的映射关系,你也可以在xml中使用#{param1}
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
3.8 selectOne
参数解析完成后,又回到了SqlSession的查询操作了。先说一下SqlSession为什么可以操作数据库,要想操作数据库,首先得拿到数据库连接Connection。
DefaultSqlSession是SqlSession的默认实现类,我们来看下它的创建过程。
当我们从SqlSessionFactory中打开一个新的回话时,会调用openSessionFromDataSource()方法。它首先会从配置对象中获取运行环境,再从环境中获取事务工厂TransactionFactory。
TransactionFactory对应的就是配置文件中如下配置项,在解析配置文件时就已经创建好了,一般是JdbcTransactionFactory。
<transactionManager type="JDBC"/>
JdbcTransactionFactory会打开一个新的事务JdbcTransaction,JdbcTransaction中就包含数据源DataSource和数据库连接Connection。
public class JdbcTransaction implements Transaction {
// 数据库连接
protected Connection connection;
// 数据源
protected DataSource dataSource;
// 事务隔离级别
protected TransactionIsolationLevel level;
// 是否自动提交
protected boolean autoCommit;
}
有了JdbcTransaction,再根据ExecutorType去创建对应的Executor,Executor是MyBatis操作数据库的执行器接口。
有了Executor就可以创建DefaultSqlSession了,源码如下:
/**
* 从DataSource打开一个Session
* @param execType 执行器类型:简单、重用、批量
* @param level 事务隔离级别
* @param autoCommit 是否自动提交
* @return
*/
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// 获取运行环境:解析xml时创建
final Environment environment = configuration.getEnvironment();
// 获取Environment关联的事务工厂
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 使用工厂类创建一个事务管理器,一般是 JdbcTransaction
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 根据事务管理器和执行器类型创建执行器,一般是 SimpleExecutor
final Executor executor = configuration.newExecutor(tx, execType);
// 创建一个SqlSession
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();
}
}
有了SqlSession就可以操作数据库了,回到selectOne()方法,它其实还是会执行selectList(),只不过会自动取第0条结果而已,因此我们重点关注selectList()。
statement是要执行的SQL的唯一标识,由【接口全限定名+方法名】组成,因此MyBatis不支持接口方法的重载。根据statement就可以定位到xml中唯一的一个SQL节点,这个SQL节点在Java中用MappedStatement类表示。
MappedStatement这个类也很重要,下节会详细说明,现在你只需要知道它代表SQL标签节点,有了它就知道要执行的SQL语句是什么,返回结果是什么,就行了。
定位到MappedStatement后,就是调用Executor.query()进行查询操作了。
/**
* 查询多条结果
* @param statement 要执行SQL的唯一标识:接口全限定名+方法名
* @param parameter 参数列表
* @param rowBounds 分页条件
* @param handler 结果处理器,如果参数中没有ResultHandler,则默认为null
* @param <E>
* @return
*/
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
// 从Map缓存中获取MappedStatement(解析xml里的SQL标签时创建)。
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
3.9 MappedStatement
MappedStatement可以理解为是MyBatis对xml文件中【增删改查】SQL节点的描述,MyBatis在解析xml文件时会自动生成该对象,详细代码在XMLStatementBuilder.parseStatementNode(),这里不详细介绍。
MappedStatement对象记录了SQL节点来自哪个xml文件,SQL语句是什么,返回结果类型是什么等等信息。有了它就知道该执行什么SQL语句,以及如何对结果集做数据映射了,因此这个类也十分重要。
类比较庞大,篇幅原因,只作属性的说明:
public final class MappedStatement {
// 来自哪个xml文件
private String resource;
// 全局配置
private Configuration configuration;
// 唯一标识:接口全限定名+方法名
private String id;
// 限制SQL执行返回的最大行数,避免大数据量的查询导致OOM
private Integer fetchSize;
// SQL执行的超时时间
private Integer timeout;
// 执行SQL所使用的Statement类型
private StatementType statementType;
// 返回的结果集类型
private ResultSetType resultSetType;
// 执行的SQL源,通过它来获取执行的SQL语句
private SqlSource sqlSource;
// 二级缓存
private Cache cache;
// 标签中parameterMap属性的封装
private ParameterMap parameterMap;
// <resultMap>标签的封装,配置数据库字段和Java类属性的映射关系
private List<ResultMap> resultMaps;
// 是否需要刷新缓存
private boolean flushCacheRequired;
// 是否使用缓存
private boolean useCache;
private boolean resultOrdered;
// SQL命令的类型(增删改查)
private SqlCommandType sqlCommandType;
// 主键生成器,insert数据时返回主键
private KeyGenerator keyGenerator;
// 主键应用的属性
private String[] keyProperties;
// 主键应用的列
private String[] keyColumns;
// 是否存在嵌套的结果
private boolean hasNestedResultMaps;
// 数据源ID,区分多数据源
private String databaseId;
// 日志
private Log statementLog;
// 不同语言驱动
private LanguageDriver lang;
// 返回多结果集时使用
private String[] resultSets;
}
3.10 Executor
Executor接口是MyBatis提供的操作数据库的执行器接口,SqlSession操作数据库就是委托Executor去执行的。
贴出部分Executor定义,看看它具有的能力,主要是操作数据库、事务的管理、缓存的管理等。
public interface Executor {
/**
* 执行SQL查询
* @param ms 执行的的Statement,理解为:执行哪个xml的哪个sql标签节点?
* @param parameter 参数
* @param rowBounds 分页数据
* @param resultHandler 结果处理器
* @param <E>
* @return
* @throws SQLException
*/
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
// 游标查询
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
// 批量执行
List<BatchResult> flushStatements() throws SQLException;
// 事务提交
void commit(boolean required) throws SQLException;
// 事务回滚
void rollback(boolean required) throws SQLException;
// 创建缓存键,用于一级缓存
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
// 是否命中缓存
boolean isCached(MappedStatement ms, CacheKey key);
// 清除本地缓存
void clearLocalCache();
// 省略部分代码......
}
Executor有个抽象子类BaseExecutor,它是其他实现类的父类,采用的是【模板方法】模式,实现了基本功能。
子类如下:
| 实现类 | 说明 |
|---|---|
| SimpleExecutor | 简单执行器,每次执行都创建新的Statement |
| ReuseExecutor | 重用执行器,缓存同一个SQL的Statement,避免Statement频繁创建,优化性能 |
| BatchExecutor | 批量操作执行器 |
| CachingExecutor | 支持二级缓存的执行器,装饰者模式 |
默认是开启缓存的,因此我们直接看CachingExecutor.query()。它首先会调用ms.getBoundSql()完成SQL的解析,这一步会完成SQL的动态拼接,完成${}/#{} 参数的替换。
然后创建缓存键CacheKey,这是MyBatis自带的基于SqlSession的一级缓存,根据【StatementID+SQL+参数+分页】的规则创建,只有这些数据完全相同才会命中缓存。
然后就是调用query()进行查询了。
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 根据实参完成SQL的解析,替换#{}和${},得到可以直接执行的SQL
BoundSql boundSql = ms.getBoundSql(parameterObject);
//System.err.println("###SQL:" + boundSql.getSql());
// 根据执行的 [StatementID+SQL+参数+分页] 来创建缓存键,只有这些数据全部相同才能命中缓存。
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
CachingExecutor是支持二级缓存的,使用【装饰者模式】,内部依赖一个Executor,CachingExecutor本身不会处理数据库操作,它的作用就是判断查询是否命中二级缓存,没有命中则委派delegate去查询,然后将结果缓存起来。
如下是CachingExecutor重写的query()方法。
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) { // Mapper开启二级缓存的情况
// 判断是否需要清空缓存
flushCacheIfRequired(ms);
// 当前Statement是否使用缓存
if (ms.isUseCache() && resultHandler == null) {
// 如果调用的是存储过程,二级缓存不支持保存输出类型的参数,会抛异常
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 从缓存中获取数据
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 缓存中没有,则查询数据库,并缓存结果
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 委托delegate去查询
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
如果没有命中二级缓存,会调用BaseExecutor.query()进行查询。BaseExecutor会先判断是否命中一级缓存,如果没命中才会真的去查询数据库。
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 向ErrorContext报告自己正在做查询
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
// 如果需要刷新缓存,则清空本地缓存
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 试图从一级缓存中获取数据
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 查询数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
在没有命中任何缓存的情况下,会调用doQuery()去真正的查询数据库。我们直接看SimpleExecutor.doQuery()。
它首先会创建StatementHandler,然后准备好JDBC原生的Statement,最后调用JDBC原生的Statement.execute()去执行SQL,然后对结果集进行映射处理,得到最终的结果。
@Override
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();
/*
创建RoutingStatementHandler,装饰者模式
根据StatementType创建[Simple/Prepared/Callable]StatementHandler
*/
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
/**
准备Statement
1.创建对应的Statement
2.设置好Timeout、FetchSize
3.设置好参数
*/
stmt = prepareStatement(handler, ms.getStatementLog());
// 执行查询,并完成结果集映射
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
3.11 StatementHandler
StatementHandler是MyBatis中对Statement的处理器描述。当Executor要执行SQL时,会通过Configuration.newStatementHandler()先创建StatementHandler,默认创建的是RoutingStatementHandler。
RoutingStatementHandler使用的【委派模式】,它本身不干活,只是根据StatementType创建对应的StatementHandler,然后委托它去干活。
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 根据StatementType创建委托对象
switch (ms.getStatementType()) {
case STATEMENT:// 普通的
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:// 预编译的,支持设置参数
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:// 支持调用存储过程
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
StatementType枚举有三个实例,分别对应JDBC三种Statement,如下:
public enum StatementType {
STATEMENT, // Statement 普通SQL,无法设置参数
PREPARED, // PreparedStatement 预编译的,防止SQL注入
CALLABLE // CallableStatement 支持调用存储过程
}
StatementHandler和JDBCStatement的对应关系:
| StatementHandler | JDBC Statement |
|---|---|
| SimpleStatementHandler | Statement |
| PreparedStatementHandler | PreparedStatement |
| CallableStatementHandler | CallableStatement |
我们先看下准备Statement的流程:先调用子类的instantiateStatement()方法创建对应的Statement,再给Statement设置好Timeout和FetchSize。
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
/*
根据StatementType创建JDBC原生的Statement
SimpleStatementHandler > connection.createStatement()
PreparedStatementHandler > connection.prepareStatement()
CallableStatementHandler > connection.prepareCall()
*/
statement = instantiateStatement(connection);
// 设置超时时间
setStatementTimeout(statement, transactionTimeout);
// 设置FetchSize,xml没有配置,则取
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("Error preparing statement. Cause: " + e, e);
}
}
我们这里是根据用户ID查询,且xml里使用的是#{id},所以用到的是PreparedStatementHandler。PreparedStatementHandler创建的Statement当然就是PreparedStatement了。
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
return connection.prepareStatement(sql, keyColumnNames);
}
} else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
return connection.prepareStatement(sql);
} else {
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
}
}
PreparedStatement创建好了,接下来就是设置参数了,代码在DefaultParameterHandler.setParameters()。
它会根据参数类型找到对应的TypeHandler,TypeHandler是MyBatis提供的类型处理器接口,它的作用有两个:一是给Statement设置参数、二是从ResultSet中获取结果做类型转换。
@Override
public void setParameters(PreparedStatement ps) {
// 向ErrorContext报告自己正在设置参数
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
// 获取参数映射
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 获取TypeHandler,我们的参数是Long,所以就是LongTypeHandler
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
/*
当参数不为空时,MyBatis可以根据Java类型推断出JdbcType。
参数为空时,就无法推断了,此时会使用默认类型:JdbcType.OTHER
Oracle数据库中,JdbcType.OTHER传入NULL会报异常:无效的列类型,此时必须指定JdbcType
*/
jdbcType = configuration.getJdbcTypeForNull();
}
try {
// 设置参数
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
我们的参数是Long id,所以对应的就是LongTypeHandler,看看它设置参数的过程,其实很简单。
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Long parameter, JdbcType jdbcType)
throws SQLException {
// 给第i号参数设置Long类型的值
ps.setLong(i, parameter);
}
PreparedStatement创建好了,参数也设置完毕了,就可以直接执行了。执行后拿到结果集ResultSet,就剩下结果集映射,将ResultSet转换成Java Bean了。
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 执行Statement
ps.execute();
// 结果集处理
return resultSetHandler.handleResultSets(ps);
}
3.12 ResultSetHandler
ResultSetHandler是MyBatis提供的结果集处理器,它的作用是负责将数据库返回的结果集ResultSet转换成我们想要的Java Bean。
默认的实现类是DefaultResultSetHandler,它首先会将JDBC原生的ResultSet包装成MyBatis的ResultSetWrapper,然后调用handleResultSet()处理结果集,将单个ResultSet转换成JavaBean并存入list中,然后循环处理,最终返回list结果。
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
// 向ErrorContext报告自己正在处理结果集
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
// 可能有多个结果集,所以用List存放
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
// 如果结果集存在,就将其包装为MyBatis的ResultSetWrapper对象
ResultSetWrapper rsw = getFirstResultSet(stmt);
// <insert>标签配置的resultMap属性对应的标签集
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
// 校验数量
validateResultMapsCount(rsw, resultMapCount);
// 处理
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
// 处理结果集,得到Java对象集合存入multipleResults
handleResultSet(rsw, resultMap, multipleResults, null);
// 获取下一个结果,继续循环处理
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
// 处理resultSets属性,多结果集
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
// 如果只有单个结果集,则返回multipleResults的第0个元素,否则直接返回multipleResults
return collapseSingleResultList(multipleResults);
}
为什么要将ResultSet包装成ResultSetWrapper?是为了方便做结果映射和类型转换。
ResultSetWrapper记录了结果集返回的所有列,以及列对应的Java类型和JDBC类型,还有列对应的TypeHandler,需要它来做结果集的类型转换。篇幅原因,这里贴部分代码:
public class ResultSetWrapper {
// JDBC原生结果集
private final ResultSet resultSet;
// TypeHandler注册器
private final TypeHandlerRegistry typeHandlerRegistry;
// 列名
private final List<String> columnNames = new ArrayList<>();
// 对应的Class名称
private final List<String> classNames = new ArrayList<>();
// 对应的JdbcType
private final List<JdbcType> jdbcTypes = new ArrayList<>();
private final Map<String, Map<Class<?>, TypeHandler<?>>> typeHandlerMap = new HashMap<>();
private final Map<String, List<String>> mappedColumnNamesMap = new HashMap<>();
private final Map<String, List<String>> unMappedColumnNamesMap = new HashMap<>();
public ResultSetWrapper(ResultSet rs, Configuration configuration) throws SQLException {
super();
this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
this.resultSet = rs;
// 获取结果集元数据
final ResultSetMetaData metaData = rs.getMetaData();
// 结果集中列的数量
final int columnCount = metaData.getColumnCount();
// 解析所有列,得到其列名、JdbcType、以及对应的Java类
for (int i = 1; i <= columnCount; i++) {
columnNames.add(configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i));
jdbcTypes.add(JdbcType.forCode(metaData.getColumnType(i)));
classNames.add(metaData.getColumnClassName(i));
}
}
}
handleResultSet()用户处理结果集,它首先会创建一个DefaultResultHandler,内部会有一个List,然后调用handleRowValues()处理返回的行记录,并将处理结果添加到List中。
/**
* 处理结果集,将结果集转换成Java对象并存入multipleResults
* @param rsw 结果集包装对象
* @param resultMap 结果映射,resultType属性会被转换成resultMap
* @param multipleResults 最终的Java对象结果集
* @param parentMapping <resultMap>中配置的列和属性的映射关系
* @throws SQLException
*/
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
if (parentMapping != null) {
handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
} else { // 我们没有配,所以走这里
/**
没有ResultHandler,则返回结果。
有ResultHandler,则将结果交给它处理,不返回List结果集。
*/
if (resultHandler == null) {
/**
ObjectFactory:MyBatis依赖它反射创建对象、给对象赋值。
DefaultResultHandler:默认的结果处理器。
*/
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
// 处理返回的数据行记录,转换成Java对象集合,存入DefaultResultHandler
handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
// 将转换后的Java结果集存入multipleResults,并返回
multipleResults.add(defaultResultHandler.getResultList());
} else {
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
// issue #228 (close resultsets)
closeResultSet(rsw.getResultSet());
}
}
如果ResultMap不包含嵌套结果映射的话,会调用handleRowValuesForSimpleResultMap()方法,它会处理分页,跳过一部分数据,然后解析鉴别器Discriminator,然后调用getRowValue()获取行结果并转换成Java对象,最终将Java对象存入List。
/**
* 处理结果结果映射,不包含嵌套
* @param rsw
* @param resultMap
* @param resultHandler
* @param rowBounds
* @param parentMapping
* @throws SQLException
*/
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
ResultSet resultSet = rsw.getResultSet();
// 根据分页规则跳过一些数据
skipRows(resultSet, rowBounds);
// 如果有更多的数据,则处理它
while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
// 解析鉴别器Discriminator
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
// 从结果集中获取Java对象
Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
// 将单个结果对象存入到list中
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
getRowValue()首先会调用createResultObject()利用反射创建结果对象,如果结果对象是复杂对象,此时它还是个空对象,还需要调用applyAutomaticMappings()进行属性的赋值,最终才可以返回。
/**
* 从数据行中获取结果
*
* @param rsw 结果集包装对象
* @param resultMap 结果集映射
* @param columnPrefix 列名前缀
* @return
* @throws SQLException
*/
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
/**
创建结果对象
1.如果是简单对象,如Long,此时已经得到结果了。
2.如果是复杂对象,如自定义User,此时拿到的是空对象,需要下面的属性赋值。
*/
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
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, columnPrefix) || foundValues;
}
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
// 返回赋值后的结果对象
return rowValue;
}
createResultObject()根据构造函数来反射创建结果对象:
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
this.useConstructorMappings = false; // reset previous mapping result
// 构造函数形参类型列表
final List<Class<?>> constructorArgTypes = new ArrayList<>();
// 构造函数实参列表
final List<Object> constructorArgs = new ArrayList<>();
/**
创建结果对象
1.如果返回结果类存在对应的TypeHandler,则直接处理了,例如返回Long,这里会直接完成类型转换。
2.如果返回结果为复杂类,例如自定义的User,则会调用构造函数创建空对象。
*/
Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
// 存在结果,且为复杂对象
if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
// issue gcode #109 && issue #149
if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
break;
}
}
}
this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
return resultObject;
}
applyAutomaticMappings()用来给结果对象属性赋值:
// 给结果对象赋值
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)) {
// gcode issue #377, call setter on nulls (value is not 'found')
metaObject.setValue(mapping.property, value);
}
}
}
return foundValues;
}
最终List里就会有一个User对象,然后selectOne()会取List中第0个元素返回,这样代理对象就能返回最终结果了。
4. 总结
MyBatis程序启动,首先会去解析配置文件创建Configuration对象,再利用Configuration去构建SqlSessionFactory,有了SqlSessionFactory就可以打开一个SqlSession。基于SqlSession我们可以拿到Mapper接口的代理对象MapperProxy,当我们调用Mapper的查询方法时,代理对象会自动帮我们调用SqlSession的select方法,SqlSession又会委托Executor去调用query方法,再根据StatementType创建对应的JDBC原生的Statement并完成参数的设置,最终执行SQL拿到结果集ResultSet,最后根据ResultMap完成结果映射,将ResultSet转换成目标结果Java Bean,最终通过代理对象返回。
东西太多了,有些可能三句两句讲不清我就跳过了,以后会针对核心类单独写文章记录,敬请期待吧~
本文详细剖析了MyBatis查询数据库的全过程,包括配置文件解析、SqlSessionFactory构建、SqlSession操作、Mapper代理对象生成及结果集映射等关键步骤。
1170

被折叠的 条评论
为什么被折叠?



