Mybatis源码06 - SqlSession的执行流程(重点)

发布于:2025-02-14 ⋅ 阅读:(8) ⋅ 点赞:(0)

SqlSession的执行流程(重点)

前面主要讲mybatis如何解析配置文件,这些都是一次性的过程。

  • xml配置 -> Configuration -> SqlSessionFactory

从这个开始讲解动态的过程,它们跟应用程序对mybatis的调用密切相关

  1. 构建SqlSession

    • xml配置 -> Configuration -> SqlSessionFactory】-> 前置处理
  2. 构建StatementHandler

    • 【SqlSession -> MapperProxy代理 -> MapperMethod分发 -> 判断选择执行器类型 -> Executor -> StatementHandler】
  3. 执行并获取结果

    • 【StatementHandler -> paramterHandler -> JDBC -> resultSetHandler】

一:SqlSessionFactory & SqlSession

从表面上来看,咱们都是通过SqlSession去执行sql语句

正如其名,Sqlsession对应着一次数据库会话。由于数据库会话不是永久的,因此Sqlsession的生命周期也不应该是永久的

相反,在你每次访问数据库时都需要创建它,当一旦关闭了Sqlsession就需要重新创建它

先看看是怎么获取SqlSession的吧:

在这里插入图片描述
首先,SqlSessionFactoryBuilder去读取mybatis的配置文件然后build一个DefaultSqlSessionFactory

在这里插入图片描述

/**
  * 一系列的构造方法最终都会调用本方法(配置文件为Reader时会调用本方法,还有一个InputStream方法与此对应)
  * @param reader
  * @param environment
  * @param properties
  * @return
  */
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
        //通过XMLConfigBuilder解析配置文件,解析的配置相关信息都会封装为一个Configuration对象
        XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
        //这儿创建DefaultSessionFactory对象
        return build(parser.parse());
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
        ErrorContext.instance().reset();
        try {
            reader.close();
        } catch (IOException e) {
            // Intentionally ignore. Prefer previous error.
        }
    }
}

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

当我们获取到SqlSessionFactory之后,就可以通过SqlSessionFactory去获取SqlSession对象。

public SqlSessionopenSession() {  
    return openSessionFromDataSource(configuration.getDefaultExecutorType(),null, false);  
}

/**
 * 从数据源打开一个会话
 * 
 * @param execType 执行器类型,用于确定会话使用的执行器模式(如简单执行器、批处理执行器等)
 * @param level 事务隔离级别,用于设置会话中事务的隔离级别
 * @param autoCommit 自动提交标志,用于设置会话中事务是否自动提交
 * @return 返回一个SqlSession对象,用于执行数据库操作
 * 
 * 此方法首先从配置中获取环境对象,然后从环境中获取事务工厂,利用事务工厂和数据源创建事务对象
 * 接着根据配置和事务对象创建执行器,最后使用这些组件创建并返回一个默认的SQL会话对象
 * 如果在创建过程中发生异常,将关闭事务并抛出异常
 */
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        // 从配置中获取环境对象
        final Environment environment = configuration.getEnvironment();
        
        // 从环境中获取事务工厂
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        // 使用事务工厂、数据源和事务隔离级别创建事务对象
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        
        // 根据配置和事务对象创建执行器
        final Executor executor = configuration.newExecutor(tx, execType);
        
        // 使用配置、执行器和自动提交标志创建并返回一个默认的SQL会话对象
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        // 如果创建过程中发生异常,关闭事务并抛出异常
        closeTransaction(tx); // 可能已经获取了连接,因此调用close()
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        // 重置错误上下文
        ErrorContext.instance().reset();
    }
}

通过以上步骤,咱们已经得到SqlSession对象了。接下来就是执行SQL语句了

回想一下之前写的Demo

String resource = "mybatis-config.xml";
SqlSessionFactory sessionFactory = null;
try {
    // 通过builder()方法获取到工厂
    sessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader(resource));
} catch(IOException e) {
    e.printDStackTrace();
}
// 通过SQLSessionFactory获取SQLSession
SqlSession sqlSession = sessionFactory.openSession();

创建Sqlsession的地方只有一个,那就是SqlsessionFactory的openSession方法

