看过其他老师的帖子后自己整理一下具体的例子,方便复习的时候用到,哪里有问题,还请各位老师务必指点我一下,我好做出更改,防止误导大家
一、参考贴子
可以先看看这几位老师的帖子
spring结合mybatis不用手动关闭sqlSession 原理
二、先测试一级缓存(可以理解为是基于SQLSession级别的)
(1)先采用不整合springboot的方式
我去找了个mybatis的项目,用里面的mybatis的confi.xml ,我们手动创建SQLSessionFactory,利用他来获取一个session,这里我们两次查询用的是同一个session
可以看到这里第二次查询确实是没有查询数据库
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- 核心配置文件中的跟节点 -->
<configuration>
<!-- 加载properties的一种写法-->
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<!--<plugins>-->
<!--<!– mybatis的分页插件 –>-->
<!--<plugin interceptor="com.github.pagehelper.PageInterceptor">-->
<!--<!– 设置数据库类型 Oracle,Mysql,MariaDB,SQLite,Hsqldb,PostgreSQL六种数据库–>-->
<!--<property name="helperDialect" value="mysql"/>-->
<!--</plugin>-->
<!--</plugins>-->
<typeAliases>
<typeAlias type="com.zgy.ssm.po.Dept" alias="Dept"/>
</typeAliases>
<!-- 环境配置,default是唯一标识 -->
<environments default="development">
<!-- 子环境配置,与 environments里面的default对应-->
<environment id="development">
<!-- 子事务管理 与jdbc的一致-->
<transactionManager type="JDBC"/>
<!-- 数据来源,管理数据的一个节点,里面放多个数据,可以管理多个数据,-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/jiushi"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<!-- 需要映射的mapper文件,-->
<mappers>
<!-- mapper文件的文件路径,在核心配置文件中加载mapper文件-->
<mapper resource="mapper/Dept.xml"/>
</mappers>
</configuration>
public class test {
@Test
public void test01() {
String resource = "config.xml";
// 读取配置文件
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
// 构建sqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
List<Dept> findall = sqlSession.selectList("findall");
System.out.println("第一次查询结果:"+findall.get(0).getDname());
List<Dept> findall2 = sqlSession.selectList("findall");
System.out.println("第二次查询结果:"+findall2.get(0).getDname());
sqlSession.commit();
} finally {
sqlSession.close();
}
}
}
(2)不自己创建sqlsession直接使用springboot中的dao(mapper)方法查询,
可以看到两次都查询了数据库
@RunWith(SpringRunner.class)
@SpringBootTest
class TestMybatisCache {
@Autowired
private EverydayPayDao everydayPayDao;
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Test
void testMD5(){
List<EverydayPay> list1 = everydayPayDao.findList();
System.out.println("第一次查询完成");
List<EverydayPay> list2 = everydayPayDao.findList();
System.out.println("第二次查询完成");
}
}
(3)为什么springboot中要两次查询
通过查看 https://my.oschina.net/u/3574106/blog/3005054 老师的帖子,你们先看,这位老师写的很详细
我自己也跟着找一下,springboot整合mybaits,就去找 MybatisAutoConfiguration 这个自动配置类
它给我们注入了一个SqlSessionTemplate,那么我们再去找这个SqlSessionTemplate
目光转移到SqlSessionTemplate,看其构造方法,发现它使用了一个代理,而实际的增删改查方法,实际是靠代理来执行的,
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
Assert.notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
Assert.notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
this.sqlSessionProxy = (SqlSession)Proxy.newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[]{SqlSession.class}, new SqlSessionTemplate.SqlSessionInterceptor());
}
看到这个Proxy.newProxyInstanc() 方法没有,大家回忆一下我们学习java基础的时候,这个java的动态代理的实现方式,是不是有一步就是这样来实现的:(声明一个接口,声明被代理类对象实现这个接口,动态代理类要实现InvocationHandler接口,我放段代码你们看一下,就是说我用代理类代理了被代理类后,执行的方法其实是代理类中被加工后的invoke方法
public interface Subject {
void doSomething();
}
public class realSubjectImpl implements Subject {
@Override
public void doSomething() {
System.out.println("我是被代理类,我实现了接口的方法");
}
}
/**
* 这是我们的动态代理类,动态代理要实现InvocationHandler的invoke才能达到动态代理的功能
*/
@Configuration
public class MyInvocationHandler implements InvocationHandler {
private Object target;
public Object bind(Object target){
this.target=target;
return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
}
/**
* 动态代理要通过这个方法来实现
* @param proxy 被代理对象
* @param method 要实现的方法
* @param args 方法运行需要的参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result=null;
System.out.println("我是动态代理,我可以在实现被代理类方法之前做一些事情");
result=method.invoke(proxy,args);
System.out.println("我是动态代理,我可以在实现了被代理类方法之后做一些事情");
return result;
}
}
@Test
void testReflexProxy(){
Subject subject=null;
try {
/**
* 注意这里接收方法返回值用接口接收,不要用被代理类来接收,否则会报错
*/
MyInvocationHandler myInvocationHandler=new MyInvocationHandler();
subject =(Subject) myInvocationHandler.bind(new realSubjectImpl());
subject.doSomething();
} catch (Exception e) {
e.printStackTrace();
}
}
)
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator
.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
SqlSessionTemplate实际上是通过内部类SqlSessionInterceptor提供的反射功能去执行具体的操作,可以看到invoke方法里面加入了commit跟close方法,这也就是为什么在springboot中,不加事务的情况每次查询都会关闭sqlsession,并在下次查询的时候重新建立一个sqlsession的原因,
在上面原理的情况下,默认每次查询完之后自动commit,这就导致两次查询使用的不是同一个sqlSessioin,根据一级缓存的原理,它将永远不会生效。
(4)问:为什么是用SqlSessionTemplate呢?
答:这里我确实没有研究,当然我写的帖子也是片面的,我也是到处搜帖子找答案来搞清楚,大家可以看一下这个帖子 https://blog.youkuaiyun.com/u010841296/article/details/89367296
我们加几个断点试一下
还是执行springboot中的方法来看,确实是走到了
2021-04-10 09:48:14.396 DEBUG 3228 --- [ main] o.s.b.f.s.DefaultListableBeanFactory : Creating shared instance of singleton bean 'sqlSessionTemplate'
2021-04-10 09:48:14.396 DEBUG 3228 --- [ main] o.s.b.f.s.DefaultListableBeanFactory : Autowiring by type from bean name 'sqlSessionTemplate' via factory method to bean named 'sqlSessionFactory'
第一次查询结束,并关闭sqlsession
第二次也执行完毕
(5)commit方法中执行了什么
通过查看https://www.cnblogs.com/happyflyingpig/p/7739749.html 中老师写的
我点击session的commit方法往里查看,我debug看一下,
还是执行第二种(采用springboot)方法,看看能不能到这里,走到了
那就是说第一个执行完后就会清空sqlsession对象中的数据,也就是说缓存中的数据被清空了,
这里我之前有个地方理解错了,我以为数据库连接池里的对象就是sqlsession对象,实际不是,连接池是我们sqlsession跟数据库连接的桥梁,
我当时想通过设置数据库连接池的最大最小以及初始化连接数为1来创造出两次查询使用一个sqlsession来完成的场景,但是一直不是一个sqlsession(不加事务的情况下),没能成功,
这里是我理解错了,连接池是连接池,sqlsession是sqlsession
还是采用springboot的查询方式,通过查看控制台,可以看到这种情况下,查询完一个非事务的语句后就会关闭当前sqlsession,再来新的查询就会新创建sqlsession,
那如果我加上事务注解会如何呢,打断点试一下
他没有走我打的断点,而且使用了缓存,而且两次使用的是同一个sqlsession,因此在加了事务注解的情况下springboot中使用连接池一级缓存是好使的
再来试一下,通过第一种方式创建一个sqlsession来执行两次查询,第一次查询完我调用clearcache()方法来试一下,看看结果,会查询两次了
public class test {
@Test
public void test01() {
String resource = "config.xml";
// 读取配置文件
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
// 构建sqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
List<Dept> findall = sqlSession.selectList("findall");
System.out.println("第一次查询结果:"+findall.get(0).getDname());
sqlSession.clearCache();
List<Dept> findall2 = sqlSession.selectList("findall");
System.out.println("第二次查询结果:"+findall2.get(0).getDname());
sqlSession.commit();
} finally {
sqlSession.close();
}
}
}
查看上面的帖子可以知道:
SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用
(6)提问
问:比如我用sqlsession1查询了一条记录,然后我用sqlsession2更新了这条记录,然后再次用sqlsession1查询这条记录,那么会如何
①:(在非事务的情况下)用springboot
通过上面的例子我们可以知道,(在非事务的情况下)用springboot,我们的查询-更新-查询,这三个操作都会在执行完关闭sqlsession,查询前创建新的sqlsession,因此数据是实时更新的
②:(在事务的情况下)用springboot
这样的情况下,三个操作共用一个sqlsession,,当执行update操作后会清空PerpetualCache对象的数据,但是该对象可以继续使用
③手动创建sqlsession1和sqlsession2,会如何
按道理来说,这里我更新了数据,但是不是用同一个sqlsession更新的,不会影响另一个sqlsession的数据。
这里要在只用mybatis的情景下使用,可以看到,第二次查询还是使用了缓存,没有查询更新后的数据
@Test
public void test01() {
String resource = "config.xml";
// 读取配置文件
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
// 构建sqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
try {
List<Dept> findall = sqlSession.selectList("findall");
System.out.println("第一次查询结果:"+findall.get(0).getDname());
//sqlSession.clearCache();
System.out.println("******************************");
Dept dept=new Dept();
dept.setDeptno(1);
dept.setDname("2");
dept.setLoc("1");
sqlSession2.update("updateDept",dept);
sqlSession2.commit();
System.out.println("******************************");
List<Dept> findall2 = sqlSession.selectList("findall");
System.out.println("第二次查询结果:"+findall2.get(0).getDname());
sqlSession.commit();
} finally {
sqlSession2.close();
sqlSession.close();
}
}
(7)两种一级缓存模式
大家也了解一下(复制其他老师的帖子),两种一级缓存模式
一级缓存的作用域有两种:session(默认)和statment,可通过设置local-cache-scope 的值来切换,默认为session。
二者的区别在于session会将缓存作用于同一个sqlSesson,而statment仅针对一次查询,所以,local-cache-scope: statment可以理解为关闭一级缓存。
二、二级缓存(可以看成是基于namespace范围的)
默认情况下,mybatis打开了二级缓存,但它并未生效,因为二级缓存的作用域是namespace,所以还需要在Mapper.xml文件中配置一下才能使二级缓存生效
下面的测试我就全在springboot项目中测试了,
(1)准备工作
配置文件中开启二级缓存:
数据库:
dept实体类中加一个ename字段,记得要实现序列化
测试dept,我们先把dept的xml中加上cache标签,
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zgy.everydayPay.dao.DeptDao">
<cache></cache>
<select id="findList" resultType="com.zgy.everydayPay.entity.Dept">
SELECT * FROM dept
</select>
<select id="findDeptAndEmp" resultType="com.zgy.everydayPay.entity.Dept">
SELECT a.*,b.ename FROM dept a left join emp b on a.deptno=b.deptno
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zgy.everydayPay.dao.EmpDao">
<update id="updateEmp" parameterType="com.zgy.everydayPay.entity.Emp">
update emp set ename=#{ename} where empno = #{empno}
</update>
<update id="updateDept" parameterType="com.zgy.everydayPay.entity.Dept">
update dept set dname=#{dname} where deptno = #{deptno}
</update>
</mapper>
(2)先测试两个相同的dept的finllist
@Test
// @Transactional
void testL2Cache(){
List<Dept> list = deptDao.findList();
System.out.println("第一次查询完成时部门名称:"+list.get(0).getDname());
List<Dept> list2 = deptDao.findList();
System.out.println("第二次查询完成时部门名称:"+list2.get(0).getDname());
}
可以看到这里第二次没有查询数据库
(3)执行deptdao的 findDeptAndEmp()方法两次,在没有其他干扰的情况下,
结果跟上面的一样
@Test
// @Transactional
void testL2Cache1(){
List<Dept> list = deptDao.findDeptAndEmp();
System.out.println("第一次查询完成时emp名称:"+list.get(0).getEname());
List<Dept> list2 = deptDao.findDeptAndEmp();
System.out.println("第二次查询完成时emp名称:"+list2.get(0).getEname());
}
(4)执行deptdao的 findDeptAndEmp()方法两次,在中间加上empdao中的更新emp操作
可以看到,在emp的mapper里的对deptmaper有影响的操作不会影响到dept的mapper,这样会导致数据不一致,我们在emp的mapper中执行对dept的修改,也是不会使dept重新查询数据库,这样就会导致数据不同步,因为两个mapper.xml的作用域不同,要想合到一个作用域,就需要用到cache-ref
@Test
// @Transactional
void testL2Cache2(){
List<Dept> list = deptDao.findDeptAndEmp();
System.out.println("第一次查询完成时emp名称:"+list.get(0).getEname());
Emp emp=new Emp();
emp.setEmpno(1);
emp.setEname("李四");
emp.setDeptno(1);
empDao.updateEmp(emp);
List<Dept> list2 = deptDao.findDeptAndEmp();
System.out.println("第二次查询完成时emp名称:"+list2.get(0).getEname());
}
(5)使用cache-ref
在emp的mapper.xml中加入cache-ref标签,再试一下,我想把数据库还原,还是上面的那个查询,可以看到第二次重新查询了
<cache-ref namespace="com.zgy.everydayPay.dao.DeptDao"></cache-ref>
(6)我执行一个findlist,但是会带上dept这个实例当做参数,我两次传入的dept的里面的属性不一致,第二次是还是从缓存中查询吗
问:什么是statementid
mybatis提供了四个主要的statement: insert select update delete 每一个statement都有一个id,
@Test
// @Transactional
void testL2Cache3(){
Dept dept=new Dept();
dept.setLoc("地方1");
List<Dept> list = deptDao.findListByDept(dept);
System.out.println("第一次查询完成时loc名称:"+list.get(0).getLoc());
dept.setLoc("地方3");
List<Dept> list2 = deptDao.findListByDept(dept);
System.out.println("第二次查询完成时loc名称:"+list2.get(0).getLoc());
}
可以看到第二次查询了数据库
(7)二级缓存片面的理解
他把数据存到哪里去了?
通过查看老师的帖子https://blog.youkuaiyun.com/qq_39768738/article/details/107788756 中的这一块
if (list == null) {
// 这里会先查一级缓存,没有的话便会到数据库中查找,详见上面一级缓存查询逻辑
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// TransactionalCacheManager对象会创建一个TransactionalCache对象,它对传入的Cache进行再一次的包装,它持有的entriesToAddOnCommit对象用来保存临时的缓存结果
// 当SqlSession执行commit或者close时都会执行transactionCache的flushPendingEntries方法将entriesToAddOnCommit中的数据提交到mapper的cache当中
this.tcm.putObject(cache, key, list);
————————————————
版权声明:本文为优快云博主「机械熊猫侠」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/qq_39768738/article/details/107788756
这里写的很清楚,那么我找到这里,然后打上断点,要是执行到这里那就说明是这样的,那我就这么理解了。自己扒源码确实看不懂,老师要是这里不加注释我根本就不知道从哪里下手