MyBatis动态SQL及执行流程

动态SQL

如果使用传统的JDBC编程,很多时候需要根据具体情况拼接SQL语句,这是一件很痛苦的事情。Mybatis为开发者提供了动态SQL语句的组装能力,并且如此强大的功能只有几个元素就可以完成,十分简单明了。大量的判断都可以在Mybatis的映射XML中完成。大大减少了编码的工作量,这也体现了Mybatis的灵活性和可维护性。下面首先了解一下Mybatis中常用的动态SQL元素。

元素 作用 备注
if 判断语句 单条件分支判断
choose(when、otherwise) 相当于Java中的case when 多条件分支判断
trim(where、set) 辅助元素 用于处理SQL拼装
foreach 循环语句 在in语句等列举条件中常用

下面,就将上述元素进行深入讨论。

if元素

if元素是日常开发中较为常用的判断语句,相当于Java中的if语句,它常常与test属性联合使用。

if元素使用方法比较简单,以第3章中的数据库为例,根据学生姓名进行模糊查询。

映射器中代码如下:

1
2
3
4
5
6
7
<select id="findStudentByStudentName" resultType="cn.bytecollege.entity.Student" parameterType="String">
SELECT STUDENT_ID,STUDENT_NAME,STUDENT_AGE,STUDENT_GENDER
FROM STUDENT WHERE 1=1
<if test="studentName != null and studentName!=''">
AND STUDENT_NAME LIKE CONCAT('%',#{studentName},'%')
</if>
</select>

接口中定义方法:

1
2
3
public interface StudentMapper {
List<Student> findStudentByStudentName(String studentName);
}

在示例代码中使用了if元素,if元素的test属性中判断studentName是否有值,如果有值则在已有SQL语句后拼接if元素中的语句,如果没有值则不拼接,下面首先传递studentName值做测试,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) throws IOException {
final Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);
//1.
List<Student> list = mapper.findStudentByStudentName("j");
list.forEach(e->logger.info(e));
}
}

运行结果如下:

可以看出,当studentName有值时拼接了SQL语句。

在上例代码中注释1处的方法调用中传入null,再次测试,运行结果如下:

从结果中可以看出SQL语句并没有拼接if元素中的语句。

choose、when、otherwise元素

在上一小节中的if元素相当于Java当中的if分支结构,是一种非此即彼的关系,但是在很多时候开发者所面对的不一定是非此即彼的情形,可能有更多的选择或者分支,此时虽说if可以满足需要,但是代码显得很蹩脚。因此Mybatis为开发者提供了类似于多分支的结构,并且这种结构在做根据条件搜索时显得尤为方便,避免了繁复的代码和判断。例如:根据学生姓名或者年龄或者性别检索数据。

在映射器中配置select元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<select id="searchStudent" resultType="cn.bytecollege.entity.Student" parameterType="cn.bytecollege.entity.Student">
SELECT STUDENT_ID,STUDENT_NAME,STUDENT_AGE,STUDENT_GENDER
FROM STUDENT WHERE 1=1
<choose>
<when test="studentName != null and studentName != ''">
AND STUDENT_NAME = #{studentName}
</when>
<when test="studentAge != null and studentAge != ''">
AND STUDENT_AGE = #{studentAge}
</when>
<when test="studentGender != null and studentGender != ''">
AND STUDENT_GENDER = #{studentGender}
</when>
<otherwise>
AND STUDENT_ID IS NOT NULL;
</otherwise>
</choose>
</select>

Mybatis会根据参数的设置进行判断来动态组装SQL,以满足不同的业务需求。

接口中定义方法:

1
2
3
public interface StudentMapper {
List<Student> searchStudent(Student student);
}

新建测试类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
public static void main(String[] args) throws IOException {
final Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);

Student student = new Student();
student.setStudentName("Jack");

List<Student> list = mapper.searchStudent(student);
list.forEach(e->logger.info(e));
}
}

运行结果如下图:

where、trim、set元素

where元素

在上面的代码中,每条SQL语句中都添加了一个1=1这个条件,这是因为如果不添加这个条件上一小节中的SQL语句可能会下面这样错误的SQL语句:

1
SELECT STUDENT_ID,STUDENT_NAME,STUDENT_AGE,STUDENT_GENDER FROM STUDENT WHERE  AND STUDENT_NAME = ?

