Mybatis如何执行Select语句,你真的知道吗?

本文详细分析了Mybatis中执行Select语句的底层逻辑,从环境准备到源码跟踪,揭示了SqlSession、Executor、StatementHandler和ResultSetHandler等组件在查询过程中的作用。通过一个具体的查询示例,解释了如何从二级缓存、一级缓存到数据库的查询流程,并对整个执行过程进行了总结。

前言

  • 本篇文章是Myabtis源码分析的第三篇,前两篇分别介绍了Mybatis的重要组件和围绕着Mybatis中的重要组件教大家如何阅读源码的一些方法。

  • 今天接上一篇,围绕Mybatis中的selectList()来看一看Mybatis底层到底做了什么,有什么高级的地方。

环境准备

  • 本篇文章讲的一切内容都是基于Mybatis3.5SpringBoot-2.3.3.RELEASE

  • 由于此篇文章是基于前两篇文章的基础之上,因此重复的内容不再详细赘述了。

撸起袖子就是干

  • 二话不说,先来一张流程图,Mybatis六剑客,如下:六剑客执行流程图

  • 上图中的这六剑客在前面两篇文章中已经介绍的非常清楚了,此处略过。为什么源码解析的每一篇文章中都要放一张这个流程图呢?因为Mybatis底层就是围绕着这六剑客展开的,我们需要从全局掌握Mybatis的源码究竟如何执行的。

测试环境搭建

  • 举个栗子:根据用户id查询用户信息,Mapper定义如下:

List<UserInfo> selectList(@Param("userIds") List<String> userIds);
  • 对应XML配置如下:

<mapper namespace="cn.cb.demo.dao.UserMapper">
    <!--开启二级缓存-->
    <cache/>
    <select id="selectList" resultType="cn.cb.demo.domain.UserInfo">
        select * from user_info where status=1
        and user_id in
        <foreach collection="userIds" item="item" open="(" separator="," close=")" >
            #{item}
        </foreach>
    </select>
</mapper>
  • 单元测试如下:

    @Test
    void contextLoads() {
        List<UserInfo> userInfos = userMapper.selectList(Arrays.asList("192","198"));
        System.out.println(userInfos);
    }

DEBUG走起

  • 具体在哪里打上断点,上篇文章已经讲过了,不再赘述了。

  • 由于SpringBoot与Mybatis整合之后,自动注入的是SqlSessionTemplate,因此代码执行到org.mybatis.spring.SqlSessionTemplate#selectList(java.lang.String, java.lang.Object),如图1

  • 从源码可以看到,实际调用的还是DefaultSqlSession中的selectList方法。如下图2

  • 「具体的逻辑如下」

    1. 根据Mapper方法的全类名从Mybatis的配置中获取到这条SQL的详细信息,比如paramterType,resultMap等等。

    2. 既然开启了二级缓存,肯定先要判断这条SQL是否缓存过,因此实际调用的是CachingExecutor这个缓存执行器。

  • DefaultSqlSession只是简单的获取SQL的详细配置,最终还是把任务交给了Executor(当然这里走的是二级缓存,因此交给了缓存执行器)。下面DEBUG走到CachingExecutor#query(MappedStatement, java.lang.Object, RowBounds,ResultHandler),源码如下图3

  • 上图中的query方法实际做了两件事,实际执行的查询还是其中重载的方法List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql),如下图4

  • 根据上图源码的分析,其实CachingExecutor执行的逻辑并不是很难,反倒很容易理解,「具体的逻辑如下」

    1. 如果开启了二级缓存,先根据cacheKey从二级缓存中查询,如果查询到了直接返回

    2. 如果未开启二级缓存,再执行BaseExecutor中的query方法从一级缓存中查询。

    3. 如果二级缓存中未查询到数据,再执行BaseExecutor中的query方法从一级缓存中查询。

    4. 将查询到的结果存入到二级缓存中。

  • BaseExecutor中的query方法无非就是从一级缓存中取数据,没查到再从数据库中取数据,一级缓存实际就是一个Map结构,这里不再细说,真正执行SQL从数据库中取数据的是SimpleExecutor中的public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)方法,源码如下图5

  • 从上面的源码也是可以知道,在真正执行SQL之前,是要调用prepareStatement(handler, ms.getStatementLog())方法做一些参数的预处理的,其中涉及到了六大剑客的另外两位,分别是ParameterHandlerTypeHandler,源码如图6

  • 从上图可以知道设置SQL参数的真正方法是handler.parameterize(stmt),真正执行的是DefaultParameterHandler中的setParameters方法,由于篇幅较长,简单的说一下思路:

    1. 获取所有参数的映射

    2. 循环遍历,获取参数的值,使用对应的TypeHandler将其转换成相应类型的参数。

    3. 真正的设置参数的方法是TypeHandlersetParameter方法

  • 继续图6的逻辑,参数已经设置完了,此时就该执行SQL了,真正执行SQL的是PreparedStatementHandler中的<E> List<E> query(Statement statement, ResultHandler resultHandler)方法,源码如下图7

  • 上图的逻辑其实很简单,一个是JDBC执行SQL语句,一个是调用六剑客之一的ResultSetHandler对结果进行处理。

  • 真正对结果进行处理的是DefaultResultSetHandler中的handleResultSets方法,源码比较复杂,这里就不再展示了,具体的逻辑如下:

    1. 获取结果映射(resultMap),如果没有指定,使用内置的结果映射

    2. 遍历结果集,对SQL返回的每个结果通过结果集和TypeHandler进行结果映射。

    3. 返回结果

  • ResultSetHandler对结果处理结束之后就会返回。至此一条selectList()如何执行的大概心里已经有了把握,其他的更新,删除都是大同小异。

