Mybatis 是如何实现预编译的?
- 一、前言
- 二、源码分析
- 三、总结
一、前言
在介绍 Mybatis 是如何实现预编译之前,需提前知道俩个预备知识:
- MySQL的运行流程(对应的 SQL 会成为一个文本-》查询缓存(8.0后没了)-》解析器(解析SQL,对SQL进行预处理,也就是判断语法等操作)-》查询优化(比如底层的索引优化,如所用联合索引的顺序调换优化查询等等)-》执行SQL,从存储引擎中得到数据后返回)。
- Mybatis 是对 JDBC 的封装(emmmm,这个大家都知道🤣)
SQL 预编译概述:数据库收到 SQL 语句之后,需要词法和语义解析,以优化 SQL 语句,制定执行计划。这需要花费一些时间,但是很多情况下,我们的同一条 sql 语句可能会反复的执行,或者每次执行的时候只有个别的值不同(比如:select 的 where 子句值不同,update 的 set 子句值不同,insert 的 values 值不同)。如果每次都需要经过词法语义解析、语句优化、制定执行计划等等,则效率明显不行。为了解决这个问题,于是有了预编译,预编译语句就是将这类语句中的值用占位符替代,可以视为将 sql 语句模块化或者说参数化。一次编译、多次运行,省去了解析优化等过程。预编译语句被 DB 的编译器编译后的执行代码被缓存下来,那么下次调用时只要是相同的预编译语句就不需要重复编译了,只要将参数直接传入编译过的语句执行代码中(相当于一个函数)就会得到执行。 并不是所有预编译语句都一定会被缓存,数据库本身会用一种策略(内部机制)。在 JDBC 中,预编译是通过 PreparedStatement 和 占位符 来实现的。PreparedStatement 也是 Mybatis 默认的语句执行器。
预编译的作用:
- 减少编译次数,提升性能:预编译之后的 sql 多数情况下可以直接执行,DBMS(数据库管理系统)不需要再次编译。越复杂的 sql,往往编译的复杂度就越大;
- 防止 SQL 注入:使用预编译,后面注入的参数将不会再次触发 SQL 编译。也就是说,对于后面注入的参数,系统将不会认为它会是一个 SQL 命令,而默认是一个参数,参数中的 or 或 and 等(SQL 注入常用伎俩,说来说去就这些)就不是 SQL 语法保留字了。
二、源码分析
在直观的分析源码之前,还得提一下我们从 SQLSource.getBoundSql 中拿到的 BoundSQL 对象中的 sql 属性,这得回到前面小编所写的动态解析SQL的博客里 【Mybatis源码分析】动态标签的底层原理,DynamicSqlSource源码分析 ,里面阐述了不管是RawSqlSource还是DynamicSqlSource,都会通过 SqlSourceBuilder
去解析 sql 文本的 #{}
,将其替换成 ?
占位符。
首先我们还得知道 Mybatis 是通过执行器去执行对应的 SQL 的(关于执行器我再写篇博客详细解释),Mybatis 为我们提供了三种执行器:SimpleExecutor(简单执行器、也是默认执行器)、ReuseExecutor(可复用执行器,就是可复用 Statement 对象,减少系统开销用的,其他和简单执行器差不多)、BatchExecutor(批处理执行器)。
这边的话以 SimpleExecutor 执行器的源码进行分析,因为这篇是预编译的文章,所以题外话还是少说吧(虽然已经说很多了)。
SimpleExecutor 类中重写了俩核心方法:doUpdate和doQuery。
这俩核心方法都有相同的步骤就是(先是获取一个 StatementHandler 对象,再去调用 prepareStatement 方法):
获取 StatementHandler 对象是通调用 Configuration.newStatementHandler 方法进行获取的,通过下面源码可以得知,得到的是一个 RoutingStatementHandler。
接下来看看 RoutingStatementHandler 构造方法源码实现吧(可以看见内部封装了一个 StatementHandler 的委托者,从构造方法实现也可以看出这个 RoutingStatementHandler 就是起一个路由的作用,盲猜相关的执行靠的是内部封装的这个委托者,由于我们又知道默认的 StatementType 是 PREPARED,所以这里的内部委托执行器是 PreparedStatementHandler 对象):
private final StatementHandler delegate;public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,ResultHandler resultHandler, BoundSql boundSql) {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());}
那么再真正执行 sql 前的操作是什么就要看 prepareStatement 方法了。
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {Statement stmt;Connection connection = getConnection(statementLog);// 获取connection对象stmt = handler.prepare(connection, transaction.getTimeout());// 进行预处理等操作handler.parameterize(stmt);// 预处理完后设置参数return stmt;}
那么接下来分析的就是 prepare 方法了,我们知道传过来的 StatementHandler 是一个 RoutingStatementHandler,但其内部是路由一个委托者去实现的即 PreparedStatementHandler。
通过上面 prepare 方法的实现可以看出,实现方法在 BaseStatementHandler 中,
为了让你不烦迷糊,还是给个继承图吧:
下面给出 BaseStatementHandler 实现的 prepare 方法的源码:
根据方法名可以判断出,主要步骤是在 instantiateStatement 方法中,为什么?因为它返回了一个 Statement 对象,我们知道 Mybatis 是对 JDBC 的封装,如果你熟知JDBC的6步走,那应该深知只有在获取数据库操纵对象的时候才会返回 Statement。
OK,我们找到 PreparedStatementHandler.instantiateStatement (实例化操纵对象方法),看看其源码实现。
分析到这就知道 Mybatis 是如何实现预编译的了吧,本质还是 JDBC 的 prepareStatement 去进行的预编译。同时通过源码分析我们也知道为什么 insert 语句中配置了 useGeneratedKeys="true"
会返回主键了。
三、总结
Mybatis 实现预编译主要是在执行 sql 前,会调用一个 prepareStatement 方法进行预处理,会传一个 StatementHandler 对象进去,本质是一个 RoutingStatementHandler,但其构造方法其实就是一个路由的作用,内部封装了一个委托者才是真正的执行者。
其委托者实现了 instantiateStatement (实例化 Statement) 方法。该方法就是 Mybatis 实现预编译的关键。prepareStatement方法会调用 BaseStatementHandler 中的 prepare 方法,然后会通过 instantiateStatement 方法返回一个 Statement 对象,即调用的是默认的 PreparedStatementHandler 中的方法,其本质呢就是 JDBC 中获取数据库操纵对象时进行的预编译处理一致,只是 Mybatis 对其进行了封装,为进行其他拓展…