1.MyBatis工作流程简述

传统工作模式

  1. public static void main(String[] args) {
  2. InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
  3. SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
  4. SqlSession sqlSession = factory.openSession();
  5. String name = "tom";
  6. List<User> list = sqlSession.selectList("com.demo.mapper.UserMapper.getUserByName",params);
  7. }

1.创建SqlSessionFactoryBuilder,根据mybatis-config.xml配置文件;来build SqlSessionFactory
2.SqlSessionFactory.openSession()来创建SqlSeeion
3.SqlSession执行具体语句
SqlSessionFactoryBuild -> SqlSessionFactory -> SqlSession

使用mapper接口

  1. public static void main(String[] args) {
  2. //前三步都相同
  3. InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
  4. SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
  5. SqlSession sqlSession = factory.openSession();
  6. //这里不再调用SqlSession 的api,而是获得了接口对象,调用接口中的方法。
  7. UserMapper mapper = sqlSession.getMapper(UserMapper.class);
  8. List<User> list = mapper.getUserByName("tom");
  9. }

JDBC完整实例
image.png
由上可以看到,最终执行语句的是Statement这个对象,其实在Mybatis最终也是这个对象真正执行语句。
下面来看看Mybatis的流程及原理
1.获取SqlSession
image.png
1.首先,SqlSessionFactoryBuillder去读取Mybatis-config.xml配置文件,然后build一个DefaultSessionFactory

  1. public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
  2. try {
  3. //通过XMLConfigBuilder解析配置文件,解析的配置相关信息都会封装为一个Configuration对象
  4. XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
  5. //这儿创建DefaultSessionFactory对象
  6. return build(parser.parse());
  7. } catch (Exception e) {
  8. throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  9. } finally {
  10. ErrorContext.instance().reset();
  11. try {
  12. reader.close();
  13. } catch (IOException e) {
  14. // Intentionally ignore. Prefer previous error.
  15. }
  16. }
  17. }
  18. public SqlSessionFactory build(Configuration config) {
  19. return new DefaultSqlSessionFactory(config);
  20. }

Configuration对象的结构和xml配置文件的对象几乎相同。
image.png
也就是说,初始化配置文件信息的本质就是创建Configuration,将解析到的xml数据封装到Configuration内部的属性中。
发散一下,既然解析xml只是将xml的数据封装到Configuration中,我们可以创建一个Configguration对象,然后手动设置一些我们需要的属性参数。

2.拿到DefaultSqSessionFactory后,可以通过SqlSessionFactory去获取SqlSession。

  1. /**
  2. * 通常一系列openSession方法最终都会调用本方法
  3. * @param execType
  4. * @param level
  5. * @param autoCommit
  6. * @return
  7. */
  8. private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  9. Transaction tx = null;
  10. try {
  11. //通过Confuguration对象去获取Mybatis相关配置信息, Environment对象包含了数据源和事务的配置
  12. final Environment environment = configuration.getEnvironment();
  13. final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
  14. tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
  15. //之前说了,从表面上来看,咱们是用sqlSession在执行sql语句, 实际呢,其实是通过excutor执行, excutor是对于Statement的封装
  16. final Executor executor = configuration.newExecutor(tx, execType);
  17. //关键看这儿,创建了一个DefaultSqlSession对象
  18. return new DefaultSqlSession(configuration, executor, autoCommit);
  19. } catch (Exception e) {
  20. closeTransaction(tx); // may have fetched a connection so lets call close()
  21. throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
  22. } finally {
  23. ErrorContext.instance().reset();
  24. }
  25. }

SqlSession是一个接口,他有两个实现类:DefaultSqlSession和SqlSessionManager(弃用)
SqlSession是Mybatis中用于和数据库交互的顶层类,通常与ThreadLocal绑定,一个会话使用一个Sqlsession,并且在使用完毕后close。

Mybatis - 图4
SqlSession中的两个最重要的参数,configuration与初始化时的相同,Executor为执行器,

Executor执行器
由上可以看到,在build Sqlsession的时候,通过configuration来获取Environment,然后获取到事务Transaction(Transaction的作用可见另一个文档,其实现类主要用于更具DataSource获取Connection)。
Executor也是一个接口,他有三个常用的实现类BatchExecutor(重用语句并执行批量更新),ReuseExecutor(重用预处理语句prepared statements),SimpleExecutor(普通的执行器,默认)。