可以看出,创建sqlsession经过了以下几个主要步骤:

  1. 从配置中获取环境envrionment
  2. 从Environment中取得DataSource & TransactionFactory
  3. 从DataSource里获取数据库连接对象Connection
  4. 在取得的数据库连接上创建事务对象Transaction
  5. 创建Executor对象(该对象非常重要,事实上sqlsession的所有操作都是通过它完成的);
  6. 创建sqlsession对象

SqlSession咱们也拿到了,咱们可以调用SqlSession中一系列的select…, insert…, update…, delete…进行CRUD操作了

二:MapperProxy

在这里插入图片描述
在mybatis中,通过MapperProxy动态代理咱们的DAO

也就是说当执行自己写的DAO里面的方法的时候,其实是对应的mapperProxy在代理

/**
  * 什么都不做,直接去configuration中找
  */
@Override
public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
}


/**
 * 烫手的山芋,俺不要,你找mapperRegistry去要
 * @param type
 * @param sqlSession
 * @return
 */
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}


/**
 * 烂活净让我来做了,没法了,下面没人了,我不做谁来做
 * @param type
 * @param sqlSession
 * @return
 */
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 能偷懒的就偷懒,俺把粗活交给MapperProxyFactory去做
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
        // 关键在这儿
        return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
        throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
}

MapperProxyFactory是个苦B的人,粗活最终交给它去做了

/**
  * 别人虐我千百遍,我待别人如初恋
  * @param mapperProxy
  * @return
  */
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
    //动态代理我们写的dao接口
    return (T) Proxy.newProxyInstance(
        mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy
    );
}

public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}

通过以上的动态代理,就可以方便地使用DAO接口啦,就像之前写的demo那样

UserDao userMapper = sqlSession.getMapper(UserDao.class);  
User insertUser = new User();

三:Excutor执行器

Executor与Sqlsession的关系就像市长与书记,Sqlsession只是个门面,真正干事的是Executor

Sqlsession对数据库的操作都是通过Executor来完成的。与Sqlsession一样,Executor也是动态创建的:

在这里插入图片描述

1:Executor创建的源代码

/**
 * 根据事务和执行器类型创建一个新的执行器
 * 
 * @param transaction 事务对象,用于在执行器中执行操作
 * @param executorType 执行器类型,决定创建哪种类型的执行器
 * @return 创建的执行器对象
 */
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    // 如果执行器类型为null,则使用默认执行器类型
    executorType = executorType == null ? defaultExecutorType : executorType;
    Executor executor;

    // 根据执行器类型创建相应的执行器实例
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }

    // 如果缓存启用,则用CachingExecutor包装当前执行器
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }

    // 使用拦截器链对执行器进行插件化处理,并返回最终的执行器对象
    return (Executor) interceptorChain.pluginAll(executor);
}

可以看出,如果不开启cache的话,创建的Executor只是3中基础类型之一:

  • BatchExecutor专门用于执行批量sql操作
  • ReuseExecutor会重用statement执行sql操作
  • SimpleExecutor只是简单执行sql没有什么特别的

开启cache的话(默认是开启的并且没有任何理由去关闭它),就会创建CachingExecutor,它以前面创建的Executor作为唯一参数

CachingExecutor在查询数据库前先查找缓存,若没找到的话调用delegate(就是构造时传入的Executor对象)从数据库查询,并将查询结果存入缓存中

CachingExecutor使用了装饰器模式,这在Mybatis中源码中这个设计模式经常出现

在这里插入图片描述

2:MapperProxy

我们知道对被代理对象的方法的访问都会落实到代理者的invoke上来,MapperProxy的invoke如下:

/**
 * MapperProxy在执行时会触发此方法
 */
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
        try {
            return method.invoke(this, args);
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //二话不说,主要交给MapperMethod自己去管
    return mapperMethod.execute(sqlSession, args);
}

3:MapperMethod

就像是一个分发者,他根据参数和返回值类型选择不同的sqlsession方法来执行。

这样mapper对象与sqlsession就真正的关联起来了[sqlSession -> MapperProxy -> MapperMethod分发 -> 判断选择执行器类型]

