简介
在上几篇中,介绍了 MyBatis3 对参数和结果的解析转换,对于常规数据类型,默认的处理已经足够应付了,但日常开发中会有一些特殊的类型,就可以通过 TypeHandler 来进行处理
示例准备
本篇文中用于调试的测试代码请参考:MyBatis3源码解析(1)探索准备
完整的工程已放到 GitHub 上:https://github.com/lw1243925457/MybatisDemo/tree/master/example
熟悉使用的可以跳过该部分,直接到源码解析部分
数据模型修改
我们先将原来的 String 类型变成 String 数组,存入数据库时,使用逗号进行分割,取出时转成 String 数组,即
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public 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 也能做到心中有数
评论