总结

  • Mybatis的源码算是几种常用框架中比较简单的,都是围绕六大组件进行的,只要搞懂了每个组件是什么角色,有什么作用,一切都会很简单。

  • 一条select语句简单执行的逻辑总结如下(前提:「默认配置」):

    1. 「SqlSesion」#SqlSessionTemplate.selectList()实际调用#DefaultSqlSession.selectList()

    2. 「Executor」#DefaultSqlSession.quer()实际调用的是#CachingExecutor().query(),如果二级缓存中存在直接返回,不存在调用#BaseExecutor.quer()查询一级缓存,如果一级缓存中存在直接返回。不存在调用#SimpleExecutor.doQuery()方法查询数据库。

    3. 「StatementHandler」#SimpleExecutor.doQuery()生成StatementHandler实例,执行#PreparedStatementHandler.parameterize()方法设置参数,实际调用的是#ParamterHandler.setParameters()方法,该方法内部调用TypeHandler.setParameter()方法进行类型转换;参数设置成功后,调用#PreparedStatementHandler.parameterize().query()方法执行SQL,返回结果

    4. 「ResultSetHandler」#DefaultResultSetHandler.handleResultSets()对返回的结果进行处理,内部调用#TypeHandler.getResult()对结果进行类型转换。全部映射完成,返回结果。

  • 以上就是六剑客在Select的执行流程,如果有错误之处欢迎指正,如果觉得陈某写得不错,有所收获,关注分享一波。

<think>好的,我需要回答用户的问题:在MyBatis中是否可以使用select标签来执行update语句。首先,我得回忆一下MyBatis的基本知识。MyBatisSQL映射文件里通常有select、insert、update、delete等标签,每个标签对应不同的操作类型。用户提到的例子中,有人在select标签里写了update语句,结果出现了问题。根据引用[1]中的情况,用户遇到了错误,因为错误地在select标签里使用了update语句。而引用[2]则说明了update标签的正确用法,比如使用<update>标签包裹update语句。 接下来,我需要确定MyBatis如何处理不同的标签。根据引用[3],SqlSession有不同的方法,比如selectOne、update等,这些方法应该与对应的标签匹配。如果使用select标签执行update语句,可能会调用错误的方法,导致问题。例如,如果使用sqlSession.selectOne()来执行一个update语句MyBatis可能不会正确处理,因为update操作通常返回影响的行数,而select期望返回查询结果。 此外,MyBatis在解析XML映射文件时,会根据标签类型进行不同的处理。比如,select标签可能被解析为查询语句,而update标签则会被视为更新操作。执行时,如果标签和SQL类型不匹配,可能会导致意外的行为,比如事务管理的问题,或者结果映射的错误。 还需要考虑数据库事务和连接的处理。update操作通常需要事务提交,而select可能自动提交或不需要。使用错误的标签可能导致事务未按预期处理,进而引发数据不一致的问题。 综合以上信息,可以得出结论:虽然在技术上可能在select标签里写update语句,但这是不正确的做法,会导致运行时错误或不可预期的行为。正确的做法是使用对应的update标签来执行update语句,并通过sqlSession的update方法来调用,这样才能确保MyBatis正确处理SQL语句和事务。</think>在MyBatis中**不能通过select标签执行update语句**,这种操作会导致语法兼容性问题和运行时异常。以下是具体分析: --- ### 一、标签与操作类型强关联 1. **标签设计规范** MyBatisSQL映射标签($<select>$、$<update>$等)与数据库操作类型严格绑定: - $<select>$:仅用于查询(SELECT语句),通过`sqlSession.selectXxx()`调用 - $<update>$:仅用于更新(UPDATE/DELETE/INSERT),通过`sqlSession.update()`调用 2. **错误示例解析** 引用[1]中的代码: ```xml <select id="updateUserIsAllowDrawStatus"> UPDATE xxxxx SET state=1 WHERE id=1 AND state=0 </select> ``` 会导致: - **返回值类型冲突**:UPDATE操作返回影响行数(int),但SELECT方法期望结果集对象 - **事务控制失效**:UPDATE操作无法通过SELECT方法触发自动提交 --- ### 二、底层执行机制差异 1. **JDBC驱动行为** | 操作类型 | 返回结果 | 是否需要ResultSet | |----------|------------------|-------------------| | SELECT | 数据集 | 是 | | UPDATE | 受影响行数 | 否 | 2. **MyBatis处理流程** ```mermaid graph TD A[调用sqlSession.selectXxx()] --> B[MyBatis解析select标签] B --> C[创建PreparedStatement] C --> D{执行executeQuery()} D --> E[尝试获取ResultSet] E --> F[UPDATE语句无结果集→报错] ``` --- ### 三、正确实践方案 1. **标准写法**(引用[2]示例): ```xml <update id="updateUser"> UPDATE user SET name=#{name} WHERE id=#{id} </update> ``` 调用方式: ```java int affectedRows = sqlSession.update("updateUser", params); ``` 2. **强制执行的后果** 即使通过特殊配置让SELECT标签执行UPDATE,也会导致: - 连接池获取错误类型连接 - 二级缓存污染 - 监控统计指标失真 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值