看着代码不少,不过其实就是先判断CRUD类型,然后根据类型去选择到底执行sqlSession中的哪个方法,绕了一圈,又转回sqlSession了

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    if (SqlCommandType.INSERT == command.getType()) {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
    } else if (SqlCommandType.DELETE == command.getType()) {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
    } else if (SqlCommandType.SELECT == command.getType()) {
        if (method.returnsVoid() && method.hasResultHandler()) {
            executeWithResultHandler(sqlSession, args);
            result = null;
        } else if (method.returnsMany()) {
            result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
            result = executeForMap(sqlSession, args);
        } else {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = sqlSession.selectOne(command.getName(), param);
        }
    } else {
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
        throw new BindingException("Mapper method '" + command.getName() 
                                   + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
}

既然又回到SqlSession了,而sqlsession只是一个门面,真正作用的是executor,对sqlsession方法的访问最终都会落到executor的相应方法上去

Executor分成两大类,一类是CacheExecutor,另一类是普通Executor。

前面已经分析过了Executor的创建,这里看看SqlSession的CURD方法,这里选取的是selectList()方法

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
        MappedStatement ms = configuration.getMappedStatement(statement);
        //CRUD实际上是交给Excetor去处理,而excutor其实也只是穿了个马甲而已
        return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

cacheExector

CacheExecutor有一个重要属性delegate,它保存的是某类普通的Executor,值在构照时传入

执行数据库update操作时,它直接调用delegate的update方法,执行query方法时先尝试从cache中取值,取不到再调用delegate的查询方法

并将查询结果存入cache中。代码如下:

/**
   * 根据给定的映射语句和其他参数查询数据
   * 如果存在缓存且满足使用缓存的条件,则从缓存中获取数据;否则,委托给底层实现进行查询
   * 
   * @param ms 映射语句对象,包含了执行信息
   * @param parameterObject 查询参数对象
   * @param rowBounds 分页信息
   * @param resultHandler 结果处理器
   * @param key 缓存键
   * @param boundSql 绑定的SQL对象
   * @return 查询结果的列表
   * @throws SQLException 如果查询过程中出现数据库错误
   */
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler,
                         CacheKey key, BoundSql boundSql) throws SQLException {
    // 获取映射语句的缓存配置
    Cache cache = ms.getCache();
    if (cache != null) {
        // 如果需要,刷新缓存
        flushCacheIfRequired(ms);
        // 检查是否满足使用缓存的条件
        if (ms.isUseCache() && resultHandler == null) {
            // 确保没有输出参数,因为缓存对象不应该包含输出参数
            ensureNoOutParams(ms, boundSql);
            // 从缓存中尝试获取数据
            @SuppressWarnings("unchecked")
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
                // 如果缓存中没有数据,则委托给底层实现进行查询
                list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // 将查询结果放入缓存
                tcm.putObject(cache, key, list); // issue #578 and #116
            }
            // 返回从缓存中获取的数据
            return list;
        }
    }
    // 如果不满足使用缓存的条件,则直接委托给底层实现进行查询
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

普通Executor

有3类,他们都继承于BaseExecutor,BatchExecutor专门用于执行批量sql操作

ReuseExecutor会重用statement执行sql操作,SimpleExecutor只是简单执行sql没有什么特别的。

先会进入BaseExecutor中的query()方法

// 抑制未检查转换的警告
@SuppressWarnings("unchecked")
// 实现查询功能,根据提供的MappedStatement和其他参数,从数据库或缓存中获取数据列表
@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.");
  }
  
  // 如果当前查询堆栈高度为0且MappedStatement要求刷新缓存,则清除本地缓存
  if (queryStack == 0 && ms.isFlushCacheRequired()) {
    clearLocalCache();
  }
  
  // 初始化结果列表,用于存储查询结果
  List<E> list;
  try {
    // 增加查询堆栈高度,以追踪递归查询的深度
    queryStack++;
    // 如果结果处理器为空,尝试从本地缓存中获取结果;否则初始化结果列表为null
    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--;
  }
  
  // 如果查询堆栈高度为0,即没有未完成的查询操作,执行一些收尾工作
  if (queryStack == 0) {
    // 加载所有延迟加载的属性
    for (DeferredLoad deferredLoad : deferredLoads) {
      deferredLoad.load();
    }
    // 清除延迟加载列表
    deferredLoads.clear();
    // 如果配置的本地缓存作用域为STATEMENT,则清除本地缓存
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      clearLocalCache();
    }
  }
  
  // 返回查询结果列表
  return list;
}

