深入mybatis源码解读~手把手带你debug分析源码

前言

更新点:结合自己阅读源码的经验,新增面试专栏,后续会一直更新。2023-02-16,如果回答的造成了误解,望斧正。看到并采纳会及时修成。

自留分析好的源码地址https://gitcode.net/qq_42875345/mybatiscore

本文大致脉络基于下图分析

在这里插入图片描述

mybatis是一款持久性的ORM框架,目的在于把数据库中的表中的信息转换成对象供我们操作,也就是说我们对数据库的操作有了mybatis可以转变为对对象的操作。

  • mybatis是怎么实现的呢?

要记到一点市面上所有的ORM框架无论如何都离不开JDBC操作,我们所谓的mybatis也好hibernate也罢其实本质都是对JDBC的包装而已。

先来回顾一下传统的JDBC操作步骤

  1. 加载驱动
  2. 获取连接
  3. 创建statement对象
  4. statement参数填充
  5. 执行查询(更新、删除)操作
  6. 从resultSet中获取结果集

在这里插入图片描述

/**
     * 使用JDBC连接并操作mysql数据库
     */
    public static void main(String[] args) {
        // 数据库驱动类名的字符串
        String driver = "com.mysql.cj.jdbc.Driver";
        // 数据库连接串
        String url = "url";
        // 用户名
        String username = "username";
        // 密码
        String password = "password";
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        try {
            // 1、加载数据库驱动( 成功加载后,会将Driver类的实例注册到DriverManager类中)
            Class.forName(driver);
            // 2、获取数据库连接
            conn = DriverManager.getConnection(url, username, password);
            // 3、获取数据库操作对象
            stmt = conn.createStatement();
            // 4、定义操作的SQL语句
            String sql = "select * from user where id = 1";
            // 5、执行数据库操作
            rs = stmt.executeQuery(sql);
            // 6、获取并操作结果集
            while (rs.next()) {
                System.out.println(rs.getInt("id"));
                System.out.println(rs.getString("name"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 7、关闭对象,回收数据库资源
            if (rs != null) { //关闭结果集对象
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (stmt != null) { // 关闭数据库操作对象
                try {
                    stmt.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (conn != null) { // 关闭数据库连接对象
                try {
                    if (!conn.isClosed()) {
                        conn.close();
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

研究mybatis源码入口的选择

如果从mybatisPlus项目分析mybatis的源码写如下demo即可直接debug了,毕竟springboot+mybatisPlus项目给我简化了很多配置,很多细节可能分析不到了,个人建议把mybatis源码克隆到本地,然后进行后续的源码研究。

 	@Autowired
    UserMapper userMapper;
    @Test
    public void test5() {
        User user = userMapper.queryManyparam("zzh", 1);
        User user2 = userMapper.queryManyparam("zzh", 1);
        //俩个不同的session,导致一级缓存失效
        System.out.println(user == user2);
    }

原生mybatis操作数据库

  1. 初始化sqlSessionFactory()
  2. 利用sqlSessionFactory获取对应的mapper
  3. 利用mapper对数据库进行查询、更新、删除操作
  private Configuration configuration;
  private JdbcTransaction jdbcTransaction;
  private Connection connection;
  private Reader resourceAsReader;
  private SqlSessionFactory sqlSessionFactory;
  
  public void init() throws SQLException, IOException {
    connection = DriverManager.getConnection("url", "username", "password");
    resourceAsReader = Resources.getResourceAsReader("mybatis.xml");
    jdbcTransaction = new JdbcTransaction(connection);
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsReader);
    configuration = sqlSessionFactory.getConfiguration();
  }
  /**
   * 相同的sql只会编译处理一次
   */
  @Test
  public void b() throws IOException, SQLException {
    init();
    SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    System.out.println(mapper.query(1));
    System.out.println(mapper.query(1));
  }

更加原生的mybatis操作数据库

  • 直接使用执行器操作数据库(simpleExecutor、cacheExecutor、reuseExecutor)
  • simpleExecutor:每次都会创建一个新的prepareStatement对象
  • cacheExecutor:针对mybatis二级缓存设计的执行器
  • reuseExecutor:相同的sql只会进行一次预处理,也就是说会进行prepareStatement的复用
  @Test
  public void a() throws SQLException, IOException {
    init();
    ReuseExecutor reuseExecutor = new ReuseExecutor(this.configuration, jdbcTransaction);
    MappedStatement mappedStatement = this.configuration.getMappedStatement("zzhTest.mybatis.mapper.UserMapper.queryManyparam");
    HashMap<String, Object> hashMap = new HashMap<>();
    hashMap.put("id",1);
    hashMap.put("name","zzh");
    List<Object> objects1 = reuseExecutor.doQuery(mappedStatement, hashMap, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, mappedStatement.getBoundSql(1));
    List<Object> objects2 = reuseExecutor.doQuery(mappedStatement, hashMap, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, mappedStatement.getBoundSql(1));
    System.out.println(objects1.get(0));
    System.out.println(objects1.get(0) == objects2.get(0));
  }

预处理次数的体现:控制台 Preparing: select * from user where id = ? 出现的次数即为预处理的次数
在这里插入图片描述

原生mybatis操作数据库源码分析

废话不多说我这里直接用原生的mybatis debug源码了(其实本质都是一样的咯),如下图片中的代码
在这里插入图片描述
我们从sqlSession中获取到的mapper对象本质是一个代理对象,debug进去newInstance()看是如何创建我们的mapper代理对象的?

在这里插入图片描述

把defultSqlSession、目标对象实现的接口类、methodCache包装成一个InvocationHandler对象,继而利用jdk代理生成对应的代理对象
在这里插入图片描述

小结:通过sqlSession.getMapper(UserMapper.class)获取到的是一个通过jdk代理生成的代理对象

分析mybatis查询(select)语句执行操作

mapper中的查询语句操作如下
在这里插入图片描述

直接debug mapper.query(1)来到这里。根据对数据库不同的操作有相应的分支进行处理,由于我们是查询操作来到select分支
在这里插入图片描述

  • select分支做了什么事情?
  1. 维护好paramName与参数paramValue的关系,包装成一个map。怎么解析的?下文有讲哦

例如拿如下代码分析就是维护@Param(“id”) 中的id 与 1 的关系,此时并没有解析sql语句中的id哦

@Select("select * from user where id = #{id}")
User query(@Param("id") Integer Uid);
query(1);

在这里插入图片描述
2. 根据这个param+查询方法的全路径限定名,进行数据库的查询操作(result=sqlSession.selectOne(command.getName(), param);)。怎么查询?下文有讲哦
在这里插入图片描述

convertArgsToSqlCommandParam(参数解析过程)

public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
      return null;
    } else if (!hasParamAnnotation && paramCount == 1) {
      Object value = args[names.firstKey()];
      return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
    } else {
      final Map<String, Object> param = new ParamMap<>();
      int i = 0;
      /**
       * 遍历所有的@params()中的参数name
       */
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
        /**
         * args:方法中传入的参数value数组
         * entry.getValue():@params()中的参数name
         */
        param.put(entry.getValue(), args[entry.getKey()]);
        /**
         * add generic param names (param1, param2, ...)
         */
        final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      /**
       * 建立paramName与paramValue的关系
       * void query(@param(id)int id,@param(name)String name)
       * query(1,"zzh")
       * 例如:id-1、name-zzh
       */
      return param;
    }
  }

看源码可能有点绕,不过我注释也写了很全哦,不懂没关系再来梳理一遍参数解析过程。mybatis中有如下俩个map

  1. args:保存的是我们的查询方法的参数value值。例如(1,2,3)
  2. names:保存的是我们查询方法的参数name值。例如(id,name,age)

由于下标都是一一对应的,通过args[names.getKey()]就可以获取到对应参数name的value值了。通过这样一个个维护一下关系,最终的param就形成了以参数name为key、以参数value为value的关系的一个map

  • 注意:mybatis显示的会加上例如param1-value这种类型的映射

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

selectOne(查询过程核心)

我们debug进去会来到这里,先获取对应的MappedStatement,然后走cacheExecutor执行器中的操作
在这里插入图片描述

mybatis使用的是一种装饰器的模式,在真正的查询数据库之前会依次从二级缓存(SynchronizedCache)、一级缓存(localCache)中获取数据,如果还获取不到才会走查询数据库的逻辑

点进上图的executor.query()会来到下图的cacheExecutor中的query方法,在此之前cacheExecutor还干了俩件事

  1. 将当前查询sql相关的信息包装成一个boundSql对象
  2. 创建一个cacheKey。cacheKey = statementId+sql+sql参数+cacheId

在这里插入图片描述

cacheExcutor中的核心方法query()

  • 查询二级缓存的前提
    1. 我们的项目中开启了二级缓存
    2. 我们mapper类中对应的查询方法上没有加 @Options(useCache = false)
  • 没有开启二级缓存:直接走cacheExecutor中的装饰器simpleExecutor.query(),
  • 开启了正常的二级缓存相关的配置:会先判断是否需要清空二级缓存,然后从缓存管理器中的二级缓存中获取缓存,获取不到的话才会查询数据库。
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    /**
     * 获取二级缓存:SynchronizedCache
     */
    Cache cache = ms.getCache();
    if (cache != null) {
      /**
       * 尝试清空二级缓存(标记 clearOnCommit = true)
       */
      flushCacheIfRequired(ms);
      /**
       * 可以设置 @Options(useCache = false) 关闭查询二级缓存
       * resultHandler = Executor.NO_RESULT_HANDLER = null
       */
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        /**
         * 从TransactionalCacheManager中获取二级缓存中的数据
         */
          List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          /**
           * 二级缓存中获取不到数据,进行查询数据库操作
           * delegate:SimpleExecutor
           */
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          /**
           * 把数据填充进暂存区中的entriesToAddOnCommit的这个map里面
           * 暂存区:TransactionalCache
           */
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

从上面这段代码可以知道我们平常使用如下配置时的生效时机以及在mybatis中的源码体现对应在哪

  • 如何配置开启二级缓存?

在mapper类上加@CacheNamespace或者在mapper对应的xml文件中加 < cache></ cache>

  • 如何禁用二级缓存?
  @Select("select * from user where id = #{id}")
  @Options(useCache = false)
  User query(@Param("id") Integer Uid);
  • 如何每次查询都清空二级缓存(SynchronizedCache)?
  @Options(flushCache = Options.FlushCachePolicy.TRUE)
  @Select("select * from user where id = #{id}")
  User query(@Param("id") Integer Uid);

配置在源码中的体现

mybatis清空二级缓存的流程时设置 clearOnCommit = true 并且清空entriesToAddOnCommit 这个map

private final Map<Object, Object> entriesToAddOnCommit;

private void flushCacheIfRequired(MappedStatement ms) {
	/**
     * 获取二级缓存
     */
    Cache cache = ms.getCache();
    /**
     * 如果开启对应的方法上面开启了 FlushCachePolicy = true
     * 那么此处会清除二级缓存(本质是标记 clearOnCommit = true)
     */
    if (cache != null && ms.isFlushCacheRequired()) {
      tcm.clear(cache);
    }
  }

 @Override
  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }

怎么从二级缓存中获取数据?

接着分析cacheExecutor.query()中的tcm.getObject()这行代码可以知道怎么从二级缓存中获取数据的哦。

  1. 首先从以SynchronizedCache为头的Cache链条依次getObject(cacheKey)获取缓存数据
  2. 然后判断是否缓存中有cacheKey对应的缓存,如果没有,将此key存入一个名字为entriesMissedInCache的map中,目的就是为了防止缓存穿透。相当于把这个人拉入黑名单了。emm可以看我以前写的 缓存问题系列文章
  3. 如果拿到了缓存,还有一个判断是否clearOnCommit被标记为true?这个是为了解决缓存一致性问题的。可以看我以前写的文章简单解决缓存+数据库数据一致性问题
  4. 最终才是返回从缓存中获取到的数据
public Object getObject(Object key) {
    /**
     * delegate:Cache(以SynchronizedCache为头的Cache链条)
     * key:statementId~sql~参数~id组成
     */
    Object object = delegate.getObject(key);
    /**
     *
     * 防止缓存穿透:二级缓存中没有数据,将key放入entriesMissedInCache(HashSet)中
     * 
     */
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    // issue #146
    /**
     * 如果此时有人清空二级缓存那么clearOnCommit = true
     * 此时的效果也相当于从二级缓存中获取不到数据
     */
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

如果是我们第一次debug到这肯定二级缓存中没有数据撒,还记得我们分析到哪了咩,分析到下图这。本质如下箭头标注的query()是调用同一个query()方法,如果没有获取到缓存将会执行箭头标注的方法(simpleExecutor.query(…))。
在这里插入图片描述

simpleExecutor中的核心方法query()

此query()大体思路分析如下:

  1. queryStack == 0 && 设置了FlushCachePolicy.TRUE那么每次都会先清空一级缓存
  2. 每次从一级缓存中获取数据前,记录查询次数(queryStack++)
  3. 查询一级缓存(localCache)
  4. 缓存中获取不到数据(list)查询数据库操作,反之不用
  5. 将延时map(deferredLoads)中的数据添加到list
  6. 设置了一级缓存的域范围为STATEMENT将会在每次查询完毕后清空一级缓存
@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.");
    }
    /**
     * 如果设置了flushCacheRequired属性将会清空缓存
     * 例如:@Options(flushCache = Options.FlushCachePolicy.TRUE)
     */
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      /**
       * 清空一级缓存中的数据
       * 一级缓存:localCache
       */
      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();
      }
      /**
       * 清空延时加载map中的数据
       */
      deferredLoads.clear();
      /**
       * 设置了一级缓存的域范围为STATEMENT将会在每次查询完毕后清空一级缓存
       */
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache();
      }
    }
    return list;
  }

思路分析完了来扣细节~~~~~~~~~~~~

  • mybatis是如何清空一级缓存的?

清空了localOutputParameterCache、localCache(一级缓存)俩个map
在这里插入图片描述

  • 如何从一级缓存中获取数据?

localCache.getObject(key),直接根据key来获取

  • mybatis是如何查询数据库的?

下面通过分析queryFromDatabase()方法一趟究竟

mybatis查询数据库源码

点开queryFromDatabase()方法代码如下,注释写的很详细着重看看doQuery()看如何进行查询的

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)和mybatis循环依赖有关
     */
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      /**
       * 执行BaseExecutor的实现类中的doQuery方法
       */
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    /**
     * 把查询到的数据放入一级缓存
     */
    localCache.putObject(key, list);
    /**
     * TODO 这个map作用暂时不清楚
     * 如果当前的mappedStatement的类型为CALLABLE,那么会往localOutputParameterCache中put一个数值
     */
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

mybatis查询数据库必然绕不开jdbc,点进doQuery()来到如下代码。查询和普通的jdbc操作一样分为俩步

  1. 准备一个statement对象
  2. 执行statement.execute()
@Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      /**
       * 创建合适的statementHandler,主要研究prepareStatementHandler
       */
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      /**
       * 利用statementHandler创建对应的statement对象
       */
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      /**
       * 关闭statement
       */
      closeStatement(stmt);
    }
  }

准备一个statement代码分为如下三步:mybatis中的prepareStatement()和我们熟悉的jdbc操作差不多,亦是如此就是多了几层包装而已。

  1. 准备连接
  2. 创建statement
  3. 为statement填充参数
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    /**
     * 获取连接
     */
    Connection connection = getConnection(statementLog);
    /**
     * 实例化prepareStatement
     */
    stmt = handler.prepare(connection, transaction.getTimeout());
    /**
     * 为prepareStatement 填充参数,涉及到sql参数解析
     */
    handler.parameterize(stmt);
    return stmt;
  }

准备statement第二步:源码就是封装了jdbc而已,就不贴代码贴几张图片了(本质都是connection.prepareStatement()),以下图片是的代码
在这里插入图片描述

在这里插入图片描述

准备statement第三步:填充statement中的参数,将我们的查询条件塞入sql语句中,即 select * from user where id = #{id} 转换成 select * from user where id = 1 的过程
在这里插入图片描述

public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      /**
       * 挨个设置参数
       */
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          /**
           * select * from user where id = #{id}
           * 获取#{id}中的name,例如:这种情况下面获取到的propertyName = id
           */
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;

          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            /**
             * 参数对象包装成一个MetaObject对象
             * parameterObject:即使我们sql中只有一个参数,mybatis也会默认额外生成param1这种
             */
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            /**
             * 获取参数的具体值
             */
            value = metaObject.getValue(propertyName);
          }
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            /**
             * 本质就是调用prepareStatement.setInt(0,value)
             */
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

上面的代码可能有点绕,现在重新理一遍思路。确定一个ps.setInt(i, value)操作分为三步:

  1. 获取对应占位符上的value
  2. 型获取填充占位符的下标 i
  3. 获取对应占位符上的参数类型
  4. 然后才是开始填充

第一步确定流程:由于我们一开始封装的boundSql对象包括了#{id} 的相关参数名称信息,且我们一开始就维护了一个map(params中维护了key:id、value :1的关系),我们通过解析#{ id } ,然后以id为key,从params中获取对应的value。
第二步确定流程:由于里面是一个for循环,例如select * from user where id = #{1} and name = “zzh”。第一次填充#{ id } ,i = 0,遍历到填充 #{ name }时,对应的 i = 1。
第三步确定流程:直接从parameterMapping中可以获取到参数类型