但是加上这个莫名其妙的条件又显得很奇怪,在这里就可以使用where元素以达到逾期的效果。对上一小节中的SQL语句进行修改,去掉1=1,并且将choose…when修改为where和if,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="searchStudent" resultType="cn.bytecollege.entity.Student" parameterType="cn.bytecollege.entity.Student">
SELECT STUDENT_ID,STUDENT_NAME,STUDENT_AGE,STUDENT_GENDER
FROM STUDENT
<where>
<if test="studentName != null and studentName != ''">
AND STUDENT_NAME = #{studentName}
</if>
<if test="studentAge != null and studentAge != ''">
AND STUDENT_AGE = #{studentAge}
</if>
<if test="studentGender != null and studentGender != ''">
AND STUDENT_GENDER = #{studentGender}
</if>
</where>
</select>

当where元素中的条件只要有一个成立时,会在已有SQL语句后拼接where以及成立的条件,如果没有条件成立,则不会拼接SQL语句,也不会拼接where,下面的示例将演示这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) throws IOException {
final Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);

List<Student> list = mapper.searchStudent(null);
list.forEach(e->logger.info(e));
}
}

运行结果如下:

在测试类中方法内传入了null,所有的参数都为null,不满足where元素中if元素的条件,因此Mybatis并没有拼接where及条件。

trim元素

有时候需要去掉一些特殊的SQL语法,比如常见的and、or。使用trim元素就可以达到这种效果。

1
2
3
4
5
6
7
8
9
<select id="trimTest" parameterType="String">
SELECT STUDENT_ID,STUDENT_NAME,STUDENT_AGE,STUDENT_GENDER
FROM STUDENT
<trim prefix="where" prefixOverrides="and">
<if test="studentName !=null ">
AND STUDENT_NAME = #{studentName}
</if>
</trim>
</select>

注意上面的SQL语句,如果studentName不等于null时,此时的SQL语句会变成:

1
2
3
SELECT STUDENT_ID,STUDENT_NAME,STUDENT_AGE,STUDENT_GENDER
FROM STUDENT
AND STUDENT_NAME = #{studentName}

这条SQL语句会产生语法错误,trim元素此时就要发挥作用了,会将and替换为where,也就是说trim元素的作用就是将prefixOverrides中配置的值替换为prefix中的值。

set元素

在日常开发中更新操作出现的也比较频繁,但是在更新操作中通常会只更新几个字段,而不是全部更新,如果将所有的字段都发送给数据库,这样就会带来不必要的内存和宽带开销,set元素就可以解决这种问题,例如更新学生信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<update id="updateStudent" parameterType="cn.bytecollege.entity.Student">
UPDATE STUDENT
<set>
<if test="studentName!=null and studentName !=''">
STUDENT_NAME = #{studentName},
</if>
<if test="studentAge !=null and studentAge !=''">
STUDENT_AGE = #{studentAge},
</if>
<if test="studentGender !=null and studentGender !=''">
STUDENT_GENDER = #{studentGender}
</if>
</set>
WHERE STUDENT_ID = #{studentId}
</update>

接口中定义方法:

1
2
3
public interface StudentMapper {
int updateStudent(Student student);
}

新建测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
public static void main(String[] args) throws IOException {
final Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);

Student student = new Student();
student.setStudentId(7);
student.setStudentAge(20);

mapper.updateStudent(student);
session.commit();
}
}

运行结果如下:

因为传入的参数中只有studentAge,因此Mybatis在拼接SQL语句时只拼接了满足studentAge的条件。

foreach元素

foreach元素是一个循环语句,作用是遍历集合,它能够很好的支持数组和List、Set接口的集合,对此提供遍历功能。例如删除多条数据,就可以使用foreach元素。

1
2
3
4
5
6
<delete id="batchDeleteStudent" parameterType="List">
DELETE FROM STUDENT WHERE STUDENT_ID IN
<foreach collection="list" open="(" close=")" separator="," item="id">
#{id}
</foreach>
</delete>
  • collection:配置的是传递进来的参数名称,可以是一个数组或者List、set等集合
  • item:配置的是循环中的元素,可以理解为数组或者集合中的元素的名称
  • open和close:配置的是以什么符号将这些集合元素包装起来
  • separator:是各个元素的分隔符

接口中定义方法:

1
2
3
public interface StudentMapper {
int batchDeleteStudent(List list);
}

新建测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
public static void main(String[] args) throws IOException {
final Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);

mapper.batchDeleteStudent(list);
session.commit();
}
}

运行结果如下:

Mybaits分页

RowBounds分页

Mybatis具有分页功能,其为开发者提供了一个类RowBounds,开发者可以使用RowBounds分页,但是RowBounds分页有一个很重要的缺陷,就是会将所有的数据查询出来后根据从第几条到第几条取出数据返回,当数据量比较小时,这么做没有任何问题,但是当数据量大时,这么做无疑对数据库造成了很大的负担。

