LOB代表大对象数据,包括BLOB和CLOB两种类型。前者用于存储大块的二进制数据,如图片数据,视频数据等(一般应该把文件存储到相应的文件服务器中),后者则用于存储长文本数据。
值得注意的是,不同数据库中,大对象对应的字段类型往往并不相同。
正如之前文章Mybatis源码研究之TypeHandler里已经提及过的,Mybatis构建在对JDBC流程的深刻理解之上。涉及到数据库操作前的参数设置,以及数据库操作完毕结果集的提取的部分都被Mybatis抽象为TypeHandler
接口。
本文中,我们将细化上一篇文章,以实际的BLOB / CLOB操作再次探究一些TypeHandler
的细节。
网上的相关例子都是以POJO为例;所以为了展示本文的必要性,这里我以map类型为例子,给出一个快速的startup。
Mybatis映射文件
"insert_blob" parameterType="map">
INSERT INTO da_affix (
ax_ident,
ax_data
) VALUES (
'123321',
#{blobData, jdbcType=BLOB}
)
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>
<typeHandler handler="mybatis.theory.typehandler.BlobAndFileParameterSetterTypeHandler" jdbcType="BLOB" />
<typeHandler handler="mybatis.theory.typehandler.BlobAndInputStreamParameterSetterTypeHandler" javaType="java.io.FileInputStream" jdbcType="BLOB" />
typeHandlers>
值得注意的以下三点:
补充两个自定义的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);
}
}
Mybatis映射文件
<resultMap type="map" id="blobResultMap">
<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.
标签中的属性设置中,我们使用的是resultMap
来引用我们配置的
,而不是通常的resultType
。
的type属性, 我们设置的依然是map
,这样可以保证最大的兼容性。Mybatis映射文件
Mybatis映射文件
"insert_clob" parameterType="map">
INSERT INTO da_affix (
id,
text
) VALUES (
'123321',
#{clobData, jdbcType=CLOB}
)
Java端调用
// 下面这种方式是不需要第三步的额外配置的
String sqlid = NAMESPACE.concat("insert_clob");
crud.insert(sqlid, Collections.singletonMap("clobData", "LQ~123456"));
System.out.println(count);
Mybatis映射文件
<resultMap type="map" id="clobResultMap">
<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);
首先让我们来看看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
则还是推荐通过
来配置。对于在
配置的映射,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 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 mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
boolean foundValues = false;
final List 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, TypeHandler>> columnHandlers = typeHandlerMap.get(columnName);
if (columnHandlers == null) {
columnHandlers = new HashMap, 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;
}