MapperProxy
在mybatis中,我们看到只有mapper接口,并没有实现类,这是因为mybatis会为每一个mapper接口生成一个mapperProxy动态代理类。
image.png
开始之前介绍一下Mybatis初始化时对接口的处理,MapperRegistry是Configuration的一个属性,它内部维护了一个Hsahmap用于存放mapper接口的工厂类,每个接口对应一个工厂类,mappers中可以配置接口的包路劲。
image.png

  • 当解析mappers标签时,它会判断解析到的是mapper配置文件时,会再将对应配置文件中的增删改查标签一一封装成MapperedStatement对象,存入mappedStatements。
  • 当判断解析到接口时,会创建此接口对应的MapperProxyFactory对象,存入HashMap中,key = 接口的字节码对象,value = 此接口对应的MapperProxyFactory对象。

这是上面解析xml文件的时候会做到

  1. //MapperRegistry类
  2. public class MapperRegistry {
  3. private final Configuration config;
  4. //这个类中维护一个HashMap存放MapperProxyFactory
  5. private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
  6. //解析到接口时添加接口工厂类的方法
  7. public <T> void addMapper(Class<T> type) {
  8. if (type.isInterface()) {
  9. if (hasMapper(type)) {
  10. throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
  11. }
  12. boolean loadCompleted = false;
  13. try {
  14. //重点在这行,以接口类的class对象为key,value为其对应的工厂对象,构造方法中指定了接口对象
  15. knownMappers.put(type, new MapperProxyFactory<>(type));
  16. // It's important that the type is added before the parser is run
  17. // otherwise the binding may automatically be attempted by the
  18. // mapper parser. If the type is already known, it won't try.
  19. MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
  20. parser.parse();
  21. loadCompleted = true;
  22. } finally {
  23. if (!loadCompleted) {
  24. knownMappers.remove(type);
  25. }
  26. }
  27. }
  28. }
  29. }
  30. ————————————————
  31. 版权声明:本文为CSDN博主「Coder648」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
  32. 原文链接:https://blog.csdn.net/weixin_43184769/article/details/91126687

然后就是获取MapperProxy了

  1. //DefaultSqlSession中的getMapper
  2. public <T> T getMapper(Class<T> type) {
  3. return configuration.<T>getMapper(type, this);
  4. }
  5. //configuration中的给getMapper
  6. public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  7. return mapperRegistry.getMapper(type, sqlSession);
  8. }
  9. //MapperRegistry中的getMapper
  10. public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  11. //从MapperRegistry中的HashMap中拿MapperProxyFactory
  12. final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  13. if (mapperProxyFactory == null) {
  14. throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
  15. }
  16. try {
  17. // 通过动态代理工厂生成示例。
  18. return mapperProxyFactory.newInstance(sqlSession);
  19. } catch (Exception e) {
  20. throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  21. }
  22. }
  23. //MapperProxyFactory类中的newInstance方法
  24. public T newInstance(SqlSession sqlSession) {
  25. // 创建了JDK动态代理的Handler类
  26. final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
  27. // 调用了重载方法
  28. return newInstance(mapperProxy);
  29. }
  30. //MapperProxy类,实现了InvocationHandler接口
  31. public class MapperProxy<T> implements InvocationHandler, Serializable {
  32. //省略部分源码
  33. private final SqlSession sqlSession;
  34. private final Class<T> mapperInterface;
  35. private final Map<Method, MapperMethod> methodCache;
  36. // 构造,传入了SqlSession,说明每个session中的代理对象的不同的!
  37. public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
  38. this.sqlSession = sqlSession;
  39. this.mapperInterface = mapperInterface;
  40. this.methodCache = methodCache;
  41. }
  42. //省略部分源码
  43. }
  44. //重载的方法,由动态代理创建新示例返回。
  45. protected T newInstance(MapperProxy<T> mapperProxy) {
  46. return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  47. }