RowBounds主要定义了两个参数,offset和limit,offset代表从第几行开始读取数据,limit则是限制返回的记录数。在默认情况下offset设置为0,而limit这是Java所允许的最大整数(2147483647)。

下面通过示例学习RowBounds的用法:

1
2
3
4
<select id="findStudentByPage" resultType="cn.bytecollege.entity.Student">
SELECT STUDENT_ID,STUDENT_NAME,STUDENT_AGE,STUDENT_GENDER
FROM STUDENT
</select>

在接口中定义方法:

1
2
3
public interface StudentMapper {
List<Student> findStudentByPage(RowBounds rowBounds);
}

新建测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) throws IOException {
final Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);
//1.
List<Student> list = mapper.findStudentByPage(new RowBounds(0,5));
list.forEach(e->logger.info(e));
}
}

在代码中注释1处,调用方法是传入RowBounds对象。即可完成分页。

运行结果如下:

从结果中可以看出使用RowBounds分页时,会将所有的数据都查询出来,然后从数据中截取目标数据,也就是通常所说的逻辑分页。

插件分页

基于RowBounds分页存在一定缺陷,通常会推荐使用自定义插件分页,并且又第三方提供了性能不错的分页插件PageHelper,下面就改插件做简单示范:

首先,在pom.xml中添加依赖

1
2
3
4
5
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>最新版本</version>
</dependency>

在Mybatis全局文件中进行配置:

1
2
3
4
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
</plugin>
</plugins>

由于Mybatis的插件是基于代理拦截实现,所以继续使用上例中的接口及测试类,插件会自动帮助开发者在SQL中拼接limit关键字。运行结果如下:

关于该插件的详细用法,可参考文档:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md

Mybatis执行流程

在前面的章节中系统学习了Mybatis框架的使用,但是学习框架并不能只停留于如何使用的阶段,需要深入去了解Mybatis的执行流程。在本章节内,将详细了解Mybatis的实现细节。

根据前面的章节,可以总结出Mybatis执行流程主要分为以下4个阶段:

  1. 获取SQLSessionFactory
  2. 获取SQLSession
  3. 执行SQL语句

下面就3个阶段进行分析。

创建SqlSessionFactroy

在创建SqlSessionFactory对象之前,需要先创建SqlSessionFactoryBuilder对象,该对象的主要作用就是解析Mybatis的全局配置文件并将配置文件抽象为Configuration对象。下面就从以下代码入手,了解该过程。

1
2
3
4
5
6
public static void main( String[] args ) throws FileNotFoundException {
InputStream is = App.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
//1.进入创建SqlSessionFactory流程
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
}

从注释1处入手,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public SqlSessionFactory build(InputStream inputStream) {
//可以看出此处调用了重载方法。
return build(inputStream, null, null);
}

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//1.此处解析xml配置文件
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//2.先调用了parse.parse()方法,该方法会将XML的配置抽象为Configuration对象
//3.调用build方法返回DefaultSessionFactory对象
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
//忽略解析的细节可以看出该方法的作用就是将XML文件解析为Configuration对象
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}

至此,已经大致了解了创建SqlSessionFactory的流程,过程如下:

  1. SqlSessionFactoryBuilder读取全局配置文件
  2. SqlSessionFactory读取配置文件后解析配置文件将配置文件解析为Configuration对象
  3. SqlSessionFactory通过Configuration对象创建了DefaultSQLSessionFactory对象

以上过程的时序图如下:

创建SqlSession

当创建完SqlSessionFactroy后,可以通过SqlSessionFactory的openSession方法创建SqlSession对象。

1
SqlSession session = sqlSessionFactory.openSession();

继续查看openSession源码。

DefaultSqlSessionFactory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//通过Configuration对象获取数据源和事务配置信息
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//获取sql语句执行器,从表面上看是SqlSession执行了sql语句,其实是Executor
//而Executor实际上是对JDBC中Statement对象的封装
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();
}
}

在执行完以上方法后就可获得SqlSession对象,这个过程比较简单,需要注意的是Mybatis中真正执行sql语句的对象并不是SqlSession,而是Executor对象。接下来将进入最重要的环节,执行sql语句。

执行Sql语句

在经历前面两步的过程后,已经获取到SqlSession对象了,接下来,以查询为例查看Sql语句的具体执行流程。查询代码如下:

