Mybatis会话、执行器、一级缓存

277 阅读12分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

JDBC执行流程

blog.csdn.net/YJT180/arti… 在这里插入图片描述


Mybatis执行流程

在这里插入图片描述 详细来看就是 在这里插入图片描述

首先学习会话和执行器两部分

Mybatis核心执行组件——会话(SqlSession)

在这里插入图片描述

首先先看Executor最简单的实现——SimpleExecutor

先引入Mybatis

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>
package com.example.xuexitongtwo;

import org.apache.ibatis.executor.SimpleExecutor;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.jdbc.JdbcTransaction;
import org.junit.Before;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;

@RunWith(SpringRunner.class)
@SpringBootTest
class XuexitongtwoApplicationTests {

    private JdbcTransaction jdbcTransaction;
    private Configuration configuration;
    private Connection connection;
    @Test
    public void test1() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory build = factoryBuilder.build(is);
        configuration = build.getConfiguration();
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/xuexitong?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC","root","root");
        jdbcTransaction = new JdbcTransaction(connection);
        SimpleExecutor executor = new SimpleExecutor(configuration,jdbcTransaction);
        MappedStatement ms = configuration.getMappedStatement("com.example.xuexitongtwo.mapper.TestMapper.selectIdByUsername");
        // 之所以需要再传一次参数名,是因为没有使用executor.query
        List<Object> es = executor.doQuery(ms, "a1", RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER ,ms.getBoundSql("a1"));
        executor.doQuery(ms, "a1", RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER ,ms.getBoundSql("a1"));
        System.out.println("id:"+es.get(0));
    }
}

在这里插入图片描述

mybatis-config.xml

<?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>
<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="${spring.datasource.driver-class-name}"></property>
            <property name="url" value="${spring.datasource.url}"></property>
            <property name="username" value="${spring.datasource.username}"></property>
            <property name="password" value="${spring.datasource.password}"></property>
        </dataSource>
    </environment>
</environments>
<mappers>
    <mapper resource="mapper/testmapper.xml"/>
</mappers>
</configuration>

运行测试类后输出: 在这里插入图片描述 可以看到一共预编译了两次。因为我使用的是SimpleExecutor 在这里插入图片描述 如果我没有执行两次,而是执行两百次,那么每一次都要进行预处理,是非常耗时并且浪费性能的。

那么可以使用下面的可重用执行器

可重用执行器——ReuseExecutor

simple每次调用都会创建一个preparestatement,然后去预编译。而resue使用了一个map来存statement,每次调用,直接从map里找,找到了就复用,不用再去编译sql。 ReuseExecutor代码如下:

//    可重用执行器
    @Test
    public void test2() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory build = factoryBuilder.build(is);
        configuration = build.getConfiguration();
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/xuexitong?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC","root","root");
        jdbcTransaction = new JdbcTransaction(connection);
        ReuseExecutor executor = new ReuseExecutor(configuration,jdbcTransaction);
        MappedStatement ms = configuration.getMappedStatement("com.example.xuexitongtwo.mapper.TestMapper.selectIdByUsername");
        // 之所以需要再传一次参数名,是因为没有使用executor.query
        List<Object> es = executor.doQuery(ms, "a1", RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER ,ms.getBoundSql("a1"));
        executor.doQuery(ms, "a1", RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER ,ms.getBoundSql("a1"));
        System.out.println("id:"+es.get(0));
    }

输出如下: 在这里插入图片描述 可见,只预编译了一次。

如果要执行大量的修改操作,如果要执行一万条修改操作,一条一条的执行的话,就要执行一万次,有没有其他方法呢?怎么做?这里就引出了批处理执行器——BatchExecutor(比如说:先处理一百条,处理完后在执行一百条)

批处理执行器——BatchExecutor

    @Test
    public void test3() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory build = factoryBuilder.build(is);
        configuration = build.getConfiguration();
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/xuexitong?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC","root","root");
        jdbcTransaction = new JdbcTransaction(connection);
        BatchExecutor executor = new BatchExecutor(configuration,jdbcTransaction);
                MappedStatement ms = configuration.getMappedStatement("com.example.xuexitongtwo.mapper.TestMapper.selectIdByUsername");
        // 之所以需要再传一次参数名,是因为没有使用executor.query
        List<Object> es = executor.doQuery(ms, "a1", RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER ,ms.getBoundSql("a1"));
        executor.doQuery(ms, "a1", RowBounds.DEFAULT,
                SimpleExecutor.NO_RESULT_HANDLER ,ms.getBoundSql("a1"));
        System.out.println("id:"+es.get(0));
    }