在动态代理返回了实例后,我们就可以直接调用mapper类中的方法了,说明在MapperProxy中的invoke方法中,已经为我们实现了方法。

  1. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  2. try {
  3. //判断调用是是不是Object中定义的方法,toString,hashCode这类非。是的话直接放行。
  4. if (Object.class.equals(method.getDeclaringClass())) {
  5. return method.invoke(this, args);
  6. } else if (isDefaultMethod(method)) {
  7. return invokeDefaultMethod(proxy, method, args);
  8. }
  9. } catch (Throwable t) {
  10. throw ExceptionUtil.unwrapThrowable(t);
  11. }
  12. final MapperMethod mapperMethod = cachedMapperMethod(method);
  13. // 重点在这:MapperMethod最终调用了执行的方法
  14. return mapperMethod.execute(sqlSession, args);
  15. }
  16. public Object execute(SqlSession sqlSession, Object[] args) {
  17. Object result;
  18. //判断mapper中的方法类型,最终调用的还是SqlSession中的方法
  19. switch (command.getType()) {
  20. case INSERT: {
  21. Object param = method.convertArgsToSqlCommandParam(args);
  22. result = rowCountResult(sqlSession.insert(command.getName(), param));
  23. break;
  24. }
  25. case UPDATE: {
  26. Object param = method.convertArgsToSqlCommandParam(args);
  27. result = rowCountResult(sqlSession.update(command.getName(), param));
  28. break;
  29. }
  30. case DELETE: {
  31. Object param = method.convertArgsToSqlCommandParam(args);
  32. result = rowCountResult(sqlSession.delete(command.getName(), param));
  33. break;
  34. }
  35. case SELECT:
  36. if (method.returnsVoid() && method.hasResultHandler()) {
  37. executeWithResultHandler(sqlSession, args);
  38. result = null;
  39. } else if (method.returnsMany()) {
  40. result = executeForMany(sqlSession, args);
  41. } else if (method.returnsMap()) {
  42. result = executeForMap(sqlSession, args);
  43. } else if (method.returnsCursor()) {
  44. result = executeForCursor(sqlSession, args);
  45. } else {
  46. Object param = method.convertArgsToSqlCommandParam(args);
  47. result = sqlSession.selectOne(command.getName(), param);
  48. if (method.returnsOptional() &&
  49. (result == null || !method.getReturnType().equals(result.getClass()))) {
  50. result = Optional.ofNullable(result);
  51. }
  52. }
  53. break;
  54. case FLUSH:
  55. result = sqlSession.flushStatements();
  56. break;
  57. default:
  58. throw new BindingException("Unknown execution method for: " + command.getName());
  59. }
  60. if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
  61. throw new BindingException("Mapper method '" + command.getName()
  62. + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  63. }
  64. return result;
  65. }

在这里我们看到,兜兜转转还是回到了SqlSession这里。
我们来选取SqlSession中一个方法来分析

  1. public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  2. try {
  3. MappedStatement ms = configuration.getMappedStatement(statement);
  4. //CRUD实际上是交给Excetor去处理, excutor其实也只是穿了个马甲而已,小样,别以为穿个马甲我就不认识你嘞!
  5. return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  6. } catch (Exception e) {
  7. throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
  8. } finally {
  9. ErrorContext.instance().reset();
  10. }
  11. }

