TypeHandler是Mybatis中Java对象和数据库JDBC之间进行类型转换的桥梁
是Mybatis内部的一个接口,实现它就可以完成Java对象到数据库之间的转换
内部结构如下:
public interface TypeHandler<T> {
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
/**
* @param columnName Colunm name, when configuration useColumnLabel
is false
*/
T getResult(ResultSet rs, String columnName) throws SQLException;
T getResult(ResultSet rs, int columnIndex) throws SQLException;
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
第一个方法是从Java对象到数据库的转换,后面三个的是从数据库到Java对象的转换
直接实现TypeHandler接口
Mybatis中有一个抽象类BaseTypeHandler,实现了TypeHandler并进行了扩展
采用直接继承BaseTypeHandler
有一张数据库表,其中有一个details字段为json类型,其DDL为
CREATE TABLE `test` (
`id` int NOT NULL AUTO_INCREMENT,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`details` json NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
将Java对象转换成数据库中json字段, 通过实现自定义的TypeHandler,完成对test表的查询和插入
首先定义JavaBean
@Data
public class TestDO {
private int id;
private LocalDateTime createTime;
private PersonDO personDO;
}
@Data
public class PersonDO {
private String name;
private int age;
}
TestDO对应test数据库表,PersonDO对应着数据库中的details字段
接下来继承BaseTypeHandler
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes({PersonDO.class})
public class ObjectJSONTypeHandler extends BaseTypeHandler<PersonDO> {
private Gson gson = new Gson();
@Override
public void setNonNullParameter(PreparedStatement ps, int i, PersonDO parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, gson.toJson(parameter));
}
@Override
public PersonDO getNullableResult(ResultSet rs, String columnName) throws SQLException {
return gson.fromJson(rs.getString(columnName), PersonDO.class);
}
@Override
public PersonDO getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return gson.fromJson(rs.getString(columnIndex), PersonDO.class);
}
@Override
public PersonDO getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return gson.fromJson(cs.getString(columnIndex), PersonDO.class);
}
}
注意:
剩下的,就是在mapper文件中查询和插入时,指定要使用的typeHandler
<mapper namespace="com.demoxxx.TestMapper" >
<resultMap id="resultMap" type="com.demo.xxx.TestDO">
<id column="id" property="id"/>
<result column="create_time" property="createTime"/>
<result column="details" property="personDO" typeHandler="com.demo.xxx.ObjectJSONTypeHandler"/>
resultMap>
<select id="selectById" parameterType="int" resultMap="resultMap">
select id,create_time,details
from test
where id=#{param1};
select>
<insert id="insert">
insert into test(create_time,details) values (#{createTime},#{personDO, typeHandler=com.demo.xxx.ObjectJSONTypeHandler})
insert>
mapper>
或者,不在
mybatis:
type-handlers-package: com.xxx.typehandler
区别在于如果写在配置文件中,任何使用PersonDO的地方都会进行转换,写在mapper中,只有对应的SQL会进行转换
当在yml中指定TypeHandler时,它的在于SqlSessionFactoryBean中进行,具体为其中的buildSqlSessionFactory方法
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
final Configuration targetConfiguration;
XMLConfigBuilder xmlConfigBuilder = null;
...省略
if (!isEmpty(this.plugins)) {
Stream.of(this.plugins).forEach(plugin -> {
targetConfiguration.addInterceptor(plugin);
LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
});
}
if (hasLength(this.typeHandlersPackage)) {
scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass())
.filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
.filter(clazz -> ClassUtils.getConstructorIfAvailable(clazz) != null)
.forEach(targetConfiguration.getTypeHandlerRegistry()::register);
}
if (!isEmpty(this.typeHandlers)) {
Stream.of(this.typeHandlers).forEach(typeHandler -> {
targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");
});
}
...省略
return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}
scanClasses方法进行加载自定义的typeHandler
在Mapper中自定义时,依然也是在SqlSessionFactoryBean的buildSqlSessionFactory方法,不过是在scanClasses下面,毕竟没在配置文件中配置TypeHandler
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
final Configuration targetConfiguration;
XMLConfigBuilder xmlConfigBuilder = null;
...省略
if (this.mapperLocations != null) {
if (this.mapperLocations.length == 0) {
LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
} else {
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}
LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
}
}
} else {
LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
}
... 省略
return this.sqlSessionFactoryBuilder.build(targetConfiguration);
这里会扫描mapper文件,扫描完后就会进入xmlMapperBuilder.parse()中解析
//XmlMapperBuilder
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
具体的解析"/mapper"元素
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
可以看到有resultMap的解析,有insert的解析,这里只看resultMap的解析
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
...省略
for (XNode resultChild : resultChildren) {
if ("constructor".equals(resultChild.getName())) {
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
List<ResultFlag> flags = new ArrayList<>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
省略...
}
重点在于buildResultMappingFromContext方法中
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
String property;
if (flags.contains(ResultFlag.CONSTRUCTOR)) {
property = context.getStringAttribute("name");
} else {
property = context.getStringAttribute("property");
}
String column = context.getStringAttribute("column");
String javaType = context.getStringAttribute("javaType");
String jdbcType = context.getStringAttribute("jdbcType");
String nestedSelect = context.getStringAttribute("select");
String nestedResultMap = context.getStringAttribute("resultMap",
processNestedResultMappings(context, Collections.emptyList(), resultType));
String notNullColumn = context.getStringAttribute("notNullColumn");
String columnPrefix = context.getStringAttribute("columnPrefix");
String typeHandler = context.getStringAttribute("typeHandler");
String resultSet = context.getStringAttribute("resultSet");
String foreignColumn = context.getStringAttribute("foreignColumn");
boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
Class<?> javaTypeClass = resolveClass(javaType);
Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}
可以看到其中String typeHandler = context.getStringAttribute(“typeHandler”);
在这里完成了Typehandler的加载
总结一下的话