在这里插入图片描述 可以看到,预编译了两次,明明使用了批处理执行器,那为什么出现了两次呢?这是因为BatchExecutor仅在进行改操作时才会进行批处理。

下面进行修改操作

@CacheNamespace(blocking = true)
public interface TestMapper {

    public Integer selectIdByUsername(String username);

    @Update("update user set email=#{arg1} where username=#{arg0}")
    int setEmail(String username,String email);
}
    @Test
    public void test3() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory build = factoryBuilder.build(is);
        configuration = build.getConfiguration();
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/xuexitong?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC","root","root");
        jdbcTransaction = new JdbcTransaction(connection);
        BatchExecutor executor = new BatchExecutor(configuration,jdbcTransaction);
        MappedStatement ms = configuration.getMappedStatement("com.example.xuexitongtwo.mapper.TestMapper.setEmail");
        Map param = new HashMap();
        param.put("arg0","a1");
        param.put("arg1","emailss");
        executor.doUpdate(ms,param);
        executor.doUpdate(ms,param);
    }

之后查看数据库,发现并没有修改数据。因为批处理操作需要手动刷新。 代码如下:

    @Test
    public void test3() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory build = factoryBuilder.build(is);
        configuration = build.getConfiguration();
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/xuexitong?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC","root","root");
        jdbcTransaction = new JdbcTransaction(connection);
        BatchExecutor executor = new BatchExecutor(configuration,jdbcTransaction);
        MappedStatement ms = configuration.getMappedStatement("com.example.xuexitongtwo.mapper.TestMapper.setEmail");
        Map param = new HashMap();
        param.put("arg0","a1");
        param.put("arg1","emailss");
        executor.doUpdate(ms,param);
        executor.doUpdate(ms,param);
        executor.doFlushStatements(false);//执行刷新
    }

数据库数据被修改 在这里插入图片描述 执行了一次预处理,两次数据填充 在这里插入图片描述


在这里插入图片描述 不是说Executor中会用到到缓存吗?我发现里面并没有使用到缓存。 下面看 在这里插入图片描述 因为要在SimpleExecutor和ReuseExecutor和BatchExecutor中都使用缓存,所以给他们继承了同一个父类BaseExecutor 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在BaseExecutor中实现了一级缓存,以及获取连接的方法。因为上面我都是直接调用的这三个实现类,所以没有使用到缓存。 在BaseExecutor中实现了两个方法query和update 在这里插入图片描述 在这里插入图片描述 在query中调用了抽象方法doQuery 在update中调用了抽象方法doUpdate 如下:

在这里插入图片描述 在这里插入图片描述 doQuery和doUpdate需要在SimpleExecutor和ReuseExecutor和BatchExecutor中实现。

执行query方法

    @Test
    public void test4() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory build = factoryBuilder.build(is);
        configuration = build.getConfiguration();
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/xuexitong?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC","root","root");
        jdbcTransaction = new JdbcTransaction(connection);
        Executor executor = new SimpleExecutor(configuration,jdbcTransaction);
        MappedStatement ms = configuration.getMappedStatement("com.example.xuexitongtwo.mapper.TestMapper.selectIdByUsername");
        executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);
        executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);
    }

运行结果 在这里插入图片描述 可以发现在使用的是SimpleExecutor执行器的情况下,因为使用到了一级缓存,所以只预编译了一次。并且也只进行了一次调用。 我看下ReuseExecutor,发现执行结果也是这样。因为第二次调用会走缓存的逻辑。

    @Test
    public void test4() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory build = factoryBuilder.build(is);
        configuration = build.getConfiguration();
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/xuexitong?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC","root","root");
        jdbcTransaction = new JdbcTransaction(connection);
        Executor executor = new ReuseExecutor(configuration,jdbcTransaction);
        MappedStatement ms = configuration.getMappedStatement("com.example.xuexitongtwo.mapper.TestMapper.selectIdByUsername");
        executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);
        executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);
    }

在这里插入图片描述 下面进行调试。

1、

executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);

2、

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