到此一个statement就成功创建好了接下来就是开始执行了execute操作了
在这里插入图片描述

查询结果集处理

来到handleResultSets()方法,观察到无论走哪个分支都会调用其中的handleResultSet()方法
在这里插入图片描述
在这里插入图片描述
来到handleResultSet()方法,观察无论如何都会调用handleRowValues()方法在这里插入图片描述

来到handleRowValues()方法,这里如果结果集只是简单的属性映射将会走简单结果集处理逻辑,如果存在嵌套结果集那么会走嵌套结果集的逻辑
在这里插入图片描述

简单结果集处理

针对结果一行行进行处理,本质创建一个空的行接收对象封装成metaObject对象,然后基于metaObject对象进行属性填充。
在这里插入图片描述

在这里插入图片描述

自动属性填充

获取对应列的值(rs.getInt(“id”)),然后把值填充到metaObject对应的property上面。

在这里插入图片描述

手动属性填充(applyPropertyMappings)
  1. 遍历所有的属性字段,先获取对应字段的value然后把value填充进metaObject中。
private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)
    throws SQLException {
    final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
    boolean foundValues = false;
    final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
    /**
     * 遍历所有的属性字段进行属性填充
     */
    for (ResultMapping propertyMapping : propertyMappings) {
      String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
      if (propertyMapping.getNestedResultMapId() != null) {
        // the user added a column attribute to a nested result map, ignore it
        column = null;
      }
      if (propertyMapping.isCompositeResult()
        || (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))
        || propertyMapping.getResultSet() != null) {
        /**
         * 获取要填充的某个属性字段的value值
         */
        Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
        // issue #541 make property optional
        final String property = propertyMapping.getProperty();
        if (property == null) {
          continue;
        }
        else if (value == DEFERRED) {
          foundValues = true;
          continue;
        }
        if (value != null) {
          foundValues = true;
        }
        if (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive())) {
          // gcode issue #377, call setter on nulls (value is not 'found')
          metaObject.setValue(property, value);
        }
      }
    }
    return foundValues;
  }
