参考:
开发百宝箱:https://pdai.tech/md/framework/orm-mybatis/mybatis-overview.html
每天都要进步一点点 :MyBatis源码阅读(八) --- Executor执行器
1、MyBatis 分页原理
(1)逻辑(内存)分页——MyBatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页
mybatis接口加入RowBounds参数
public List<UserBean> queryUsersByPage(String userName, RowBounds rowBounds);
(2)物理分页——使用插件:拦截器拼接分页sql进行物理分页(mysql-limit oracle-rownum)
PageHelper 是MyBatis的一个插件,内部实现了一个PageInterceptor拦截器。Mybatis会加载这个拦截器到拦截器链中。在我们使用过程中先使用PageHelper.startPage这样的语句在当前线程上下文中设置一个ThreadLocal变量,再利用PageInterceptor这个分页拦截器拦截,从ThreadLocal中拿到分页的信息,如果有分页信息拼装分页SQL(limit语句等)进行分页查询,最后再把ThreadLocal中的东西清除掉。
详解:https://blog.csdn.net/fedorafrog/article/details/104412140
- order by 和 mysql-limit oracle-rownum 并列的话:
- mysql 是先 order by 再 limit 的(分页插件拼接就直接把 limit 并列了);
- oracle 是先 rownum 再 order by 的,所以想要先 order by 再 rownum 就要 select tmp.* (select....order by) as tmp where rownum<xxx (分页插件拼接的就是这样的)
- PageHelper.startPage() 方法把分页信息设置到 protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();
- PageInterceptor 类 在 intercept 方法的 finally 里执行 public static void clearPage() { LOCAL_PAGE.remove();} 清除当前线程的分页信息
- PageInterceptor 拦截器 实现了 ibatis 接口 org.apache.ibatis.plugin.Interceptor
- 通过 PageInterceptor 类上的 @Intercepts 注解可以看到拦截的是 org.apache.ibatis.executor.Executor 类中的 query(MappedStatement ms,Object o,RowBounds ob ResultHandler rh) 这个方法。
- org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.Reader, java.lang.String, java.util.Properties) ——> org.apache.ibatis.builder.xml.XMLConfigBuilder#parse 加载了全局配置文件及映射文件同时还将配置的拦截器添加到了拦截器链中。
- org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSession() ——> org.apache.ibatis.session.Configuration#newExecutor:Executor executor = (Executor)this.interceptorChain.pluginAll(executor); ——> org.apache.ibatis.plugin.InterceptorChain#pluginAll:target = interceptor.plugin(target) ——> org.apache.ibatis.plugin.Plugin#wrap:Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap))
循环调用了之前加载的所有拦截器,通过 JDK 代理返回了 Executor 的增强类,这个增强类,如下图是 PageInterceptor 将 Executor 的两个 query 方法增强了 - newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
- ClassLoader loader: org.apache.ibatis.executor.CachingExecutor.getClassLoader()
- Class<?>[] interfaces: interface org.apache.ibatis.executor.Executor
- InvocationHandler h: new Plugin ,而 Plugin implements InvocationHandler,Plugin 实现的 InvocationHandler 的 invoke 方法判断如果是自己 signatureMap 里的方法,就用自己的 Interceptor 类的 intercept
2、简述 MyBatis 的插件运行原理,以及如何编写一个插件
MyBatis 仅可以编写针对 ParameterHandler
、 ResultSetHandler
、 StatementHandler
、 Executor
这 4 种接口的插件,MyBatis 使用 JDK 的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体就是 InvocationHandler
的 invoke()
方法,当然,只会拦截那些你指定需要拦截的方法。
实现 MyBatis 的 Interceptor
接口并复写 intercept()
方法,然后在给插件加上 @Intercepts 注解,指定要拦截哪一个接口的哪些方法即可。
(例如分页插件 PageHelper 就是针对 Excutor 的 query 接口的插件,PageInterceptor implements Interceptor 在注解上指定拦截了 Excutor 的 query 接口,并重写了 intercept 进行分页参数拼接)
3、#{} 和 ${} 的区别是什么?
#{} | ${} | |
定义 | 进行参数占位,即 sql 预编译 | 进行 sql 拼接 |
替换时机 | 替换是在 DBMS 中(数据库管理系统) | 替换是在DBMS 外 |
工作方式 | 将sql中的#{}替换为?号,在sql执行前会使用PreparedStatement的参数设置方法,按序给sql的?号占位符设置参数值。 | |
替换效果 | 变量替换后,#{} 对应的变量自动加上单引号 ‘’ | 变量替换后,${} 对应的变量不会加上单引号 ‘’ |
sql 注入 | 很大程度防止sql注入(SQL注入是发生在编译的过程中,而如果进行预编译,其后注入的参数将不会再进行SQL编译) | 无法防止Sql注入 |
使用建议 | 一般能用#的就别用$ | 一般用于传入数据库对象,例如传入表名和列名,还有排序时使用order by动态参数时需要使用$ ,ORDER BY ${columnName} ,因为使用 #{} ,加上引号' ',导致语法不正确 |
4、为什么说 MyBatis 是半自动 ORM 映射工具?
ORM
是面向对象程序设计语言和关系型数据库发展不同步时的解决方案,采用 ORM
框架后,应用程序不再直接访问底层数据库,而是以面向对象的方式来操作持久化对象,而ORM
框架则将这些面向对象的操作转换成底层的 SQL 操作。数据中的一个表对应一个对象。
Hibernate
是自动化的,内部封装了JDBC
,连 SQL 语句都封装了,理念是即使开发人员不懂SQL语言也可以进行开发工作,向应用程序提供调用接口,直接调用即可。不用编程人员去写SQL。
Mybatis
是半自动化的,是介于 jdbc
和 Hibernate
之间的持久层框架,也是对 JDBC
进行了封装,不过将SQL的定义工作独立了出来交给实现,负责完成剩下的SQL解析,处理等工作。编程人员还是需要去写SQL
5、三种 Executor
前情:JDBC 的核心接口之一就是 Statement,它表示一个 SQL 语句。
- Statement 用于执行简单的 SQL 语句,不带参数。Statement 接口不接受参数。只能进行参数拼接,可能会被 sql 注入。
- PreparedStatement 用于执行预编译的 SQL 语句,可以带输入参数。预编译,防止 sql 注入。
- CallableStatement 用于执行数据库存储过程,可以带输入和输出参数。
Executor接口有两个实现,一个是BaseExecutor抽象类,BaseExecutor又有四个子类:
- SimpleExecutor:简单类型的执行器,也是默认的执行器,每次执行update或者select操作,都会创建一个Statement对象,执行结束后关闭Statement对象;
- ReuseExecutor:可重用的执行器,重用的是Statement对象,第一次执行一条sql,会将这条sql的Statement对象缓存在key-value结构的map缓存中。下一次执行,就可以从缓存中取出Statement对象,减少了重复编译的次数,从而提高了性能。每个SqlSession对象都有一个Executor对象,因此这个缓存是SqlSession级别的,所以当SqlSession销毁时,缓存也会销毁;
- BatchExecutor:批量执行器,默认情况是每次执行一条sql,MyBatis都会发送一条sql。而批量执行器的操作是,每次执行一条sql,不会立马发送到数据库,而是批量一次性发送多条sql;
另外一个是CachingExecutor实现类,在缓存的时候用到,使用到了装饰者模式对executor进行二次包装,动态增强了executor的功能;
6、MyBatis 一级缓存
每当我们使用MyBatis开启一次和数据库的会话,MyBatis会创建出一个SqlSession对象表示一次数据库会话。
MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来。
当创建了一个SqlSession对象时,MyBatis会为这个SqlSession对象创建一个新的Executor执行器,而缓存信息就被维护在这个Executor执行器中,MyBatis将缓存和对缓存相关的操作封装成了Cache接口中。
Executor接口的实现类BaseExecutor中拥有一个Cache接口的实现类PerpetualCache,则对于BaseExecutor对象而言,它将使用PerpetualCache对象维护缓存。PerpetualCache实现原理其实很简单,其内部就是通过一个简单的 HashMap<k,v>
来实现的
-
如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;
-
如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用;
-
SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用;
MyBatis认为,对于两次查询,如果以下条件都完全一样(也是 CacheKey),那么就认为它们是完全相同的两次查询(据此 CacheKey 查询缓存):
- statementId + rowBounds + 传递给JDBC的SQL + 传递给JDBC的参数值;
缓存会不会导致HashMap太大,而导致 java.lang.OutOfMemoryError错误啊? 读者这么考虑也不无道理,不过MyBatis这样设计也有它自己的理由:
-
一般而言SqlSession的生存时间很短。一般情况下使用一个SqlSession对象执行的操作不会太多,执行完就会消亡;
-
对于某一个SqlSession对象而言,只要执行update操作(update、insert、delete),都会将这个SqlSession对象中对应的一级缓存清空掉,所以一般情况下不会出现缓存过大,影响JVM内存空间的问题;
-
可以手动地释放掉SqlSession对象中的缓存。
7、MyBatis 二级缓存
二级缓存是 Application 级别的 / Mapper 级别的,可以配置一个 Mapper 用一个缓存,也可以指定多个 Mapper 用一个缓存
虽然在Mapper中配置了<cache>
,并且为此Mapper分配了Cache对象,这并不表示我们使用Mapper中定义的查询语句查到的结果都会放置到Cache对象之中,我们必须指定Mapper中的某条选择语句是否支持缓存,即如下所示,在<select>
节点中配置useCache="true",Mapper才会对此Select的查询支持缓存特性,否则,不会对此Select查询,不会经过Cache缓存。如下所示,Select语句配置了useCache="true",则表明这条Select语句的查询会使用二级缓存。
<select id="selectByMinSalary" resultMap="BaseResultMap" parameterType="java.util.Map" useCache="true">
要想使某条Select查询支持二级缓存,你需要保证:
- MyBatis支持二级缓存的总开关:全局配置变量参数 cacheEnabled=true
- 该select语句所在的Mapper,配置了
<cache>
或<cached-ref>
节点,并且有效 - 该select语句的参数 useCache=true
如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 ———> 一级缓存 ——> 数据库。
MyBatis在为SqlSession对象创建Executor对象时,会对Executor对象加上一个装饰者:CachingExecutor,这时SqlSession使用CachingExecutor对象来完成操作请求。CachingExecutor对于查询请求,会先判断该查询请求在Application级别的二级缓存中是否有缓存结果。
使用MyBatis的二级缓存有三个选择:
- MyBatis自身提供的缓存实现(比较灵活,提供了各种缓存刷新策略如LRU,FIFO等等);
- 用户自定义的Cache接口实现;
- 跟第三方内存缓存库的集成;