Mybatis技巧之LOB对象处理

LOB代表大对象数据,包括BLOB和CLOB两种类型。前者用于存储大块的二进制数据,如图片数据,视频数据等(一般应该把文件存储到相应的文件服务器中),后者则用于存储长文本数据。
值得注意的是,不同数据库中,大对象对应的字段类型往往并不相同。

1. 概述

正如之前文章Mybatis源码研究之TypeHandler里已经提及过的,Mybatis构建在对JDBC流程的深刻理解之上。涉及到数据库操作前的参数设置,以及数据库操作完毕结果集的提取的部分都被Mybatis抽象为TypeHandler接口。

本文中,我们将细化上一篇文章,以实际的BLOB / CLOB操作再次探究一些TypeHandler的细节。

2. BLOB操作

网上的相关例子都是以POJO为例;所以为了展示本文的必要性,这里我以map类型为例子,给出一个快速的startup。

2.1 BLOB操作之存储
  1. Mybatis映射文件

    <insert id="insert_blob" parameterType="map">               
        INSERT INTO  da_affix (                 
                 ax_ident, 
                 ax_data 
            ) VALUES ( 
                '123321', 
                #{blobData, jdbcType=BLOB}
        )
    </insert>
  2. 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));
    
  3. 额外配置
    为了减少调用层的重复性代码(将流/文件转换为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>

    值得注意的以下三点:

    1. 因为Mybatis在挑选typeHandler时, 是根据传入参数的实际类型, 比如这里的FileInputStream, 而非我们在定义时的InputStream; 所以如果有其他InputStream子类需求, 我们需要在这里额外再注册一次
    2. 为了防止二义性, 我在这里进行了完全约束, 同时约定了javaType, jdbcType。
    3. 我们在这里注册的两个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操作之读取
  1. 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>
  2. 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));
  3. 这里我需要提醒如下几点:
    1. 查询的SQL语句中, 是针对两个字段; 但在resultMap的映射配置中, 我们只设置了一个,也就是只配置了特殊情况——BLOB映射,其他字段的匹配最终还是交给了Mybatis自主完成。
    2. <select> 标签中的属性设置中,我们使用的是resultMap来引用我们配置的<resultMap>,而不是通常的resultType
    3. <resultMap>的type属性, 我们设置的依然是map,这样可以保证最大的兼容性。

3. CLOB操作

3.1 CLOB操作之存储
  1. Mybatis映射文件

  2. Mybatis映射文件

    <insert id="insert_clob" parameterType="map">               
        INSERT INTO  da_affix (                 
                 id, 
                 text
            ) VALUES ( 
                '123321', 
                #{clobData, jdbcType=CLOB}
        )
    </insert>
  3. Java端调用

    // 下面这种方式是不需要第三步的额外配置的
    String sqlid = NAMESPACE.concat("insert_clob");
    crud.insert(sqlid, Collections.singletonMap("clobData", "LQ~123456"));
    System.out.println(count);  
3.2 CLOB操作之读取
  1. 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>
  2. 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 ,已经为我们提供了BlobTypeHandlerBlobByteObjectArrayTypeHandler。其中最常用的是BlobTypeHandler,而BlobByteObjectArrayTypeHandler是用于数据库兼容性的,并不常用。
2. 针对Clob,则是提供了ClobTypeHandler

按照Mybatis目前的最佳实践,对于paramter,已经不再推荐程序员在配置时直接配置parameterMap参数, 而是推荐通过 #{start,jdbcType=INTEGER}配置来让Mybatis在内部自动构建相对应的ParameterMapping

resultMap 则还是推荐通过<resultMap>来配置。对于在<resultMap>配置的映射,DefaultResultSetHandler中分别定义了私有方法 applyAutomaticMappingsapplyPropertyMappings来处理未声明映射和显式声明映射的处理。

// -------------------------------------------- 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;
  }
  1. 《深入浅出MyBatis技术原理与实战》 第九章 - 9.1小节
  2. 《精通Spring4.x》 P444
  3. mybatis的BLOB存储与读取

猜你喜欢

转载自blog.csdn.net/lqzkcx3/article/details/80969105
LOB