简介
在上几篇中,介绍了 MyBatis3 对参数和结果的解析转换,对于常规数据类型,默认的处理已经足够应付了,但日常开发中会有一些特殊的类型,就可以通过 TypeHandler 来进行处理
示例准备
本篇文中用于调试的测试代码请参考:MyBatis3源码解析(1)探索准备
完整的工程已放到 GitHub 上:https://github.com/lw1243925457/MybatisDemo/tree/master/example
熟悉使用的可以跳过该部分,直接到源码解析部分
数据模型修改
我们先将原来的 String 类型变成 String 数组,存入数据库时,使用逗号进行分割,取出时转成 String 数组,即
@Builder@Data@AllArgsConstructor@NoArgsConstructorpublic class Person {
private Long id; private String[] name;}
复制代码
自定义 TypeHandler 编写
我们修改完数据结构后,需要编写一个自定义的 TypeHandler,如下:
public class StringArrayTypeHandler extends BaseTypeHandler<String[]> {
@Override public void setNonNullParameter(PreparedStatement preparedStatement, int i, String[] strings, JdbcType jdbcType) throws SQLException { preparedStatement.setString(i, StringUtils.join(strings, ",")); }
@Override public String[] getNullableResult(ResultSet resultSet, String s) throws SQLException { return convert(resultSet.getString(s)); }
@Override public String[] getNullableResult(ResultSet resultSet, int i) throws SQLException { return convert(resultSet.getString(i)); }
@Override public String[] getNullableResult(CallableStatement callableStatement, int i) throws SQLException { return convert(callableStatement.getString(i)); }
/** * 将查询值转换为数组 * * @param value 查询值, String * @return 转换结果, String[] */ private String[] convert(String value) { return StringUtils.isEmpty(value) ? new String[0] : value.split(","); }}
复制代码
TypeHandler 注册
将我们自定义的 TypeHandler 进行注册,MyBatis 后将对应 String【】和 Varchar 使用此 TypeHandler
public class MybatisTest { public static SqlSessionFactory buildSqlSessionFactory() { ...... Configuration configuration = new Configuration(environment); // 注册TypeHandler,注意需要在添加Mapper之前,因为添加Mapper操作的时候就需要TypeHandler了 configuration.getTypeHandlerRegistry().register(String[].class, JdbcType.VARCHAR, StringArrayTypeHandler.class); configuration.addMapper(PersonMapper.class); SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); return builder.build(configuration); }}
复制代码
运行
测试结果如下:
Connected to the target VM, address: '127.0.0.1:53557', transport: 'socket'[Person(id=1, name=[1, 2]), Person(id=2, name=[1, 2])]Disconnected from the target VM, address: '127.0.0.1:53557', transport: 'socket'
复制代码
源码解析
参数解析设置
通过前面几篇文章的分析,我们得到设置参数的相关源码如下:
public class DefaultParameterHandler implements ParameterHandler { @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)) { // 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 typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) { 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); } } } } }}
复制代码
设置值,函数如下:
public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> { @Override 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 { 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 { // 调用我们自定义的TypeHandler函数 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); } } }}
复制代码
如上所示,我们看到 TypeHandler 调用的大致流程
其中 MyBatis3 是根据 Java Type 和 JDBC Type 来对应调用不同的 TypeHandler 的,在示例代码中我们也显式的注册了 String 数组和 Varchar
这里使用的是一个全局的,也可以单独在查询中使用,可以自行搜索
结果解析返回
通过之前的文章,我们知道代码如下:
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); } else if (propertyMapping.getResultSet() != null) { addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK? return DEFERRED; } else { // 得到对应的TypeHandler,直接返回处理结果 final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler(); final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix); return typeHandler.getResult(rs, column); } }
复制代码
GetResult 函数如下,后面调用我们自定义的 TypeHandler 方法
@Override public T getResult(ResultSet rs, String columnName) throws SQLException { try { return getNullableResult(rs, columnName); } catch (Exception e) { throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set. Cause: " + e, e); } }
复制代码
总结
本篇文章,展示了如何定义 TypeHandler 和使用 TypeHandler,并展示了源码部分,TypeHandler 是如何对应生效的,了解该部分,在日常开发中,使用 TypeHandler 也能做到心中有数
评论