所以,Sql内部还是交给了Excetor去处理。
image.png
但其实Excetor也只是穿了一个马甲而已,最终一层层的,最终会来到doQuery方法。

  1. //此方法在SimpleExecutor的父类BaseExecutor中实现
  2. public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  3. //根据传入的参数动态获得SQL语句,最后返回用BoundSql对象表示
  4. BoundSql boundSql = ms.getBoundSql(parameter);
  5. //为本次查询创建缓存的Key
  6. CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  7. return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  8. }
  9. //进入query的重载方法中
  10. public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  11. ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  12. if (closed) {
  13. throw new ExecutorException("Executor was closed.");
  14. }
  15. if (queryStack == 0 && ms.isFlushCacheRequired()) {
  16. clearLocalCache();
  17. }
  18. List<E> list;
  19. try {
  20. queryStack++;
  21. list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
  22. if (list != null) {
  23. handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
  24. } else {
  25. // 如果缓存中没有本次查找的值,那么从数据库中查询
  26. list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
  27. }
  28. } finally {
  29. queryStack--;
  30. }
  31. if (queryStack == 0) {
  32. for (DeferredLoad deferredLoad : deferredLoads) {
  33. deferredLoad.load();
  34. }
  35. // issue #601
  36. deferredLoads.clear();
  37. if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
  38. // issue #482
  39. clearLocalCache();
  40. }
  41. }
  42. return list;
  43. }
  44. //从数据库查询
  45. private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  46. List<E> list;
  47. localCache.putObject(key, EXECUTION_PLACEHOLDER);
  48. try {
  49. // 查询的方法
  50. list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  51. } finally {
  52. localCache.removeObject(key);
  53. }
  54. // 将查询结果放入缓存
  55. localCache.putObject(key, list);
  56. if (ms.getStatementType() == StatementType.CALLABLE) {
  57. localOutputParameterCache.putObject(key, parameter);
  58. }
  59. return list;
  60. }
  61. // SimpleExecutor中实现父类的doQuery抽象方法
  62. public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  63. Statement stmt = null;
  64. try {
  65. Configuration configuration = ms.getConfiguration();
  66. // 传入参数创建StatementHanlder对象来执行查询
  67. StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
  68. // 创建jdbc中的statement对象
  69. stmt = prepareStatement(handler, ms.getStatementLog());
  70. // StatementHandler进行处理
  71. return handler.query(stmt, resultHandler);
  72. } finally {
  73. closeStatement(stmt);
  74. }
  75. }
  76. // 创建Statement的方法
  77. private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  78. Statement stmt;
  79. //条代码中的getConnection方法经过重重调用最后会调用openConnection方法,从连接池中获得连接。
  80. Connection connection = getConnection(statementLog);
  81. stmt = handler.prepare(connection, transaction.getTimeout());
  82. handler.parameterize(stmt);
  83. return stmt;
  84. }
  85. //从连接池获得连接的方法
  86. protected void openConnection() throws SQLException {
  87. if (log.isDebugEnabled()) {
  88. log.debug("Opening JDBC Connection");
  89. }
  90. //从连接池获得连接
  91. connection = dataSource.getConnection();
  92. if (level != null) {
  93. connection.setTransactionIsolation(level.getLevel());
  94. }
  95. setDesiredAutoCommit(autoCommit);
  96. }
  97. //进入StatementHandler进行处理的query,StatementHandler中默认的是PreparedStatementHandler
  98. public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  99. PreparedStatement ps = (PreparedStatement) statement;
  100. //原生jdbc的执行
  101. ps.execute();
  102. //处理结果返回。
  103. return resultSetHandler.handleResultSets(ps);
  104. }

首先来看看MappedStatement

  • 作用:MappedStatment与Mapper配置文件中一个select/update/insert/delete节点相对应。mapper中配置的标签都被封装到了此对象中,主要用途是描绘一条SQL语句。
  • 初始化过程:回顾到刚开始介绍的加载配置文件的过程中,会对mybatis-config.xml中的各个标签都进行解析。其中由mappers标签用来引用mapper.xml或者配置mapper接口的配置。 ```java
  1. 这样的一个select标签会在初始化配置文件时被解析封装成一个MappedStatement对象,然后存储在Configuration对象的mappedStatements属性中,mappedStatements 是一个HashMap,存储时key = 全限定类名 + 方法名,value = 对应的MappedStatement对象。
  2. ```java
  3. Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")

image.png
再看上面的
stmt = prepareStatement(handler, ms.getStatementLog());

  1. private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  2. Statement stmt;
  3. Connection connection = getConnection(statementLog);
  4. //执行语句之前的准备
  5. stmt = handler.prepare(connection, transaction.getTimeout());
  6. handler.parameterize(stmt);
  7. return stmt;
  8. }