1
2
3
4
5
6
7
8
public static void main( String[] args ) throws FileNotFoundException {
InputStream is = App.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession();

UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User>list = mapper.findAllUser();
}

从上面的代码可以看出直接调用了mapper中定义的findAllUser()方法,但是mapper是一个接口,findAllUser又是一个抽象方法,并没有方法体,那么Mybaits是怎么执行的呢?在这里需要先了解MapperProxy。这个类的作用是为Mapper接口生成代理类,也就是子类对象。

MapperProxy

首先从sqlSession.getMapper()方法入手,从前面的代码中可以知道此时的sqlSession,实际上是DefaultSqlSession对象,因此进入DefaultSqlSession对象中的getMapper()方法,代码如下:

DefaultSqlSession:

1
2
3
4
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}

从代码中可以看出该方法仅仅是从Configuration对象中读取了配置。接下来进入configuration.getMapper()方法,代码如下:

Configuration:

1
2
3
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}

在Configuration对象的getMapper方法中调用了mapperRegistry的getMapper()方法,继续进入MapperRegistry的getMapper()方法。

MapperRegistry:

1
2
3
4
5
6
7
8
9
10
11
12
13
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//首先创建代理工厂类对象mapperProxyFactory
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);
}
}

继续进入mapperProxyFactory.newInstance()方法。

MapperProxyFactory

1
2
3
4
5
6
7
8
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
//在这里创建接口的实现类
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

在经历了以上步骤后,Mybaits生成了Mapper接口的代理类,接下来就可以执行sql语句了。

Executor

此时执行findAllUser()方法时,Mybatis会进入MapperProxy的invoke()方法,进入此方法查看源码:

MapperProxy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
//代码会执行到此处
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
//交由mapperMethod.execute()方法
return mapperMethod.execute(sqlSession, args);
}

继续查看MapperMethod的execute()方法:

MapperMethod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//判断SQL语句类型
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()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
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;
}

可以看出在这个方法中主要对SQL语句所做的操作进行了判断,并且执行对用的操作。在示例中我们查询了所有的用户,因此会进入executeForMany()方法。

MapperMethod:

1
2
3
4
5
6
7
8
9
10
11
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
//1.判断SQL有没有分页
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
//2.没有分页,因此代码执行到此处
result = sqlSession.selectList(command.getName(), param);
}

在该方法中判断SQL语句是否有分页,因为示例中没有分页,因此代码会执行到注释2处。继续查看此方法,会发现调用了SqlSession的selectList()方法。

DefaultSqlSession:

1
2
3
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}

继续进入重载的selectList()方法:

DefaultSqlSession:

1
2
3
4
5
6
7
8
9
10
11
12
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//1.从Configuration中获取MappedStatements,MappedStatements即Mapper文件抽象的对象
MappedStatement ms = configuration.getMappedStatement(statement);
//2.调用query方法
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

在该方法中获取了MappedStatement后调用了query()方法,继续进入该方法。因为在示例中并未开启二级缓存,因此在此处直接调用BaseExecutor的方法。

BaseExecutor:

1
2
3
4
5
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

继续进入query()方法:

BaseExecutor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
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 {
//1.核心代码在此处
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;
}

进入此query方法后,核心查询在注释1处,继续进入queryFromDatabase()方法。

BaseExecutor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//1.本质上时调用了这个方法
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}

可以看出,在此方法内调用了doQuery()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@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();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
//1.最终会调用query方法。
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}

继续进入注释1处的query方法,代码最终会执行到PreparedStatementHandler中的query()方法,代码如下:

1
2
3
4
5
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}

代码执行到此处已经是我们所熟悉的JDBC了,在执行完SQL语句后,接下来就要对查询结果进行解析。整个过程的时序图如下:

小结

总结上述流程可以梳理为以下步:

  1. 加载配置文件解析为Configuration对象
  2. 使用Configuration对象获取DefaultSqlSessionFactory对象
  3. 通过DefaultSqlSessionFactory对象获取DefaultSqlSession对象
  4. 通过DefaultSqlSession创建MapperProxy对象
  5. MapperProxy调用MapperMthod方法判断SQL操作类型
  6. 判断是否开启缓存二级缓存
  7. 将SQL语句交由BaseExecutor处理
  8. 判断在一级缓存中是否可以命中SQL语句执行结果,如果有则返回结果
  9. 如果没有数据在调用RoutingStatemenHandler选则SQL语句执行方式
  10. 默认情况下,MappedStatement为预编译执行,所以使用PrepareStatementHandler
  11. 最后通过DefaultResultSetHandler封装结果给客户端

整个过程流程图如下: