Mybatis源码解析--ParameterHandler
背景
在Mybatis源码解析--StatementHandler中我们知道StatementHandler是对JDBC中Statement
功能的封装,而针对3种Statement
提供了对应的StatementHandler
,而StatementHandler
将Statement
的参数设置和结果集的解析逻辑分别交给ParameterHandler
和ResultSetHandler
进行处理,本篇就来研究ParameterHandler
的内部实现。
ParameterHandler的创建
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; }
ParameterHandler与JDBC
JDBC方式实现参数设置
首先使用JDBC提供的接口实现与PraneterHandler相同的功能,伪代码如下:
public class StudentMapper { public int insertStudent(Student student) { //构造sql语句 String sql = "insert into student (n_id, c_name, n_age, c_school) values (?, ?, ?, ?)"; //获取PrepareStatement对象 PrepareStatement statement = connection.prepareStatement(sql); //设置参数 statement.setInt(1, student.getId()); statement.setString(2, student.getName()); statement.setInt(3, student.getAge()); statement.setString(4, student.getStudent()); //省略执行语句、结果集的解析、资源释放等操作 } }
首先需要一个SQL语句模板,接着需要创建
PrepareStatement
对象对sql进行预编译,最后根据sql语句的占位符位置设置对应参数。可以分析出有以下几个必须参数:
SQL语句模板:要执行的sql语句模板,需要将占位符替换为真实参数才能执行
PrepareStatement:对sql进行预编译以及提供了替换SQL占位符的API
Student:提供真正参数的对象
statement.setInt(1, student.getAge()):根据占位符的下标替换真实参数(占位符与真实参数的映射关系)
由于SQL的预编译是在StatementHandler类中完成的,请参考Mybatis源码解析--StatementHandler,本篇只关注对PrepareStatement的赋值(也就是后3个参数)
ParameterHandler方式实现参数设置
ParameterHandler接口定义
ParameterHandler
的职责就是对ParameterStatement
设置参数,我们就先来看一下它对这些操作的抽象
public interface ParameterHandler { //用来获取封装真实入参的对象 Object getParameterObject(); //PreParedStatement参数替换的逻辑实现 void setParameters(PreparedStatement ps) throws SQLException; }
与JDBC实现对比,已经有2个重要参数出现,还差占位符与真实参数的映射关系,通过
ParameterHandler
提供的方法名称可以猜测核心逻辑肯定是在setParameters(PreparedStatement ps)
方法中,Mybatis框架提供了一个默认实现类DefaultParameterHandler
,那就来看一下DefaultParameterHandler#setParameters
的具体实现
public void setParameters(PreparedStatement ps) { List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); // 对每个ParameterMapping进行处理,此处先省略内部具体实现 } } }
可以看到先从
boundSql
对象中获取List<ParameterMapping>
,然后对其进行遍历,逐个处理,这个和JDBC中追个替换占位符逻辑类似。那ParameterMapping
该类提供了什么功能呢?我们跟进去研究研究
ParameterMapping类
public class ParameterMapping { private Configuration configuration; // java对象的属性名 private String property; private ParameterMode mode; // java对象该属性的类型 private Class<?> javaType = Object.class; // 数据库类型 private JdbcType jdbcType; private Integer numericScale; // 用于java对象和数据库表记录之间的转换 private TypeHandler<?> typeHandler; private String resultMapId; private String jdbcTypeName; private String expression; // 建造器和getter方法省略 }
ParameterMapping
类中定义了一堆属性,除此之外使用建造者模式提供了一个建造器(作用:简化ParameterMapping
的创建和关键属性的校验)以及获取这些属性的getter方法。可以将以上的属性抽象成下面这张图:
首先Mybatis是一个ORM框架,ORM框架的出现是因为JAVA和数据库各自都有一套类型的定义,它两之间要进行交互,就需要对所有类型建立一种映射关系。而
ParameterMapping
类就是对这种映射关系的封装。将转换的逻辑交给TypeHandler
进行处理,Mybatis在type子包下定义了很多TypeHandler的实现类用来实现JAVA类型与数据库类型的相互转换。说了这么多好像和上面JDBC的关系映射没有多大关系,使用JDBC时直接在SQL语句中使用?
占位符来代替真实属性进行预编译,然后根据占位符的下标替换为对应的真实属性,最后执行获取结果;而MyBatis中我们是如何定义SQL的呢?
PrepareHandler设置参数原理
下图是对上图分析的印证
由此可见
BoundSql
封装了一次SQL执行所需的所有数据,通过上面分析可以得知Mybatis通过自定义占位符#{}
里面的字符串可以与真实对象的属性建立某种映射,并将这些映射关系保存到List<ParameterMapping>
中,然后再将#{}
替换成JDBC的?
进行预编译,此时?
与List<ParameterHandler>
是一一对应的;Mybaits通过List<ParameterHandler>
的对应顺序就知道对应?
应该获取真实对象的哪个属性。到此上面的所有疑问都解决了,下面我们就看具体对PrepareStatement
的赋值操作
ParameterHandler参数设置
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 value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { // 如果该参数有对应的TypeHandler,直接交给TypeHandler转换 value = parameterObject; } else { // 将真实入参封装成MetaObject MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); // 处理null if (value == null && jdbcType == null) { jdbcType = configuration.getJdbcTypeForNull(); } try { // 调用TypeHandler对PrepareStatement进行参数设置 typeHandler.setParameter(ps, i + 1, value, jdbcType); } catch (TypeException | SQLException e) { throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); } } }
核心逻辑就是获取属性的值,最后交给TypeHandler对象来对PrepareStatement进行参数设置。获取属性值有以下几个分支:
BoundSql指定了该属性的值,则value = boundSql.getAdditionalParameter(propertyName)
没有入参(parameterObject == null),则value = null
TypeHandlerRegistry中有处理该入参的TypeHandler,则value = 入参
其他情况,则需要将入参包装成
MetaObject
对象,MetaObject提供了工具类辅助获取属性值根据不同情况获取到属性的值,最终交给TypeHandler进行真正的参数设置
ParameterHandler参数设置流程图如下
对于前3种情况很好理解,不再展开分析,其实大多数情况都是走的最后一种,所以下面我们详细分析MetaObject方式是如何获取属性值的。
对于TypeHandler是如何进行具体参数设置的,请查看Mybatis源码分析--TypeHandler
属性值的获取
MetaObject类
public class MetaObject { // 该对象是SQL所需的数据,可以是map,集合,bean实体对象 private final Object originalObject; // 根据originalObject的真实类型,将其交给对应的Wrapper来处理 private final ObjectWrapper objectWrapper; private final ObjectFactory objectFactory; private final ObjectWrapperFactory objectWrapperFactory; private final ReflectorFactory reflectorFactory; /** * 私有构造方法 */ private MetaObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) { this.originalObject = object; this.objectFactory = objectFactory; this.objectWrapperFactory = objectWrapperFactory; this.reflectorFactory = reflectorFactory; /** * 根据object的真实类型 创建对应的包装类 * 此处的object是SqlSession中相关API传入的object参数,是最终替换SQL的入参 * {@link org.apache.ibatis.session.SqlSession#insert(String, Object)} * {@link org.apache.ibatis.session.SqlSession#update(String, Object)} * {@link org.apache.ibatis.session.SqlSession#delete(String, Object)} * {@link org.apache.ibatis.session.SqlSession#select(String, Object, RowBounds, ResultHandler)}} */ if (object instanceof ObjectWrapper) { this.objectWrapper = (ObjectWrapper) object; } else if (objectWrapperFactory.hasWrapperFor(object)) { this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object); } else if (object instanceof Map) { this.objectWrapper = new MapWrapper(this, (Map) object); } else if (object instanceof Collection) { this.objectWrapper = new CollectionWrapper(this, (Collection) object); } else { this.objectWrapper = new BeanWrapper(this, object); } } }