然后进入到queryFromDatabase()方法

/**
   * 从数据库查询数据
   * 
   * @param ms MappedStatement 对象,包含了完整的语句信息
   * @param parameter 查询参数,用于执行SQL查询
   * @param rowBounds 用于分页查询的参数,控制返回结果的范围
   * @param resultHandler 结果处理器,处理查询结果
   * @param key 缓存的唯一键,用于标识缓存项
   * @param boundSql 绑定的SQL对象,包含了预处理SQL语句和参数
   * @return 查询结果的列表
   * @throws SQLException 如果查询过程中发生SQL异常
   * 
   * 此方法首先尝试从本地缓存中获取查询结果,如果缓存中已存在,则直接返回缓存结果,
   * 否则执行SQL查询,并将查询结果存入本地缓存。这样做可以减少数据库访问次数,提高查询效率。
   * 如果查询语句类型为CALLABLE(存储过程),还将参数对象存入输出参数缓存,以便后续使用。
   */
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 {
        // 执行实际的SQL查询
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        // 查询执行完毕后,移除缓存中的占位符
        localCache.removeObject(key);
    }
    // 将查询结果存入缓存
    localCache.putObject(key, list);
    // 如果查询语句是CALLABLE类型,将参数对象存入输出参数缓存
    if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
    }
    // 返回查询结果列表
    return list;
}

可以看到核心就是doQuery()方法,这里以SimpleExecutor中的doQuery为例

/**
   * 根据给定的MappedStatement和其他参数执行查询操作,返回查询结果列表
   * 
   * @param ms MappedStatement对象,包含映射信息
   * @param parameter 查询参数对象
   * @param rowBounds 用于分页查询的行边界对象
   * @param resultHandler 结果处理器,用于处理查询结果
   * @param boundSql 预处理的SQL对象
   * @return 包含查询结果的List对象
   * @throws SQLException 如果查询过程中出现数据库访问异常
   */
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
                           BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
        // 获取Configuration对象,用于创建StatementHandler
        Configuration configuration = ms.getConfiguration();
        // 创建StatementHandler对象,用于处理SQL语句
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler,
                                                                     boundSql);
        // 准备SQL语句并返回Statement对象
        stmt = prepareStatement(handler, ms.getStatementLog());
        // 通过StatementHandler执行查询并返回结果列表
        return handler.query(stmt, resultHandler);
    } finally {
        // 确保关闭Statement对象,避免资源泄露
        closeStatement(stmt);
    }
}

Mybatis内置的ExecutorType有3种,默认的是simple,该模式下它为每个语句的执行创建一个新的预处理语句,单条提交sql;

而batch模式重复使用已经预处理的语句, 并且批量执行所有更新语句,显然batch性能将更优;

但batch模式也有自己的问题,比如在Insert操作时,在事务没有提交之前,是没有办法获取到自增的id,这在某型情形下是不符合业务要求的;

通过走码和研读spring相关文件发现,在同一事务中batch模式和simple模式之间无法转换

由于本项目一开始选择了simple模式,所以碰到需要批量更新时,只能在单独的事务中进行;

在代码中使用batch模式可以使用以下方式:

//从spring注入原有的sqlSessionTemplate
@Autowired
private SqlSessionTemplate sqlSessionTemplate;

public void testInsertBatchByTrue() {
    //新获取一个模式为BATCH,自动提交为false的session
    //如果自动提交设置为true,将无法控制提交的条数,改为最后统一提交,可能导致内存溢出
    SqlSession session = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH, false);
    //通过新的session获取mapper
    fooMapper = session.getMapper(FooMapper.class);
    int size = 10000;
    try {
        for (int i = 0; i < size; i++) {
            Foo foo = new Foo();
            foo.setName(String.valueOf(System.currentTimeMillis()));
            fooMapper.insert(foo);
            if (i % 1000 == 0 || i == size - 1) {
                //手动每1000个一提交,提交后无法回滚
                session.commit();
                //清理缓存,防止溢出
                session.clearCache();
            }
        }
    } catch (Exception e) {
        //没有提交的数据可以回滚
        session.rollback();
    } finally {
        session.close();
    }
}

