四、ParameterHandler
1. ParameterHandler 接口方法
ParameterHandler 接口相对比较简单,只有两个方法
public interface ParameterHandler { Object getParameterObject(); void setParameters(PreparedStatement ps) throws SQLException; }
所以看得出来,ParameterHandler 其实就是两个功能,一个是提供入参对象,另一个就是把入参给sql注入进去
2. 实现类核心方法
ParameterHandler 接口只有一个默认实现类 DefaultParameterHandler
我们来看一下,要构建个参数处理器需要些什么内容,来看一下该类的构造方法:
public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { // 由 MyBatis解析xml得出的某个方法对应的sql的信息,包含sql语句 ,参数和结果集的映射 this.mappedStatement = mappedStatement; // MyBatis总配置对象,包含mybatis的所有设置信息 this.configuration = mappedStatement.getConfiguration(); // 类型处理器注册表, 类型处理器是用来处理Java对象和字段类型之间映射,负责将Java类型和JDBC类型相互转换 this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry(); // 方法的入参拼出的对象,实际内部为HashMap this.parameterObject = parameterObject; // 完整的 sql语句,但参数部分尚未装填,由 ? 代替 this.boundSql = boundSql; }
再来看下这所谓的处理器到底是怎么把入参设置进sql内的呢?我们来看看其核心方法 setParameters ,该方法作用是为预处理语句设置参数
@Override public void setParameters(PreparedStatement ps) { ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); // 从boundSql中获取参数映射列表,即方法入参对象 和 sql 中预留的参数位置的映射信息 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); // 检查参数模式是否不是OUT , OUT模式一般用在存储过程,拿参数去接返回值 if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; 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 metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } // 获取类型处理器,主要是根据方法入参类型确定的 TypeHandler typeHandler = parameterMapping.getTypeHandler(); // 获取jdbc类型,由用户在xml中预留参数时指定,如 #{orderdesc , JDBCTYPE = VARCHAR} JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) { // 如果值为null且JDBC类型也为null,则将JDBC类型设置为null值的默认JDBC类型, // ORACLE 出现此情况可能或报错: Error setting null for parameter #XXX with JdbcType OTHER jdbcType = configuration.getJdbcTypeForNull(); } try { // 使用类型处理程序为预处理语句设置参数值 typeHandler.setParameter(ps, i + 1, value, jdbcType); } catch (TypeException | SQLException e) { throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); } } } } }
通过对该方法的分析,其实不难看出来,就是把对方法的入参拆成基础的数据类型,然后替换掉sql里的对应的 " ?" 部分。但是具体是怎么做的呢。我们还得看其调用的一个关键类 TypeHandler 以及这里的 typeHandler.setParameter 方法。
3. TypeHandler 类型处理器
TypeHandler 是 MyBatis 框架的一部分,它是一个接口,用于将 Java 类型和数据库类型之间进行转换。在 MyBatis 中,通过 TypeHandler 将 Java 对象转换为 JDBC 可以处理的数据类型,同时也将查询结果从数据库中的数据类型转换为 Java 类型。
public interface TypeHandler<T> { void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; T getResult(ResultSet rs, String columnName) throws SQLException; T getResult(ResultSet rs, int columnIndex) throws SQLException; T getResult(CallableStatement cs, int columnIndex) throws SQLException; }
可以看出,它不仅有设置参数的能力,而且还能返回结果,即把sql的返回结果,使用对应的java类型展示出来。而且我们先来看一看它的基类 BaseTypeHandler 及核心方法 setParameter
@Override // 将 Java 对象转换为 JDBC 可以处理的数据类型,并设置到 PreparedStatement 对象中 // 把sql里第i个问号注入参数parameter,且要将parameter对象转换为指定的Jdbc类型 public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException { if (parameter == null) { if (jdbcType == null) { throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters."); } try { // 为第i个问号注入null,第二个参数则为jdbc类型的唯一代码,比如FLOAT = 6,DATE = 91等 ps.setNull(i, jdbcType.TYPE_CODE); } catch (SQLException e) { throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . " + "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. " + "Cause: " + e, e); } } else { try { // 如果参数不为空,则需要真正塞值进去,此方法抽象类里没有实现,交由各子类实现 setNonNullParameter(ps, i, parameter, jdbcType); } catch (Exception e) { throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . " + "Try setting a different JdbcType for this parameter or a different configuration property. " + "Cause: " + e, e); } } }
而 BaseTypeHandler 在mybatis3.5.6 内置了四十三种子类,即有四十三种类型处理器,基本覆盖了当前数据库的各大字段类型,当然同时也支持用户自定义 TypeHandler。
我们选择最典型的几种
Double
public void setNonNullParameter(PreparedStatement ps, int i, Double parameter, JdbcType jdbcType) throws SQLException { ps.setDouble(i, parameter); }
Date
public void setNonNullParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType) throws SQLException { ps.setTimestamp(i, new Timestamp(parameter.getTime())); }
String
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter); }
需要注意的是,上面的ps, 已经是来自各数据库的驱动的实现类了,因此可以根据数据库对该字段不同的命名,自行来处理。
4. 实例演示
我们写下这样的代码,Dao层以及SQL文件
// 实体类,构造方法 public User(int id, String username, String password) { this.id = id; this.username = username; this.password = password; }
public boolean addUser(User user) { User user1 = new User(1 ,"zhangsan","123456"); User user2 = new User(2 ,"lisi","123456"); User user3 = new User(3 ,"wangwu","123456"); List<User> list = new ArrayList<>(); list.add(user1); list.add(user2); list.add(user3); return userMapper.addUser(list); }
boolean addUser(@Param("list") List<User> users);
<insert id="addUser" parameterType="com.zhanfu.springboot.demo.entity.User" > insert into user (id,username,password) values <foreach collection="list" item="item"> (#{item.id}, #{item.username}, #{item.password}) </foreach> </insert>
4.1 解析出参数映射关系——ParameterMappings
需要注意的是,因为我们的示例中,含有foreach标签,所以是一段动态sql。而动态sql的逻辑和样式,是在有入参之后才能确定的,所以动态sql解析映射关系是在dao方法真正被调用的时候才开始。而静态sql,会在程序运行时,就构造出ParameterMappings,并存储在StaticSqlSource对象内
像上面这样的Dao方法和Sql,当我们执行Dao方法时,会先把动态条件进行判断好,比如此处列表有三条数据,意味着foreach 就会重复三次,最后解析出来的原生sql 就是
insert into user(id,username,password) values
(#{__frch_item_0.id}, #{__frch_item_0.username},#{__frch_item_0.password})
(#{__frch_item_1.id}, #{__frch_item_1.username},#{__frch_item_1.password})
(#{__frch_item_2.id}, #{__frch_item_2.username},#{__frch_item_2.password})
在通过对这段sql 的 #{} 里面的内容如"__frch_item_0.id",进行分析,结合入参的同名字段的适配,最终会生成一个长度为 9 的映射关系列表,如下图:
这样我们就得到了一个ParameterMappings,注意,这里只是映射关系,就是明确了有9个java对象,对应Sql的九个位置,而真正的参数填充还没开始
4.2 遍历填充参数
现在,我们聚焦到DefaultParameterHandler.setParameter() 方法,关注mybatis是怎么填充参数的。
最终参数的设置,会交由 ClientPreparedStatement 完成,而 ClientPreparedStatement 则是mysql的驱动层了。
五、ResultSetHandler
1. 接口方法
ResultHandler 的接口就是用来处理结果集的,根绝不同的sql分类有三种方法,第一种是最常用的
public interface ResultSetHandler { // 处理 ResultSet 的结果,将其转换成一个 List<java对象> 并返回 ———— 最常用 <E> List<E> handleResultSets(Statement stmt) throws SQLException; // 处理Cursor游标的的结果 <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException; // 处理存储过程等带回调参数的情况 void handleOutputParameters(CallableStatement cs) throws SQLException; }
2. 实现功能讲解
MyBatis中提供了一个默认的ResultSetHandler实现
这个类比较大,我们直接看其核心方法,正是将结果集一行行遍历,然后针对指定的出参java类型,使用构造函数构造完后,往里面填入映射的值。
// public class DefaultResultSetHandler implements ResultSetHandler private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { DefaultResultContext<Object> resultContext = new DefaultResultContext<>(); ResultSet resultSet = rsw.getResultSet(); skipRows(resultSet, rowBounds); while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) { ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null); // 循环从结果集中获取数据,注意此时的 rowValue 已经是业务对象了 Object rowValue = getRowValue(rsw, discriminatedResultMap, null); // 储存该业务对象 storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); } } private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException { if (parentMapping != null) { linkToParents(rs, parentMapping, rowValue); } else { // 调用 resultHandler 存储本行结果 callResultHandler(resultHandler, resultContext, rowValue); } }
3. ResultSetHandler 与 ResultHandler 的联系
ResultHandler 其实是一个储存用的接口,它有两个实现类
public interface ResultHandler<T> { void handleResult(ResultContext<? extends T> resultContext); }
- DefaultResultHandler:默认的ResultHandler实现类。它将查询结果存储在一个List中
- MapResultHandler:将查询结果转换为Map类型的ResultHandler实现类。它将每条记录转换为一个Map,其中Map的key是列名,value是列的值
这两个类的逻辑非常简单,两个类分别维护了一个List 和 Map,将入参的resultContext内的值解析,存入各自的集合里即可。
需要注意的是,ResultHandler 的入参resultContext仅代表一行数据,真正的返回值可能是多行的,所以 ResultHandler 其实是在for循环中,一行行解析和转换的,而负责处理多行的结果处理器是ResultSetHandler,ResultSet在数据量较大时,会占用较大的内存,而ResultHandler可以将查询结果逐条处理,避免了占用大量内存的问题
所以,ResultSetHandler主要用于将查询结果转换成Java对象;而ResultHandler主要用于对查询结果以某种形式展现。它们的使用场景是不同的
六、总结
ParameterHandler 负责翻译,把java对象的值,翻译进sql的指定位置;
ResultSetHandler 则是一个讲解员,把SQL的结果集按框架搭建出来,再汇报给上级;
StatementHandler则是一个部门经理,它不仅管理着前两者,还能创建statement(即存储着sql的对象),并指挥翻译把入参翻译进statement,然后调用驱动执行statement,最后的结果指挥讲解员把结果以特定格式展示出来;
Executor则是个公司老板,位置更高,不再执行那些基础的工作,而是负责招聘部门经理(创建StatementHandler),并一键通知经理做事,而他自己的主要职责则是与其他公司搞关系(获取数据库连接、事务的提交回滚),调度仓储(缓存)