StatementHandler是最终的执行语句,看一下是实现类PreparedStatementHandler(最常用,封装的是PreparedStatement)

  1. public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  2. //到此,原形毕露, PreparedStatement(预编译的SQl语句),PreparedStatement可以有效的防止Sql注入
  3. PreparedStatement ps = (PreparedStatement) statement;
  4. //Sql语句执行
  5. ps.execute();
  6. //结果交给了ResultSetHandler 去处理,对结果做处理
  7. return resultSetHandler.<E> handleResultSets(ps);
  8. }
  1. @Override
  2. public List<Object> handleResultSets(Statement stmt) throws SQLException {
  3. //此时stmt里面已经有查询出来的结果了
  4. ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
  5. final List<Object> multipleResults = new ArrayList<>();
  6. int resultSetCount = 0;
  7. ResultSetWrapper rsw = getFirstResultSet(stmt);
  8. List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  9. int resultMapCount = resultMaps.size();
  10. validateResultMapsCount(rsw, resultMapCount);
  11. while (rsw != null && resultMapCount > resultSetCount) {
  12. ResultMap resultMap = resultMaps.get(resultSetCount);
  13. handleResultSet(rsw, resultMap, multipleResults, null);
  14. rsw = getNextResultSet(stmt);
  15. cleanUpAfterHandlingResultSet();
  16. resultSetCount++;
  17. }
  18. String[] resultSets = mappedStatement.getResultSets();
  19. if (resultSets != null) {
  20. while (rsw != null && resultSetCount < resultSets.length) {
  21. ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
  22. if (parentMapping != null) {
  23. String nestedResultMapId = parentMapping.getNestedResultMapId();
  24. ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
  25. handleResultSet(rsw, resultMap, null, parentMapping);
  26. }
  27. rsw = getNextResultSet(stmt);
  28. cleanUpAfterHandlingResultSet();
  29. resultSetCount++;
  30. }
  31. }
  32. return collapseSingleResultList(multipleResults);
  33. }

2.Mybatis拦截器

开发当中我们经常需要对执行的sql加上自定义的处理,比如分页(日志打印可通过配置开启,默认不开启),这个时候我们可以自定义MyBatis拦截器,只需要实现Interceptor接口

  1. @Intercepts({@Signature(type = Executor.class,method = "query",args = {MappedStatement.class,Object.class, RowBounds.class,ResultHandler.class})})
  2. public class MyPageInterceptor implements Interceptor {
  3. private static final Logger logger= LoggerFactory.getLogger(MyPageInterceptor.class);
  4. @Override
  5. public Object intercept(Invocation invocation) throws Throwable {
  6. logger.warn(invocation.toString());
  7. return invocation.proceed();
  8. }
  9. @Override
  10. public Object plugin(Object o) {
  11. return Plugin.wrap(o,this);
  12. }
  13. @Override
  14. public void setProperties(Properties properties) {
  15. logger.warn(properties.toString());
  16. }
  17. }

3.mybatis批量执行sql语句

  1. @Service
  2. public class ProposedService{
  3. @Autowired
  4. SqlSessionFactory sqlSessionFactory;
  5. @Transactional
  6. public void insertBatch(List<TRiskToleranceAnswer> tRiskToleranceAnswerList) {
  7. SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
  8. TRiskToleranceAnswerMapper mapper = sqlSession.getMapper(TRiskToleranceAnswerMapper.class);
  9. for (int i = 0; i < tRiskToleranceAnswerList.size(); i++) {
  10. mapper.insertSelective(tRiskToleranceAnswerList.get(i));
  11. }
  12. sqlSession.flushStatements();
  13. }
  14. }

4.SqlSession线程安全问题

在Mybatis中SqlSession默认有DefaultSqlSession和SqlSessionManager两个实现类。

DefaultSqlSession是真正的实现类调用Executor,但是这里是线程不安全的
这里的线程是指我们在代码中直接openSqlSession,然后用这个SqlSession多次调用,如果直接使用mybatis中的mapper,他会每次执行sql的时候都去创建一个新Sqlsession,这样其实不会造成不安全问题。
DefaultSqlSession线程不安全原理
DefaultSqlSession实际上真正执行的是BaseExecutor,BaseExecutor里面有一个cache,它是一个map,负责存储sql的查询结果的一个缓存。如果多线程的情况下,会有线程不安全的问题。