获取属性值的过程(getPropertyMappingValue)

按照属性类型的不同分别对应三种不同的获取方式

  1. 获取子查询类型属性
  2. 获取resultset类型的结果值
  3. 获取简单属性
private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
    throws SQLException {
    /**
     * 存在子查询,先走嵌套子查询逻辑,进行相应的子查询操作,查到相应的子查询中的数值然后返回
     */
    if (propertyMapping.getNestedQueryId() != null) {
      return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
    }
    /**
     * 存在ResultSet类型的结果映射,进行相应的属性填充操作
     */
    else if (propertyMapping.getResultSet() != null) {
      addPendingChildRelation(rs, metaResultObject, propertyMapping);   // TODO is that OK?
      return DEFERRED;
    }
    /**
     * 简单单属性填充
     */
    else {
      final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
      final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
      return typeHandler.getResult(rs, column);
    }
  }

简单属性值的获取

int result = rs.getInt(columnName);

获取获取子查询类型属性

先执行子查询中的逻辑,直至子查询中的简单属性填充完毕,然后返回子查询的结果

private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
    throws SQLException {
    /**
     * 子查询语句的全路径限定名
     */
    final String nestedQueryId = propertyMapping.getNestedQueryId();
    /**
     * 需要填充属性字段的name
     */
    final String property = propertyMapping.getProperty();
    /**
     * 子查询所有信息包装成的一个MappedStatement对象
     */
    final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
    /**
     * 子查询的参数信息
     */
    final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
    /**
     * 传入子查询的参数value
     */
    final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);
    Object value = null;
    if (nestedQueryParameterObject != null) {
      final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
      final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
      final Class<?> targetType = propertyMapping.getJavaType();
      /**
       * 如果一级缓存中有数值,且缓存值是一个占位符号,与循环依赖有关
       */
      if (executor.isCached(nestedQuery, key)) {
        /**
         * 把此数据添加到延时加载,为的是解决循环依赖问题
         */
        executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
        value = DEFERRED;
      } else {
        final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
        /**
         * 如果是懒加载执行相应逻辑
         */
        if (propertyMapping.isLazy()) {
          lazyLoader.addLoader(property, metaResultObject, resultLoader);
          value = DEFERRED;
        } else {
          /**
           * 其他情况正常加载,进行查询数据库操作
           */
          value = resultLoader.loadResult();
        }
      }
    }
    return value;
  }