3、

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;//从缓存中获取到数据
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);//如果没有缓存的话,会调用相应的数据库。
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

调用数据库的方法:queryFromDatabase

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);//这个doQuery实际上就是BaseExecutor的子类所实现的方法。
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);//写入缓存
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

doQuery方法

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Configuration configuration = ms.getConfiguration();
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    Statement stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.<E>query(stmt, resultHandler);
  }

上面这个过程需要亲自debug一下。

CachingExecutor——二级缓存

在这里插入图片描述 在这里插入图片描述

上面提到的delegate属性:源码如下: 在这里插入图片描述

在这里插入图片描述 梳理一下:

一级缓存在BaseExecutor中实现
二级缓存在CachingExecutor中实现

首先,先开启二级缓存 mapper.xml

<?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.example.xuexitongtwo.mapper.TestMapper">
    <cache></cache>
    <select id="selectIdByUsername" resultType="java.lang.Integer" useCache="true">
        select id from user where username=#{username}
    </select>
</mapper>

mybatis-config.xml

<?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>
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="${spring.datasource.driver-class-name}"></property>
            <property name="url" value="${spring.datasource.url}"></property>
            <property name="username" value="${spring.datasource.username}"></property>
            <property name="password" value="${spring.datasource.password}"></property>
        </dataSource>
    </environment>
</environments>
<mappers>
    <mapper resource="mapper/testmapper.xml"/>
</mappers>
</configuration>

ok,现在已经开启了二级缓存

    @Test
    public void test5() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory build = factoryBuilder.build(is);
        configuration = build.getConfiguration();
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/xuexitong?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC","root","root");
        jdbcTransaction = new JdbcTransaction(connection);
        Executor reuseexecutor = new ReuseExecutor(configuration,jdbcTransaction);
        Executor executor = new CachingExecutor(reuseexecutor);
        MappedStatement ms = configuration.getMappedStatement("com.example.xuexitongtwo.mapper.TestMapper.selectIdByUsername");
        executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);
        executor.commit(true);
        executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);
        executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);
        executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);
        executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);

在这里插入图片描述 tips:二级缓存需要提交后才生效。而且二级缓存是多个线程共享的。

    @Test
    public void test5() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory build = factoryBuilder.build(is);
        configuration = build.getConfiguration();
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/xuexitong?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC","root","root");
        jdbcTransaction = new JdbcTransaction(connection);
        Executor reuseexecutor = new ReuseExecutor(configuration,jdbcTransaction);
        Executor executor = new CachingExecutor(reuseexecutor);
        MappedStatement ms = configuration.getMappedStatement("com.example.xuexitongtwo.mapper.TestMapper.selectIdByUsername");
        executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);
        executor.commit(true);
        new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);
                executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);
                executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);
                executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);
            }
        }).start();
    }

上面我开了一个新线程来执行查询操作。 在这里插入图片描述 可以看到,结果没有任何改变。

如果我不使用二级缓存呢?一级缓存可以实现这种跨线程操作缓存的功能吗? 输出结果如下 在这里插入图片描述 可以看到多个线程也是可以使用一个一级缓存的。那不就是说一级缓存也是可以解决跨线程的缓存功能吗?这里先挖一个坑。 我查看了一下缓存的key 在这里插入图片描述 只要上面的数据一样,就可以访问到缓存。

SqlSession会话调用过程

在这里插入图片描述 上面的红线,表示了执行的过程。

平时业务开发的时候,并不会自己调用执行器,上面使用执行器,是为了方便大家理解执行器内部的实现。

    @Test
    public void test6() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory factory = factoryBuilder.build(is);
        SqlSession sqlSession = factory.openSession(true);//开启一个SqlSession,默认自动提交
        List<Object> list = sqlSession.selectList("com.example.xuexitongtwo.mapper.TestMapper.selectIdByUsername", "a1");
        System.out.println(list.get(0));
        //
    }

使用这种方法和上面我手动构造的执行器相比其实是一样的,就好比通过服务员点单和通过厨师来点单效果是一样的。服务员(开启Sqlsession)方便些。 在这里插入图片描述 可以看到并没有什么改变。