SqlSessionManager
Mybatis又实现了对SqlSession和SQLSessionFactory的封装类SqlSessionManager,线程安全并通过localSqlSession实现复用从而提高性能。
SqlSessionManager维护了一个ThreadLoacl,将sqlSession与线程绑定,当执行sql时,调用代理类中的拦截器,拦截器中会获取sqlsession
image.png

  1. private class SqlSessionInterceptor implements InvocationHandler {
  2. public SqlSessionInterceptor() {
  3. }
  4. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  5. //由调用者决定当前线程是否复用 SqlSession
  6. SqlSession sqlSession = (SqlSession)SqlSessionManager.this.localSqlSession.get();
  7. if (sqlSession != null) {
  8. try {
  9. return method.invoke(sqlSession, args);
  10. } catch (Throwable var12) {
  11. throw ExceptionUtil.unwrapThrowable(var12);
  12. }
  13. } else {
  14. //如果不复用,则每次调用都新建 SqlSession 并使用后销毁
  15. SqlSession autoSqlSession = SqlSessionManager.this.openSession();
  16. Object var7;
  17. try {
  18. Object result = method.invoke(autoSqlSession, args);
  19. autoSqlSession.commit();
  20. var7 = result;
  21. } catch (Throwable var13) {
  22. autoSqlSession.rollback();
  23. throw ExceptionUtil.unwrapThrowable(var13);
  24. } finally {
  25. autoSqlSession.close();
  26. }
  27. return var7;
  28. }
  29. }
  30. }

DefaultSqlSession和SqlSessionManager之间的区别:
1、单例模式下DefaultSqlSession是线程不安全的,而SqlSessionManager是线程安全的;
2、SqlSessionManager可以选择通过localSqlSession这个ThreadLocal变量,记录与当前线程绑定的SqlSession对象,供当前线程循环使用,从而避免在同一个线程多次创建SqlSession对象造成的性能损耗;
3、使用DefaultSqlSession为了保证线程安全需要为每一个操作都创建一个SqlSession对象,其性能可想而知;

SqlSessionTemplate
SqlSessionTemplate是MyBatis专门为Spring提供的,支持Spring框架的一个SqlSession获取接口。主要是为了继承Spring,并同时将是否共用SqlSession的权限交给Spring去管理。
同样是创建代理类

  1. public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
  2. Assert.notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
  3. Assert.notNull(executorType, "Property 'executorType' is required");
  4. this.sqlSessionFactory = sqlSessionFactory;
  5. this.executorType = executorType;
  6. this.exceptionTranslator = exceptionTranslator;
  7. this.sqlSessionProxy = (SqlSession)Proxy.newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[]{SqlSession.class}, new SqlSessionTemplate.SqlSessionInterceptor());
  8. }
  1. private class SqlSessionInterceptor implements InvocationHandler {
  2. private SqlSessionInterceptor() {
  3. }
  4. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  5. //获取同一个线程的复用sqlSession,如果没有则新生成一个并存到线程私有存储中
  6. SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
  7. Object unwrapped;
  8. try {
  9. //实际调用DefaultSession的对应方法
  10. Object result = method.invoke(sqlSession, args);
  11. //判断当前sqlSession是否被Spring管理,如果没有直接commit
  12. if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
  13. sqlSession.commit(true);
  14. }
  15. unwrapped = result;
  16. } catch (Throwable var11) {
  17. unwrapped = ExceptionUtil.unwrapThrowable(var11);
  18. if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
  19. SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
  20. sqlSession = null;
  21. Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
  22. if (translated != null) {
  23. unwrapped = translated;
  24. }
  25. }
  26. throw (Throwable)unwrapped;
  27. } finally {
  28. if (sqlSession != null) {
  29. //正常返回将线程私有sqlSession调用次数减一
  30. SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
  31. }
  32. }
  33. return unwrapped;
  34. }
  35. }
  1. public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
  2. Assert.notNull(sessionFactory, "No SqlSessionFactory specified");
  3. Assert.notNull(executorType, "No ExecutorType specified");
  4. //从线程私有存储中获取SqlSession
  5. SqlSessionHolder holder = (SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory);
  6. SqlSession session = sessionHolder(executorType, holder);
  7. if (session != null) {
  8. return session;
  9. } else {
  10. if (LOGGER.isDebugEnabled()) {
  11. LOGGER.debug("Creating a new SqlSession");
  12. }
  13. //没有则新建一个DefaultSqlSession
  14. session = sessionFactory.openSession(executorType);
  15. //存到线程私有存储中
  16. registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
  17. return session;
  18. }
  19. }

