本文已参与「新人创作礼」活动,一起开启掘金创作之路。
JDBC执行流程
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个条件:
1、sql和参数必须相同
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&characterEncoding=utf-8&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… 这篇文章字太多了,卡的一批。