resultLoader.loadResult()会重复这段代码的执行
在这里插入图片描述
最终我们不论是简单属性还是子查询属性,都能获取到了,最后也就完成了我们的属性填充了

在这里插入图片描述

最终我们获取到了对应的值,然后就可以把值填充进metaObject中了。最终我们的这个metaObject会被包装然后返回。我们最终得到的数据也就是这个MetaObject的包装对象了。

面试专栏

mybatis是怎么和数据库连上的?

答:在 mybatis 进行 crud 的时候,会经过 @param 注解参数解析映射等等操作,然后从 Executor 的里面尝试获取缓存数据,如果获取不到缓存的话,最终会进行 db 操作,在获取 statement 的时候里面,和原生的 jdbc 操作一样,通过 driverManager 获取数据库连接,然后通过连接创建 statement,最终实现对数据库的操作。

Mybatis 缓存了解吗?

答:了解的,mybatis 分一级缓存与二级缓存,我们在使用 mybatis 进行数据查询的时候,先是会经过 cacheExcutor ,尝试从二级缓存中拿数据,当然里面还有包含对 @option 注解,也就是对二级缓存策略的解析等等操作,然后如果二级缓存为空,接着会去 simpleExecutor 里面尝试从一级缓存中获取数据,如果俩层缓存都获取不到数据的话,最终查 db 。