上述代码没有使用spring的事务,改手动控制,如果和原spring事务一起使用,将无法回滚,必须注意,最好单独使用

四:StatementHandler

可以看出,Executor本质上也是个甩手掌柜,具体的事情原来是StatementHandler来完成的。

当Executor将指挥棒交给StatementHandler后,接下来的工作就是StatementHandler的事了。

我们先看看StatementHandler是如何创建的:

public StatementHandler newStatementHandler(Executor executor, MappedStatementmappedStatement,  
                                            ObjectparameterObject, RowBounds rowBounds, ResultHandler resultHandler) {  
    
    // 通过执行器,mappedStatement, 参数列表,结果处理器构建StatementHandler
    StatementHandler statementHandler = 
        new RoutingStatementHandler(executor, mappedStatement,parameterObject,rowBounds, resultHandler);  
    
    // 添加拦截器插件
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);  
    return statementHandler;
}

可以看到每次创建的StatementHandler都是RoutingStatementHandler,它只是一个分发者

他一个属性delegate用于指定用哪种具体的StatementHandler。

可选的StatementHandler有SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler三种。

选用哪种在mapper配置文件的每个statement里指定,默认的是PreparedStatementHandler。

在这里插入图片描述
同时还要注意到StatementHandler是可以被拦截器拦截的,和Executor一样,被拦截器拦截后的对像是一个代理对象。

由于mybatis没有实现数据库的物理分页,众多物理分页的实现都是在这个地方使用拦截器实现的

StatementHandler创建后需要执行一些初始操作,比如statement的开启和参数设置、对于PreparedStatement还需要执行参数的设置操作等

private Statement prepareStatement(StatementHander handler) throws SQLException {
    Statement stmt;
    Connection connection = transaction.getConnection();
    // Statement的开启和参数设置没什么特别的地方
    stmt = handler.prepare(connection);
    // 这个可以看看
    handler.parameterize(stmt);
    
    return stmt;
}

handler.parameterize()通过调用ParameterHandler#setParameters()完成参数的设置

ParameterHandler随着StatementHandler的创建而创建,默认的实现是DefaultParameterHandler:

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {  
    ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement,parameterObject,boundSql);  
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);  
    return parameterHandler;  
}

同Executor和StatementHandler一样,ParameterHandler也是可以被拦截的。

DefaultParameterHandler里设置参数的代码如下:

// DefaultParamterHandler#setParameters()

/**
   * 设置PreparedStatement的参数
   * 
   * @param ps PreparedStatement对象,用于执行SQL语句
   * 
   * 该方法根据mappedStatement中的参数映射,将参数对象的属性值设置到PreparedStatement中
   * 它首先获取参数映射列表,然后遍历列表,对于每个参数映射,根据其属性名和类型处理器设置参数值
   * 在设置参数值时,会根据参数对象的类型和属性名获取相应的值,并使用类型处理器将其设置到PreparedStatement中
   * 如果遇到类型错误或SQL异常,将抛出TypeException异常
   */
@Override
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;
                // 获取参数映射的属性名
                String propertyName = parameterMapping.getProperty();
                // 首先检查是否有额外的参数,如果有,则使用额外的参数值
                if (boundSql.hasAdditionalParameter(propertyName)) { 
                    value = boundSql.getAdditionalParameter(propertyName);
                } else if (parameterObject == null) {
                    // 如果参数对象为空,则参数值也为null
                    value = null;
                } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    // 如果参数对象的类有对应的类型处理器,则直接使用参数对象作为值
                    value = parameterObject;
                } else {
                    // 否则,通过反射获取参数对象的属性值
                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                    value = metaObject.getValue(propertyName);
                }
                // 获取参数映射的类型处理器和JDBC类型
                TypeHandler typeHandler = parameterMapping.getTypeHandler();
                JdbcType jdbcType = parameterMapping.getJdbcType();
                // 如果参数值为null且JDBC类型未设置,则使用配置中定义的默认JDBC类型
                if (value == null && jdbcType == null) {
                    jdbcType = configuration.getJdbcTypeForNull();
                }
                try {
                    // 使用类型处理器设置PreparedStatement的参数值
                    typeHandler.setParameter(ps, i + 1, value, jdbcType);
                } catch (TypeException | SQLException e) {
                    // 如果设置参数时发生异常,则抛出TypeException异常
                    throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
                }
            }
        }
    }
}

