Mybatis缓存
缓存是一般的ORM 框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。跟Hibernate 一样,MyBatis 也有一级缓存和二级缓存 并且预留了集成第三方缓存的接口
MyBatis 跟缓存相关的类都在cache 包里面,其中有一个Cache接口,只有一个默认的实现类 PerpetualCache,它是用HashMap 实现的。 我们可以通过 以下类找到这个缓存的庐山真面目 DefaultSqlSession -> BaseExecutor -> PerpetualCache localCache ->private Map<Object, Object> cache = new HashMap();
除此之外,还有很多的装饰器,通过这些装饰器可以额外实现很多的功能:回收策略、日志记录、定时刷新等等。但是无论怎么装饰,经过多少层装饰,最后使用的还是基本的实现类(默认PerpetualCache)。
一级缓存(本地缓存)
一级缓存也叫本地缓存,一般实在会话(SqlSession)层面进行缓存的。Mybatis的一级缓存是默认开启的
DefaultSqlSession 里面只有两个属性,Configuration 是全局的,所以缓存只可能放在Executor 里面维护——SimpleExecutor/ReuseExecutor/BatchExecutor 的父类BaseExecutor 的构造函数中持有了PerpetualCache。 在同一个会话里面,多次执行相同的SQL 语句,会直接从内存取到缓存的结果,不会再发送SQL 到数据库。但是不同的会话里面,即使执行的SQL 一模一样(通过一个Mapper 的同一个方法的相同参数调用),也不能使用到一级缓存。
每当我们使用MyBatis开启一次和数据库的会话,MyBatis会创建出一个SqlSession对象表示一次数据库会话。 对数据库的一次会话中,我们有可能会反复地执行完全相同的查询语句,如果不采取一些措施的话,每一次查询都会查询一次数据库,而我们在极短的时间内做了完全相同的查询,那么它们的结果极有可能完全相同,由于查询一次数据库的代价很大,这有可能造成很大的资源浪费。 为了解决这一问题,减少资源的浪费,MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。
一级缓存的生命周期:
- MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
- 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用
- 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用
- SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用
SqlSession 一级缓存的工作流程:
- 对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果
- 判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中
- 如果命中,则直接将缓存结果返回
- 如果没命中: 4.1. 去数据库中查询数据,得到查询结果 4.2. 将key和查询到的结果分别作为key,value对存储到Cache中 4.3. 将查询结果返回
配置mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl即可在控制台看到信息
- 一级缓存在BaseExecutor 的query()——queryFromDatabase()中存入。在queryFromDatabase()之前会getObject()
- 同一个会话中,update(包括delete)会导致一级缓存被清空, 一级缓存是在BaseExecutor中的update()方法中调用clearLocalCache()清空的(无条件),query 中会判断
- 其他会话更新了数据,导致读取到脏数据(一级缓存不能跨会话共享)
使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。 如果要解决这个问题,就要用到二级缓存。MyBatis一级缓存(MyBaits 称其为 Local Cache)无法关闭,但是有两种级别可选:
- session 级别的缓存,在同一个 sqlSession 内,对同样的查询将不再查询数据库,直接从缓存中。
- statement 级别的缓存,避坑: 为了避免这个问题,可以将一级缓存的级别设为 statement 级别的,这样每次查询结束都会清掉一级缓存
二级缓存
二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace级别的,可以被多个SqlSession共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。 如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库
使用二级缓存的话,它肯定是在SqlSession的外层,否则不能被多会话共享,MyBatis 用了一个装饰器的类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis 在创建Executor 对象的时候会对Executor 进行装饰。 CachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器Executor 实现类,比如SimpleExecutor 来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户
开启二级缓存的方法
- 配置 mybatis.configuration.cache-enabled=true,只要没有显式地设置cacheEnabled=false,都会用CachingExecutor 装饰基本的执行器。
- 在Mapper.xml 中配置
标签
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024"
eviction="LRU"
flushInterval="120000"
readOnly="false"/>
flushInterval: 可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新
size: 属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024
readOnly:(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。
二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。
Mapper.xml 配置了<cache>之后,select()会被缓存。update()、delete()、insert()会刷新缓存。:如果cacheEnabled=true,Mapper.xml 没有配置标签,还有二级缓存吗?(没有)还会出现CachingExecutor 包装对象吗?(会)
只要cacheEnabled=true 基本执行器就会被装饰。有没有配置<cache>,决定了在启动的时候会不会创建这个mapper 的Cache 对象,只是最终会影响到CachingExecutorquery 方法里面的判断。如果某些查询方法对数据的实时性要求很高,不需要二级缓存,怎么办?我们可以在单个Statement ID 上显式关闭二级缓存(默认是true)
<select id="selectBlog" resultMap="BaseResultMap" useCache="false">
二级缓存注意事项:
- 事务不提交,二级缓存不存在
为什么事务不提交,二级缓存不生效?因为二级缓存使用TransactionalCacheManager(TCM)来管理,最后又调用了TransactionalCache 的getObject()、putObject 和commit()方法,TransactionalCache里面又持有了真正的Cache对象,比如是经过层层装饰的PerpetualCache。在putObject 的时候
只是添加到了entriesToAddOnCommit 里面,只有它的commit()方法被调用的时候才会调用flushPendingEntries()真正写入缓存。它就是在DefaultSqlSession调用commit()的时候被调用的
- 在其他的session 中执行增删改操作,缓存会被刷新
为什么增删改操作会清空缓存?在CachingExecutor 的update()方法里面会调用flushCacheIfRequired(ms),isFlushCacheRequired就是从标签里面渠道的flushCache的值。 而增删改操作的flushCache属性默认为true。
- 什么时候开启二级缓存? 3.1. 因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单的查询。否则缓存就失去了意义 3.2. 如果多个namespace 中有针对于同一个表的操作,比如Blog 表,如果在一个namespace 中刷新了缓存,另一个namespace 中没有刷新,就会出现读到脏数据的情况。所以,推荐在一个Mapper 里面只操作单表的情况使用
TIPS:
- 如果要让多个namespace 共享一个二级缓存,应该怎么做?跨namespace 的缓存共享的问题,可以使用
来解决:
<cache-ref namespace="com.wuzz.crud.dao.DepartmentMapper" />
- 第三方缓存做二级缓存
除了MyBatis 自带的二级缓存之外,我们也可以通过实现Cache 接口来自定义二级缓存。MyBatis 官方提供了一些第三方缓存集成方式,比如ehcache 和redis。当然,我们也可以使用独立的缓存服务,不使用MyBatis自带的二级缓存。
问题
#{}和${}的区别是什么?
- #{}是预编译处理,${}是字符串替换
- Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调用PreparedStatement 的 set 方法来赋值
- Mybatis 在处理${}时,就是把${}替换成变量的值
- 使用#{}可以有效的防止 SQL 注入,提高系统安全性
通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?
Dao 接口,就是人们常说的 Mapper 接口,接口的全限名,就是映射文件中的 namespace 的值,接口的方法名,就是映射文件中MappedStatement的id 值,接口方法内的参数,就是传递给sql的参数。 Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符 串作为 key 值,可唯一定位一个 MappedStatement。 举 例: com.mybatis3.mappers.StudentDao.findStudentById ,可以唯一找到 namespace 为 com.mybatis3.mappers.StudentDao 下面 id = findStudentById 的 MappedStatement 。在 Mybatis 中,每一个
Dao 接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略
Dao 接口的工作原理是 JDK 动态代理,Mybatis 运行时会使用JDK动态代理为 Dao 接口生成代 理 proxy 对象。 代理对象 proxy 会拦截接口方法,转而执行 MappedStatement所代表的sql,然后将sql执行结果返回
Mybatis 是如何进行分⻚的?分⻚插件的原理是什么?
Mybatis使用RowBounds对象进行分⻚,在DefaultResultSetHandler中,逻辑分页会将所有的结果都查询到,然后根据RowBounds中提供的offset和limit值来获取最后的结果。 它是针对 ResultSet 结果集执行的内存分⻚,而 非物理分⻚,可以在 sql 内直接书写带有物理分⻚的参数来完成物理分⻚功能。也可以使用分⻚ 插件来完成物理分⻚
分⻚插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分⻚语句和物理分⻚参数。
简述 Mybatis 的插件运行原理,以及如何编写一个插件?
Mybatis 仅可以编写针对ParameterHandler 、 ResultSetHandler 、 StatementHandler 、 Executor这4种接口的插件, Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法。 具体就是InvocationHandler的invoke()方法,只会拦截那些你指定需要拦截的方法
实现 Mybatis的Interceptor 接口并复写 intercept() 方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,在配置文件中配置你编写的插件
自定义实例
# 自定义分页信息
public class MyPage {
private Integer currentPage; //当前页是第几页
private Integer pageSize; //每页的大小
//省略set、get...
}
# 自定义拦截器
//表示拦截StatementHandler类的prepare方法
@Intercepts({@Signature(type = StatementHandler.class,method = "prepare",args = {Connection.class,Integer.class})})
public class MyPageInterceptor implements Interceptor{
private Integer defaultPageSize; //默认每页的大小
private Integer defaultCurrentPage; //默认页数
//实现拦截逻辑
public Object intercept(Invocation invocation) throws Throwable {
//获取目标对象
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
//获取元数据
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
//获取绑定的sql
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
//获取参数对象
Object parameterObject = boundSql.getParameterObject();
//获取传入分页参数
MyPage myPage = this.getPage(parameterObject);
String sql = (String) metaObject.getValue("delegate.boundSql.sql");
boolean isSelect = sql.trim().toLowerCase().startsWith("select");
//如果不是查询语句,则直接进入下一个操作
if(!isSelect){
return invocation.proceed();
}
System.out.println("原来的sql:"+sql);
//修改sql
String page_sql = "select * from ("+sql+") page_table limit ?,?";
//设置修改过的sql
metaObject.setValue("delegate.boundSql.sql",page_sql);
//如果没有传入相应的分页参数,则设置为默认值
if (myPage.getCurrentPage() == null){
//设置默认值
myPage.setCurrentPage(this.defaultCurrentPage);
}
if (myPage.getPageSize() == null){
//设置默认值
myPage.setPageSize(this.defaultPageSize);
}
//获取参数
PreparedStatement ps = (PreparedStatement) invocation.proceed();
// 获取sql的总参数个数
int paramCount = ps.getParameterMetaData().getParameterCount();
// 设置分页参数
ps.setInt(paramCount - 1, (myPage.getCurrentPage() - 1) * myPage.getPageSize());
ps.setInt(paramCount, myPage.getPageSize());
return ps;
}
//返回被拦截对象的代理对象
public Object plugin(Object target) {
System.out.println("开始包装目标对象");
return Plugin.wrap(target,this);
}
//设置插件配置的参数
public void setProperties(Properties properties) {
this.defaultCurrentPage = Integer.valueOf(properties.getProperty("default.currentPage", "1"));
this.defaultPageSize = Integer.valueOf(properties.getProperty("default.pageSize", "3"));
}
/**
* 获取分页参数
*
* @param parameterObject
* @return
*/
private MyPage getPage(Object parameterObject) {
if (parameterObject == null) {
return null;
}
if (parameterObject instanceof Map) {
// 如果传入的参数是map类型的,则遍历map取出Page对象
Map<String, Object> parameMap = (Map<String, Object>) parameterObject;
Set<String> keySet = parameMap.keySet();
for (String key : keySet) {
Object value = parameMap.get(key);
if (value instanceof MyPage) {
// 返回MyPage对象
return (MyPage) value;
}
}
} else if (parameterObject instanceof MyPage) {
// 如果传入的是Page类型,则直接返回该对象
return (MyPage) parameterObject;
}
// 初步判断并没有传入MyPage类型的参数,返回null
return null;
}
}
# mybatis的主配置文件mybatis-config.xml中
<plugins>
<!--配置插件-->
<plugin interceptor="com.qxf.interceptor.MyPageInterceptor">
<!-- 给插件传值,一般是默认值-->
<property name="default.currentPage" value="1"></property>
<property name="default.pageSize" value="5"></property>
</plugin>
</plugins>
Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理不?
Mybatis 动态 sql 可以让我们在 Xml 映射文件内,以标签的形式编写动态 sql,完成逻辑判断和动态拼接sql的功能,Mybatis提供了9种动态sql标签 trim、where、set、foreach、if、choose、when、otherwise、bind
其执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能
Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?
第一种是使用
有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一 赋值并返回,那些找不到映射关系的属性,是无法完成赋值的
Mybatis的Xml 映射文件中,不同的 Xml 映射文件,id是否可以重复?
不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;毕竟namespace不是必须的,只是最佳实践而已
原因就是 namespace+id 是作为 Map<String, MappedStatement> 的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。 有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同
association 及 collection
public class Teacher {
private int id;
private String name;
}
public class Student {
private int id;
private String name;
//多个学生可以是同一个老师,即多对一
private Teacher teacher;
}
association:
<mapper namespace="com.ttt.mapper.StudentMapper">
<select id="getStudents" resultMap="StudentTeacher">
select * from student
</select>
<resultMap id="StudentTeacher" type="Student">
<!--association关联属性 property属性名 javaType属性类型 column在多的一方的表中的列名-->
<association property="teacher" column="tid" javaType="Teacher" select="getTeacher"/>
</resultMap>
<!--
这里传递过来的id,只有一个属性的时候,下面可以写任何值
association中column多参数配置:
column="{key=value,key=value}"
其实就是键值对的形式,key是传给下个sql的取值名称,value是片段一中sql查询的字段名。
-->
<select id="getTeacher" resultType="teacher">
select * from teacher where id = #{id}
</select>
</mapper>
collection:
<mapper namespace="com.ttt.mapper.TeacherMapper">
<select id="getTeacher" resultMap="TeacherStudent">
select s.id sid, s.name sname , t.name tname, t.id tid
from student s,teacher t
where s.tid = t.id and t.id=#{id}
</select>
<resultMap id="TeacherStudent" type="Teacher">
<result property="name" column="tname"/>
<collection property="students" ofType="Student">
<result property="id" column="sid" />
<result property="name" column="sname" />
<result property="tid" column="tid" />
</collection>
</resultMap>
</mapper>
Mybatis延迟加载
什么是延迟加载? 举个例子:如果查询订单并且关联查询用户信息。如果先查询订单信息即可满足要求,当我们需要查询用户信息时再查询用户信息。 把对用户信息的按需去查询就是延迟加载。 所以延迟加载即先从单表查询、需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快
关联查询:SELECT orders.*, user.username FROM orders, USER WHERE orders.user_id = user.id
延迟加载相当于:
SELECT orders.*,
(SELECT username FROM USER WHERE orders.user_id = user.id)username FROM orders
所以这就比较直观了,也就是说,我把关联查询分两次来做,而不是一次性查出所有的。第一步只查询单表orders,必然会查出orders中的一个user_id字段 然后我再根据这个user_id查user表,也是单表查询。下面来总结一下如何使用这个延迟加载
- 使用association实现延迟加载
resultMap可以实现高级映射(使用association、collection实现一对一及一对多映射),其实association和collection还具备延迟加载的功能,这里我就拿association来说明,collection和association使用的方法都是一样的。 需求就是上面提到的,查询订单并且关联查询用户,查询用户使用延迟加载。 由上面的分析可知,延迟加载要查询两次,第二次是按需查询,之前一对一关联查询的时候只需要查一次,把订单和用户信息都查出来了,所以只要写一个mapper即可,但是延迟加载查两次,所以理所当然要有两个mapper。
- 两个mapper.xml
思路:
1. 只查询订单信息的statement,使用resultMap
2. 通过查询到的订单信息中的user_id去查询用户信息的statement,得到用户
3. 定义的resultMap将两者关联起来,即用订单信息user_id去查用户
实现:
1. 只查询订单信息的statement
<select id="findOrdersUserLazyLoading" resultMap="OrdersUserLazyLoadingResultMap">
SELECT * FROM orders
</select>
2. 只查询用户信息的statement
# 此mapper可以与上面不再同一文件中
# 若在不同,在下一步配置resultMap添加延迟加载代码的时候需在select中指定
<select id="findUserById" parameterType="int" resultType="user">
select * from user where id = #{id}
</select>
3. 延迟加载的 resultMap
<resultMap type="mybatis.po.Orders" id="OrderUserLazyResultMap">
<id column="id" property="id" />
<id column="user_id" property="userId" />
<id column="order_name" property="orderName" />
# 延迟加载实现方式
# select : 指定延迟加载需要执行的statement的id
# column : 订单信息中关联用户信息查询的列
<association property="user" javaType="mybatis.po.User"
select="mybatis.mapper.UserMapper.findUserById" column="user_id">
</association>
</resultMap>
4. 配置开启延迟加载
mybatis默认没有开启延迟加载,需要在SqlMapConfig.xml中setting配置
<settings>
<!-- 打开延迟加载的开关 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 将积极加载改为消极加载,即延迟加载 -->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
Mybatis批处理
Mybatis有三种基本的Executor执行器。SimpleExecutor,ReuseExecutor,BatchExecutor。
SimpleExecutor :每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象
ReuseExecutor :执行update或select,以sql作为key查找Statement对象,存在就使用, 不存在就创建,用完后,不关闭 Statement 对象。 而是放置于 Map<String, Statement>内,供下一次使用。简言之,就是重复使用Statement对象
BatchExecutor :执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch())。 等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后。 等待逐一执行executeBatch()批处理。与JDBC批处理相同
Executor的这些特点,都严格限制在SqlSession生命周期范围内
Mybatis 中如何指定使用哪一种Executor执行器?
在 Mybatis 配置文件中,可以指定默认的ExecutorType执行器类型,也可以手动给 DefaultSqlSessionFactory的创建SqlSession的方法传递ExecutorType类型参数
Mybatis映射文件中使用include标签时,被引用的定义在文件中的位置是否可以在使用的地方之后?
Mybatis解析 A 标签,发现 A 标签引用了 B 标签,但是 B 标签尚未解析到,尚不存在,此时,Mybatis会将 A 标签标记为未解析状态。 然后继续解析余下的标签,包含 B 标签,待 所有标签解析完毕,Mybatis 会重新解析那些被标记为未解析的标签,此时再解析 A 标签时,B 标签已经存在,A 标签也就可以正常解析完成了
Mybatis映射文件和内部数据结构映射关系
Mybatis 将所有 Xml 配置信息都封装到 All-In-One 重量级对象Configuration内部。在 Xml 映射文件中,