Mybatis 怎么Interface 对应的?(考察 mapper 为什么会生效?还是 xml 与 mapper 对应?)

答:就 spring boot项目而言,使用 mybatis 的时候都会用到 @mapperScans 这个注解,里面实际上是一个 import 注册配置类,他会扫描指定路径下的类,解析成一个个 beanDefaine,注册到 beanDefaineMap 中,然后项目启动的时候会将 beanDefaineMap 中的beanDefaine 注册成一个个 bean,这样一来我们的 Interface 就和 mybatis 对应了。
答:每个 xml 文件里面都有一个叫做 namespace 的东西,value 值与 Interface 对应。具体源码怎么对应的没有研究过

小咸鱼的技术窝

关注不迷路,日后分享更多技术干货,B站、CSDN、微信公众号同名,名称都是(小咸鱼的技术窝)更多详情在主页
在这里插入图片描述

  • 11
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
MyBatis是一个开源的持久层框架,它的配置文件包含了会深深影响MyBatis行为的设置和属性信息。配置文件的顶层结构包括properties(属性)、settings(设置)、typeAliases(类型别名)、typeHandlers(类型处理器)、objectFactory(对象工厂)、plugins(插件)、environments(环境配置)、mappers(映射器)等。\[3\] 在MyBatis源码中,有一个方法build,该方法用于解析动态脚本并生成SqlSource对象。在该方法中,首先会调用parseDynamicTags方法解析动态标签,得到一个包含多个SqlNode的列表contents。然后,通过将contents传入MixedSqlNode的构造函数,创建一个MixedSqlNode对象rootSqlNode。接下来,根据是否为动态脚本,分别创建DynamicSqlSource或RawSqlSource对象,并将configuration、rootSqlNode和parameterType作为参数传入构造函数。最后,返回创建的SqlSource对象。\[2\] 另外,还有一个方法configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql),该方法用于创建StatementHandler对象。具体的解读需要查看该方法的实现代码。 #### 引用[.reference_title] - *1* [mybatis源码深度解析](https://blog.csdn.net/qq_31359923/article/details/126582701)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v4^insert_chatgpt"}} ] [.reference_item] - *2* [【MybatisMybatis源码解读](https://blog.csdn.net/keepfriend/article/details/124356649)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v4^insert_chatgpt"}} ] [.reference_item] - *3* [mybatis源码解析](https://blog.csdn.net/weixin_43189971/article/details/125418419)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v4^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小咸鱼的技术窝

你的鼓励将是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值