本质原理其实和SqlSessionManager一样,但是存储的机制不一样。
究其根本SqlSession真正的实现类只有DefaultSqlSession,SqlSessionManager和SqlSessionTemplate都是通过代理转发到DefaultSqlSession对应方法

单例模式下的DefaultSqlSession不是线程安全的,SqlSessionManager和SqlSessionTemplate线程安全的根本就是每一个线程对应的SqlSession都是不同的。如果每一个操作都创建一个SqlSession对象,操作完又进行销毁导致性能极差。通过线程私有ThreadLocal存储SqlSession进行复用,从而提高性能。

5.Mybatis缓存问题

5.1 一级缓存

在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。具体执行过程如下图所示。

Mybatis - 图10
每个SqlSession中持有了Executor,每个Executor中有一个LocalCache。当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement,在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户。
我们来看看如何使用MyBatis一级缓存。开发者只需在MyBatis的配置文件中,添加如下语句,就可以使用一级缓存。共有两个选项,SESSION或者STATEMENT,默认是SESSION级别,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个Statement有效。

  1. <setting name="localCacheScope" value="SESSION"/>
  • 开启一级缓存,范围为会话级别,调用三次getStudentById,只有第一次会查询数据库,后续使用了一级缓存。

Mybatis - 图11

  • 在修改操作后执行的相同查询,查询了数据库一级缓存失效
  • 验证一级缓存只在数据库会话内部共享,不同的会话缓存不能共用。

Mybatis - 图12
主要学习BaseExecutor的内部实现
在一级缓存的介绍中提到对Local Cache的查询和写入是在Executor内部完成的。在阅读BaseExecutor的代码后发现Local CacheBaseExecutor内部的一个成员变量,如下代码所示。

  1. public abstract class BaseExecutor implements Executor {
  2. protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  3. protected PerpetualCache localCache;

BaseExecutor成员变量之一的PerpetualCache,是对Cache接口最基本的实现,其实现非常简单,内部持有HashMap,对一级缓存的操作实则是对HashMap的操作。如下代码所示:

  1. public class PerpetualCache implements Cache {
  2. private String id;
  3. private Map<Object, Object> cache = new HashMap<Object, Object>();

在源码分析的最后,如果是insert/delete/update方法,缓存就会刷新的原因。

  1. @Override
  2. public int update(MappedStatement ms, Object parameter) throws SQLException {
  3. ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
  4. if (closed) {
  5. throw new ExecutorException("Executor was closed.");
  6. }
  7. clearLocalCache();
  8. return doUpdate(ms, parameter);
  9. }

一级缓存默认的是在一个SqlSession中。

  • MyBatis一级缓存的生命周期和SqlSession一致。
  • MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
  • MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。

    5.2 二级缓存

    如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询,具体的工作流程如下所示。

Mybatis - 图13
二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。

image.png

  • sqlSession1查询完数据后,不提交事务,sqlSession2相同的查询不命中缓存
  • sqlSession1查询完数据后,当提交事务时,sqlSession2相同的查询命中缓存
  • sqlSession3更新数据库,并提交事务后,sqlsession2StudentMapper namespace下的查询走了数据库,没有走Cache。
  • MyBatis的二级缓存不适应用于映射文件中存在多表查询的情况。

为什么要开启事务
由于使用了数据库连接池,默认每次查询完之后自动commit,这就导致两次查询使用的不是同一个sqlSessioin,根据一级缓存的原理,它将永远不会生效。 当我们开启了事务,两次查询都在同一个sqlSession中,从而让第二次查询命中了一级缓存。读者可以自行关闭事务验证此结论。 2、两种一级缓存模式 一级缓存的作用域有两种:session(默认)和statment,可通过设置local-cache-scope 的值来切换,默认为session。 二者的区别在于session会将缓存作用于同一个sqlSesson,而statment仅针对一次查询,所以,local-cache-scope: statment可以理解为关闭一级缓存。