这里面最重要的一句其实就是最后一句代码,它的作用是用合适的TypeHandler完成参数的设置。

那么什么是合适的TypeHandler呢,它又是如何决断出来的呢?

BaseStatementHandler的构造方法里有这么一句:

this.boundSql= mappedStatement.getBoundSql(parameterObject);

它触发了sql的解析,在解析sql的过程中,TypeHandler也被决断出来了

决断的原则就是根据参数的类型和参数对应的JDBC类型决定使用哪个TypeHandler

比如:参数类型是String的话就用StringTypeHandler,参数类型是整数的话就用IntegerTypeHandler等

参数设置完毕后,执行数据库操作(update或query)。如果是query最后还有个查询结果的处理过程。

PrepareStatementHandler

接下来,看看StatementHandler 的一个实现类 PreparedStatementHandler(最常用的,封装的是PreparedStatement)

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    // 到此 -> PreparedStatement
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    // 结果交给了ResultSetHandler 去处理
    return resultSetHandler.<E> handleResultSets(ps);
}


// 结果处理使用ResultSetHandler来完成,默认的ResultSetHandler是FastResultSetHandler
// 它在创建StatementHandler时一起创建
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement,  
                                            RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {  
    ResultSetHandler resultSetHandler = mappedStatement.hasNestedResultMaps() ? newNestedResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds): new FastResultSetHandler(executor,mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);  
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);  
    return resultSetHandler;  
}

可以看出ResultSetHandler也是可以被拦截的,可以编写自己的拦截器改变ResultSetHandler的默认行为

ResultSetHandler内部一条记录一条记录的处理,在处理每条记录的每一列时会调用TypeHandler转换结果

/**
 * 自动应用结果集中的列到对象属性
 * 1:遍历所有未映射的列名。
 * 2:对于每个列名,尝试查找对应的对象属性。
 * 3:如果找到了对应的属性,并且这个属性的类型有对应的类型处理器,就从结果集中读取该列的数据,并转换成相应的Java类型。
 * 4:如果读取到的值不为null,则设置到对象的相应属性上,并标记已找到并设置了值。
 * 5:最后返回是否至少有一个值被成功映射。
 *
 * @param rs 结果集
 * @param unmappedColumnNames 未映射的列名列表
 * @param metaObject 元对象,用于访问对象的属性
 * @return 如果至少有一个值被成功映射,则返回true;否则返回false
 * @throws SQLException 如果数据库访问出现错误
 */
protected boolean applyAutomaticMappings(ResultSet rs, List<String> unmappedColumnNames, MetaObject metaObject) throws SQLException {
    boolean foundValues = false;  // 标记是否找到并设置了值
    for (String columnName : unmappedColumnNames) {  // 遍历未映射的列名
        final String property = metaObject.findProperty(columnName);  // 查找对应的属性名
        if (property != null) {  // 如果找到了对应的属性
            final Class<?> propertyType = metaObject.getSetterType(property);  // 获取属性的类型
            if (typeHandlerRegistry.hasTypeHandler(propertyType)) {  // 检查是否有对应类型的类型处理器
                final TypeHandler<?> typeHandler = typeHandlerRegistry.getTypeHandler(propertyType);  // 获取类型处理器
                final Object value = typeHandler.getResult(rs, columnName);  // 从结果集中获取值
                if (value != null) {  // 如果值不为null
                    metaObject.setValue(property, value);  // 设置属性值
                    foundValues = true;  // 标记已找到并设置值
                }
            }
        }
    }
    return foundValues;  // 返回是否找到并设置了值
}

从代码里可以看到,决断TypeHandler使用的是结果参数的属性类型。

因此我们在定义作为结果的对象的属性时一定要考虑与数据库字段类型的兼容性。到此,一次sql的执行流程就完了。