下面再来看看SqlSession的执行过程 在这里插入图片描述 debug看一下SqlSession的结构 在这里插入图片描述 可以看到SqlSession中的executor属性指向了CachingExecutor,CachingExecutor指向了SimpleExecutor(继承了BaseExecutor)。可以证明,SqlSession的执行过程跟我上面贴的图是一样的。 继续debug 在这里插入图片描述 在这里插入图片描述 上面我用红线框住的一行代码,在自定义执行器中也使用到了。可以翻到上面查看一下。 在这里插入图片描述 这一行代码不就是我上面写的CachingExecutor.query()吗? 给你们查看一下这个executor是什么。 在这里插入图片描述 可以看到,就是CacheingExecutor。在这里Sqlsession调用了CachingExecutor,看到这是不是觉得上面学的知识联系起来了。 下面的就没必要继续debug了。

作业:将一级缓存移出来,跟二级缓存一样用装饰器模式实现。 答案:

    @Test
    public void test7() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory build = factoryBuilder.build(is);
        configuration = build.getConfiguration();
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/xuexitong?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC","root","root");
        jdbcTransaction = new JdbcTransaction(connection);
        Executor first = new FirstExecutor(configuration,jdbcTransaction);
        Executor executor = new CachingExecutor(first);
        MappedStatement ms = configuration.getMappedStatement("com.example.xuexitongtwo.mapper.TestMapper.selectIdByUsername");
        executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);
//        executor.commit(true);
        executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);
        executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);
        executor.query(ms,"a1",RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER);
    }

FirstExecutor

package com.example.xuexitongtwo.MyExecutor;

import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.cache.impl.PerpetualCache;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.executor.*;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.LocalCacheScope;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.transaction.Transaction;
import org.apache.ibatis.type.TypeHandlerRegistry;

import java.sql.SQLException;
import java.util.List;

import static org.apache.ibatis.executor.ExecutionPlaceholder.EXECUTION_PLACEHOLDER;

public class FirstExecutor implements Executor{
    protected Configuration configuration;
    protected Transaction transaction;
    protected SimpleExecutor delegate;
    protected PerpetualCache localCache;

    public FirstExecutor(Configuration configuration, Transaction transaction) {
        this.configuration = configuration;
        this.transaction = transaction;
        this.delegate = new SimpleExecutor(configuration,transaction);
        this.localCache = new PerpetualCache("LocalCache");;
    }

    public FirstExecutor(SimpleExecutor delegate) {
        this.delegate = delegate;
        this.localCache = new PerpetualCache("LocalCache");
    }

    @SuppressWarnings("unchecked")
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        System.out.println(localCache);
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list == null){
            System.out.println("没有缓存");
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }else System.out.println("有缓存");
        return list;
    }


    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        localCache.putObject(key, EXECUTION_PLACEHOLDER);
        try {
            list = delegate.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);//将这个改造成selegate调用doQuery,比如simpleExecutor
        } finally {
            localCache.removeObject(key);
        }
        System.out.println("将数据写入一级缓存");
        localCache.putObject(key, list);
        return list;
    }

    @Override
    public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        CacheKey cacheKey = new CacheKey();
        cacheKey.update(ms.getId());
        cacheKey.update(rowBounds.getOffset());
        cacheKey.update(rowBounds.getLimit());
        cacheKey.update(boundSql.getSql());
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
        // mimic DefaultParameterHandler logic
        for (ParameterMapping parameterMapping : parameterMappings) {
            if (parameterMapping.getMode() != ParameterMode.OUT) {
                Object value;
                String propertyName = parameterMapping.getProperty();
                if (boundSql.hasAdditionalParameter(propertyName)) {
                    value = boundSql.getAdditionalParameter(propertyName);
                } else if (parameterObject == null) {
                    value = null;
                } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    value = parameterObject;
                } else {
                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                    value = metaObject.getValue(propertyName);
                }
                cacheKey.update(value);
            }
        }
        if (configuration.getEnvironment() != null) {
            // issue #176
            cacheKey.update(configuration.getEnvironment().getId());
        }
        return cacheKey;
    }

    @Override
    public int update(MappedStatement ms, Object parameter) throws SQLException {
        return 0;
    }

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        return null;
    }

    @Override
    public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
        return null;
    }

    @Override
    public List<BatchResult> flushStatements() throws SQLException {
        return null;
    }

    @Override
    public void commit(boolean required) throws SQLException {

    }

    @Override
    public void rollback(boolean required) throws SQLException {

    }


    @Override
    public boolean isCached(MappedStatement ms, CacheKey key) {
        return false;
    }

    @Override
    public void clearLocalCache() {

    }

    @Override
    public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {

    }

    @Override
    public Transaction getTransaction() {
        return null;
    }

    @Override
    public void close(boolean forceRollback) {

    }

    @Override
    public boolean isClosed() {
        return false;
    }

    @Override
    public void setExecutorWrapper(Executor executor) {

    }
}

