LOB代表大对象数据,包括BLOB和CLOB两种类型。前者用于存储大块的二进制数据,如图片数据,视频数据等(一般应该把文件存储到相应的文件服务器中),后者则用于存储长文本数据。
值得注意的是,不同数据库中,大对象对应的字段类型往往并不相同。
1. 概述
正如之前文章Mybatis源码研究之TypeHandler里已经提及过的,Mybatis构建在对JDBC流程的深刻理解之上。涉及到数据库操作前的参数设置,以及数据库操作完毕结果集的提取的部分都被Mybatis抽象为TypeHandler
接口。
本文中,我们将细化上一篇文章,以实际的BLOB / CLOB操作再次探究一些TypeHandler
的细节。
2. BLOB操作
网上的相关例子都是以POJO为例;所以为了展示本文的必要性,这里我以map类型为例子,给出一个快速的startup。
2.1 BLOB操作之存储
Mybatis映射文件
<insert id="insert_blob" parameterType="map"> INSERT INTO da_affix ( ax_ident, ax_data ) VALUES ( '123321', #{blobData, jdbcType=BLOB} ) </insert>
Java端调用
// 下面这种方式是不需要第三步的额外配置的 String sqlid = NAMESPACE.concat("insert_blob"); crud.insert(sqlid, Collections.singletonMap("blobData", FileUtil.readBytes(new File("D:/1.txt")))); // 以下方法和上面的方法等价, 区别只是传入参数的差别, 需要下面第三步配置的支持 // 以下两行代码模拟了工程中的不同参数类型 Object blobDataParam = new File("D:/1.txt"); // Object blobDataParam = FileUtils.openInputStream(new File("D:/1.txt")); crud.insert(sqlid, Collections.singletonMap("blobData", blobDataParam));
额外配置
为了减少调用层的重复性代码(将流/文件转换为byte[],这步操作可能因为程序员的经历而选择不同的实现方式),我们特意加入了如下Mybatis的TypeHandler。<typeHandlers> <!--注意这里的jdbcType配置, 其值来源是 org.apache.ibatis.type.JdbcType枚举项 --> <typeHandler handler="mybatis.theory.typehandler.BlobAndFileParameterSetterTypeHandler" jdbcType="BLOB" /> <typeHandler handler="mybatis.theory.typehandler.BlobAndInputStreamParameterSetterTypeHandler" javaType="java.io.FileInputStream" jdbcType="BLOB" /> </typeHandlers>
值得注意的以下三点:
- 因为Mybatis在挑选typeHandler时, 是根据传入参数的实际类型, 比如这里的FileInputStream, 而非我们在定义时的InputStream; 所以如果有其他InputStream子类需求, 我们需要在这里额外再注册一次
- 为了防止二义性, 我在这里进行了完全约束, 同时约定了javaType, jdbcType。
- 我们在这里注册的两个typeHandler,只有对数据库操作前的参数处理,对于操作完毕的返回值直接注明不支持。
补充两个自定义的typehandler
// ----------------------------------------- BlobAndFileParameterSetterTypeHandler /** * 执行Mybatis操作时, 数据库操作前的参数设置, 如果传入参数类型为File, 将调用本TypeHandler * @author LQ * */ public class BlobAndFileParameterSetterTypeHandler extends BaseTypeHandler<File> { private static final String ERROR_INFO = "本类只负责参数设置部分, BLOB提取参见本人博客"; @Override public File getNullableResult(ResultSet rs, String columnName) throws SQLException { throw new UnsupportedOperationException(ERROR_INFO); } @Override public File getNullableResult(ResultSet rs, int columnIndex) throws SQLException { throw new UnsupportedOperationException(ERROR_INFO); } @Override public File getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { throw new UnsupportedOperationException(ERROR_INFO); } @Override public void setNonNullParameter(PreparedStatement ps, int i, File parameter, JdbcType jdbcType) throws SQLException { ByteArrayInputStream bis; try { bis = new ByteArrayInputStream(FileUtils.readFileToByteArray(parameter)); } catch (IOException e) { throw new RuntimeException(e); } ps.setBinaryStream(i, bis); } } // ----------------------------------------- BlobAndInputStreamParameterSetterTypeHandler /** * 执行Mybatis操作时, 数据库操作前的参数设置, 如果传入参数类型为Inputstream, 将调用本TypeHandler * @author LQ * */ public class BlobAndInputStreamParameterSetterTypeHandler extends BaseTypeHandler<InputStream> { private static final String ERROR_INFO = "本类只负责参数设置部分, BLOB提取参见本人博客"; @Override public InputStream getNullableResult(ResultSet rs, String columnName) throws SQLException { throw new UnsupportedOperationException(ERROR_INFO); } @Override public InputStream getNullableResult(ResultSet rs, int columnIndex) throws SQLException { throw new UnsupportedOperationException(ERROR_INFO); } @Override public InputStream getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { throw new UnsupportedOperationException(ERROR_INFO); } @Override public void setNonNullParameter(PreparedStatement ps, int i, InputStream parameter, JdbcType jdbcType) throws SQLException { ps.setBinaryStream(i, parameter); } }
2.2 BLOB操作之读取
Mybatis映射文件
<!-- 映射文件--> <resultMap type="map" id="blobResultMap"> <!--为了以示区分, 特意将property配置为和column不一样 --> <result property="AX_D" column="AX_DATA" jdbcType="BLOB" javaType = "_byte[]"/> </resultMap> <select id="select_blob" parameterType="map" resultMap ="blobResultMap"> SELECT ax_data, ax_ident FROM affix WHERE ax_ident = '123321' </select>
Java端调用
String sqlid =NAMESPACE.concat("select_blob"); Map<String, Object> resultMap = crud.<Map<String, Object>>selectOne(sqlid, null); byte[] ax_d = (byte[])resultMap.get("AX_D"); System.out.println(new String(convert));
- 这里我需要提醒如下几点:
- 查询的SQL语句中, 是针对两个字段; 但在resultMap的映射配置中, 我们只设置了一个,也就是只配置了特殊情况——BLOB映射,其他字段的匹配最终还是交给了Mybatis自主完成。
<select>
标签中的属性设置中,我们使用的是resultMap
来引用我们配置的<resultMap>
,而不是通常的resultType
。<resultMap>
的type属性, 我们设置的依然是map
,这样可以保证最大的兼容性。
3. CLOB操作
3.1 CLOB操作之存储
Mybatis映射文件
Mybatis映射文件
<insert id="insert_clob" parameterType="map"> INSERT INTO da_affix ( id, text ) VALUES ( '123321', #{clobData, jdbcType=CLOB} ) </insert>
Java端调用
// 下面这种方式是不需要第三步的额外配置的 String sqlid = NAMESPACE.concat("insert_clob"); crud.insert(sqlid, Collections.singletonMap("clobData", "LQ~123456")); System.out.println(count);
3.2 CLOB操作之读取
Mybatis映射文件
<resultMap type="map" id="clobResultMap"> <!-- 只需要对CLOB类型进行特殊配置, 其他类型依然交给Mybatis自主决定 --> <result property="TEXT" column="TEXT" jdbcType="CLOB" javaType = "java.lang.String" /> </resultMap> <select id="clob_select_clob" parameterType="map" resultMap ="clobResultMap"> SELECT w.id id ,w.text text FROM mh_nr_wz w WHERE w.id = '24340774353A4A758785812DE492EFC5' </select>
Java端调用
String sqlid =NAMESPACE.concat("clob_select_clob"); Map<String, Object> resultMap = crud.<Map<String, Object>>selectOne(sqlid, null); System.out.println(resultMap);
4. 底层细节
首先让我们来看看TypeHandler
容器TypeHandlerRegistry
中针对BLOB / CLOB字段所注册的默认TypeHandler
。
// 以下这段代码出现在 TypeHandlerRegistry的唯一构造函数中。
//--------------- BlobTypeHandler
register(Byte[].class, JdbcType.BLOB, new BlobByteObjectArrayTypeHandler());
register(byte[].class, JdbcType.BLOB, new BlobTypeHandler());
register(Byte[].class, new ByteObjectArrayTypeHandler());
register(Byte[].class, JdbcType.BLOB, new BlobByteObjectArrayTypeHandler());
register(Byte[].class, JdbcType.LONGVARBINARY, new BlobByteObjectArrayTypeHandler());
register(byte[].class, new ByteArrayTypeHandler());
register(byte[].class, JdbcType.BLOB, new BlobTypeHandler());
register(byte[].class, JdbcType.LONGVARBINARY, new BlobTypeHandler());
register(JdbcType.LONGVARBINARY, new BlobTypeHandler());
register(JdbcType.BLOB, new BlobTypeHandler());
//--------------- ClobTypeHandler
register(String.class, JdbcType.CLOB, new ClobTypeHandler());
register(String.class, JdbcType.LONGVARCHAR, new ClobTypeHandler());
register(JdbcType.CLOB, new ClobTypeHandler());
register(JdbcType.LONGVARCHAR, new ClobTypeHandler());
register(JdbcType.NCLOB, new NClobTypeHandler());
register(String.class, JdbcType.NCLOB, new NClobTypeHandler());
所以,Mybatis在其默认的类型处理器中
1. 针对Blob
,已经为我们提供了BlobTypeHandler
和BlobByteObjectArrayTypeHandler
。其中最常用的是BlobTypeHandler,而BlobByteObjectArrayTypeHandler是用于数据库兼容性的,并不常用。
2. 针对Clob
,则是提供了ClobTypeHandler
。
按照Mybatis目前的最佳实践,对于paramter,已经不再推荐程序员在配置时直接配置parameterMap参数, 而是推荐通过 #{start,jdbcType=INTEGER}
配置来让Mybatis在内部自动构建相对应的ParameterMapping
。
而resultMap
则还是推荐通过<resultMap>
来配置。对于在<resultMap>
配置的映射,DefaultResultSetHandler
中分别定义了私有方法 applyAutomaticMappings
和applyPropertyMappings
来处理未声明映射和显式声明映射的处理。
// -------------------------------------------- DefaultResultSetHandler.getRowValue
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
// 根据用户在xml中配置resultType等要素构建一个相应的返回值类型实例
// 例如我们一般会指定 resultType ="map", 于是这里的resultObject将是一个size=0的 HashMap实例
Object resultObject = createResultObject(rsw, resultMap, lazyLoader, null);
if (resultObject != null && !typeHandlerRegistry.hasTypeHandler(resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(resultObject);
boolean foundValues = resultMap.getConstructorResultMappings().size() > 0;
if (shouldApplyAutomaticMappings(resultMap, !AutoMappingBehavior.NONE.equals(configuration.getAutoMappingBehavior()))) {
//
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
}
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
resultObject = foundValues ? resultObject : null;
return resultObject;
}
return resultObject;
}
// -------------------------------------------- DefaultResultSetHandler.applyAutomaticMappings
// 未配置的java字段和数据库字段之间进行的自动映射
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
// 是否有未配置的 映射, 就像我们上面配置的那种情况, 这里的unmappedColumnNames 就是 [AX_IDENT]
final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
boolean foundValues = false;
for (String columnName : unmappedColumnNames) {
String propertyName = columnName;
if (columnPrefix != null && columnPrefix.length() > 0) {
// When columnPrefix is specified,
// ignore columns without the prefix.
if (columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
propertyName = columnName.substring(columnPrefix.length());
} else {
continue;
}
}
// 我们这里 metaObject 的底层对象是一个空的HashMap实例
final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());
if (property != null && metaObject.hasSetter(property)) {
// 这里最终调用 org.apache.ibatis.reflection.wrapper.MapWrapper.getSetterType; 所以这里的propertyType 其实就是Object的类型
final Class<?> propertyType = metaObject.getSetterType(property);
if (typeHandlerRegistry.hasTypeHandler(propertyType)) {
// StringTypeHandler
final TypeHandler<?> typeHandler = rsw.getTypeHandler(propertyType, columnName);
final Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
if (value != null || configuration.isCallSettersOnNulls()) { // issue #377, call setter on nulls
if (value != null || !propertyType.isPrimitive()) {
metaObject.setValue(property, value);
}
foundValues = true;
}
}
}
}
return foundValues;
}
// -------------------------------------------- DefaultResultSetHandler.applyPropertyMappings
// 针对配置了的映射, 进行处理
private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
// 针对上面的配置, 这里 mappedColumnNames 则是 [AX_DATA], 而不是 [AX_D]; 所以这里获取到的是 数据库字段名
final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
boolean foundValues = false;
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
if (propertyMapping.isCompositeResult()
|| (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))
|| propertyMapping.getResultSet() != null) {
Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
final String property = propertyMapping.getProperty(); // issue #541 make property optional
if (value != NO_VALUE && property != null && (value != null || configuration.isCallSettersOnNulls())) { // issue #377, call setter on nulls
if (value != null || !metaObject.getSetterType(property).isPrimitive()) {
metaObject.setValue(property, value);
}
foundValues = true;
}
}
}
return foundValues;
}
最终确定TypeHandler的实际类型的工作被放到了ResultSetWrapper
中。
// ResultSetWrapper.getTypeHandler
public TypeHandler<?> getTypeHandler(Class<?> propertyType, String columnName) {
TypeHandler<?> handler = null;
Map<Class<?>, TypeHandler<?>> columnHandlers = typeHandlerMap.get(columnName);
if (columnHandlers == null) {
columnHandlers = new HashMap<Class<?>, TypeHandler<?>>();
typeHandlerMap.put(columnName, columnHandlers);
} else {
handler = columnHandlers.get(propertyType);
}
if (handler == null) {
handler = typeHandlerRegistry.getTypeHandler(propertyType);
// Replicate logic of UnknownTypeHandler#resolveTypeHandler
// See issue #59 comment 10
if (handler == null || handler instanceof UnknownTypeHandler) {
final int index = columnNames.indexOf(columnName);
final JdbcType jdbcType = jdbcTypes.get(index);
//classNames 是根据数据库查询得到的返回值, 每个值的类型, 例如 [oracle.sql.BLOB, java.lang.String]
// 类级别字段classNames 是在ResultSetWrapper构造函数中完成赋值的
final Class<?> javaType = resolveClass(classNames.get(index));
if (javaType != null && jdbcType != null) {
handler = typeHandlerRegistry.getTypeHandler(javaType, jdbcType);
} else if (javaType != null) {
// 先用javaType来取typeHandler
handler = typeHandlerRegistry.getTypeHandler(javaType);
} else if (jdbcType != null) {
handler = typeHandlerRegistry.getTypeHandler(jdbcType);
}
}
if (handler == null || handler instanceof UnknownTypeHandler) {
// 一番努力之后, 还是没找到
handler = new ObjectTypeHandler();
}
columnHandlers.put(propertyType, handler);
}
return handler;
}
5. Links
- 《深入浅出MyBatis技术原理与实战》 第九章 - 9.1小节
- 《精通Spring4.x》 P444
- mybatis的BLOB存储与读取