输出结果: 在这里插入图片描述


一级缓存的命中场景

在平时的业务开发中,我都是调用mapper中的方法来操作数据库的。并不是像上面那样直接操作执行器。 TestMapper

@Mapper
public interface TestMapper {

    public Integer selectIdByUsername(String username);

    @Select("select username,password,email from user where username=#{username}")
    public User selectUserByUsername(String username);

    @Update("update user set email=#{arg1} where username=#{arg0}")
    int setEmail(String username,String email);
}

我新增了selectUserByUsername方法,因为Integer存在常量池,体现不出来缓存的效果,因为Integer会缓存-128到127的数据。无论是否使用到了缓存,只要输出结果在这个范围里面,a1==a2都返回true。源码如下。 在这里插入图片描述

    @Test
    public void test8() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory factory = factoryBuilder.build(is);
        SqlSession sqlSession = factory.openSession(true);//开启一个SqlSession,默认自动提交
        TestMapper mapper = sqlSession.getMapper(TestMapper.class);
        User a1 = mapper.selectUserByUsername("a1");
        User a2 = mapper.selectUserByUsername("a1");
        System.out.println(a1==a2);
    }

输出结果: 在这里插入图片描述 可见返回了true。就说明第二次查询命中了一级缓存。

如果我调用一个除了方法名不一样,其他执行逻辑都一样的mapper中的方法,会命中缓存吗? 答案是不会,我在上面debug时已经展示过key了 在这里插入图片描述 key的组成包括了mapper中的方法名,所以说方法名(statementID)不一致,是无法命中缓存的。 在这里插入图片描述 我画线的就可以叫做StatementID。

那么如果我是用一个新的SqlSession会命中缓存吗?

    @Test
    public void test9() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory factory = factoryBuilder.build(is);
        SqlSession sqlSession = factory.openSession(true);//开启一个SqlSession,默认自动提交
        TestMapper mapper = sqlSession.getMapper(TestMapper.class);
        User a1 = mapper.selectUserByUsername("a1");
        User a2 = factory.openSession(true).getMapper(TestMapper.class).selectUserByUsername("a1");
        System.out.println(a1==a2);
    }

输出如下: 在这里插入图片描述

可见并没有命中缓存。

如果不走动态代理的话,会命中缓存吗?(我上面写的mapper.selectUserByUsername("a1");其实底层对这个方法进行了增强)

    @Test
    public void test9() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory factory = factoryBuilder.build(is);
        SqlSession sqlSession = factory.openSession(true);//开启一个SqlSession,默认自动提交
        TestMapper mapper = sqlSession.getMapper(TestMapper.class);
        User a1 = mapper.selectUserByUsername("a1");
        User a2 = sqlSession.selectOne("com.example.xuexitongtwo.mapper.TestMapper.selectUserByUsername","a1");
        List<User> a3 = sqlSession.selectList("com.example.xuexitongtwo.mapper.TestMapper.selectUserByUsername", "a1", RowBounds.DEFAULT);
        List<User> a4 = sqlSession.selectList("com.example.xuexitongtwo.mapper.TestMapper.selectUserByUsername", "a1", new RowBounds(0,10));
//        User a2 = factory.openSession(true).getMapper(TestMapper.class).selectUserByUsername("a1");
        System.out.println(a1==a2);
        System.out.println(a1==a3.get(0));
        System.out.println(a1==a4.get(0));
    }

输出结果: 在这里插入图片描述 之所以a1==a3返回true是因为,在a1执行的时候会默认给他加一个RowBounds.DEFAULT。默认取出一条数据。

所以说,要想命中一级缓存,有以下4个条件:
1sql和参数必须相同
2、必须是相同的statementID
3、sqlSession必须一样(会话级缓存)
4、RowBounds返回行范围必须相同

清空一级缓存试试

    @Test
    public void test10() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory factory = factoryBuilder.build(is);
        SqlSession sqlSession = factory.openSession(ExecutorType.REUSE,true);//开启一个SqlSession,默认自动提交,并将执行器改为ReuseExecutor
        TestMapper mapper = sqlSession.getMapper(TestMapper.class);
        User a1 = mapper.selectUserByUsername("a1");
        sqlSession.clearCache();
        User a2 = mapper.selectUserByUsername("a1");
        System.out.println(a1==a2);
    }

输出结果: 在这里插入图片描述 可见,预编译了一次,执行了两次,因为我使用了可重用执行器(ReuseExecutor)并且清空了缓存。 sqlsession的默认执行器是SimpleExecutor,我在代码中写了

SqlSession sqlSession = factory.openSession(ExecutorType.REUSE,true);

还有一种方法,可以改变全局sqlsession的默认执行器配置。 修改mybatis-config.xml

<?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>
    <settings>
        <setting name="cacheEnabled" value="true"/>
        <!--        这里-->
        <setting name="defaultExecutorType" value="REUSE"/>
    </settings>
<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="com.mysql.jdbc.Driver"></property>
            <property name="url" value="jdbc:mysql://localhost:3306/xuexitong?useUnicode=true&amp;characterEncoding=utf-8&amp;serverTimezone=UTC"></property>
            <property name="username" value="root"></property>
            <property name="password" value="root"></property>
        </dataSource>
    </environment>
</environments>
<mappers>
    <mapper resource="mapper/testmapper.xml"/>
</mappers>
</configuration>

下面我在执行一次。

    @Test
    public void test10() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory factory = factoryBuilder.build(is);
        SqlSession sqlSession = factory.openSession(true);//开启一个SqlSession,默认自动提交,并将执行器改为ReuseExecutor
        TestMapper mapper = sqlSession.getMapper(TestMapper.class);
        User a1 = mapper.selectUserByUsername("a1");
        sqlSession.clearCache();
        User a2 = mapper.selectUserByUsername("a1");
        System.out.println(a1==a2);
    }

输出如下: 在这里插入图片描述 效果一样。

我也可以在mapper中设置清空缓存

package com.example.xuexitongtwo.mapper;

import com.example.xuexitongtwo.encity.User;
import org.apache.ibatis.annotations.*;

//@CacheNamespace(blocking = true)
@Mapper
public interface TestMapper {

    public Integer selectIdByUsername(String username);

    @Select("select username,password,email from user where username=#{username}")
    @Options(flushCache = Options.FlushCachePolicy.TRUE)
    public User selectUserByUsername(String username);

    @Update("update user set email=#{arg1} where username=#{arg0}")
    int setEmail(String username,String email);
}

执行测试代码:

    @Test
    public void test10() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory factory = factoryBuilder.build(is);
        SqlSession sqlSession = factory.openSession(true);//开启一个SqlSession,默认自动提交,并将执行器改为ReuseExecutor
        TestMapper mapper = sqlSession.getMapper(TestMapper.class);
        User a1 = mapper.selectUserByUsername("a1");
//        sqlSession.clearCache();
        User a2 = mapper.selectUserByUsername("a1");
        System.out.println(a1==a2);
    }

输出如下: 在这里插入图片描述 这是因为每执行一次缓存都会被清空,调用了

User a1 = mapper.selectUserByUsername("a1");

就相当于调用

sqlSession.clearCache();

下面我将mapper中的代码去掉

@Options(flushCache = Options.FlushCachePolicy.TRUE)

然后执行测试方法

    @Test
    public void test10() throws SQLException, IOException {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory factory = factoryBuilder.build(is);
        SqlSession sqlSession = factory.openSession(true);//开启一个SqlSession,默认自动提交,并将执行器改为ReuseExecutor
        TestMapper mapper = sqlSession.getMapper(TestMapper.class);
        User a1 = mapper.selectUserByUsername("a1");
//        sqlSession.clearCache();
        mapper.setEmail("a2","saasasa");
        User a2 = mapper.selectUserByUsername("a1");
        System.out.println(a1==a2);
    }

里面的代码

mapper.setEmail("a2","saasasa");

是一个改操作,为了保证数据的一致性,也需要清空一次缓存,只要是@Update操作都会清空,哪怕我在@Update里面写了select 在这里插入图片描述

一级缓存的作用域

letianwin.blog.csdn.net/article/det… 这篇文章字太多了,卡的一批。