背景
接到任务需要做一个数据上传的功能,主要是从20多个视图中查询数据,然后调用接口上传数据。
经过
我把这个功能分成两部分:数据查询、数据上传。
上传数据的接口只有一个,通过指定一个参数来区分不同类型的数据,而查询数据的视图中的数据不需要我们处理,因此决定查询数据时使用map来接收参数。
这么做的原因有几个:
- 视图字段与接口参数不完全匹配,有的少了几个字段
- 接口参数名是驼峰命名,视图也是驼峰命名但因为是Oracle数据库不区分大小写所以实际是大写
- 不能通过SQL写下划线别名,因为添加下划线后别名长度会超过30
因此,我决定直接使用SELECT *
查询数据,使用List<Map<String,Object>>
来接收数据。
但是还有个问题,接口中有部分数据是存在对象嵌套的,部分字段是List类型。
List类型的数据不是通过关联查询得到的,而是需要另外写一个查询去查。
考虑到以上,我决定mapper层查询数据使用map来接收,然后在service层先查询数据再转换成对象。
数据查询编写了一个DataQueryMapper
和DataQueryService
:
public interface DataQueryMapper {List<Map<String, Object>> getPage(@Param("req") PatientReq req);
}
@Service
@RequiredArgsConstructor
public class DataQueryService implements IDataQueryService {private final DataQueryMapper baseMapper;@Overridepublic List<PageData> getPage(PatientReq req) {return NcBeanUtil.mapToBean(baseMapper.getPage(req), PageData.class);}
}
BeanUtil用于转换对象:
public class NcBeanUtil {private static final Map<Class<?>, Map<String, Field>> CACHE = new HashMap<>();private NcBeanUtil() {}public static <T> T mapToBean(Map<String, Object> map, Class<T> clazz) {if (CollUtil.isEmpty(map)) {return null;}Map<String, Field> fieldMap = getFieldMap(clazz);T instance = ReflectUtil.newInstance(clazz);for (Map.Entry<String, Object> entry : map.entrySet()) {String key = entry.getKey();Object value = entry.getValue();Field field = fieldMap.get(key);if (field == null) {continue;}Class<?> fieldType = field.getType();Object fieldValue = Convert.convert(fieldType, value);ReflectUtil.setFieldValue(instance, field, fieldValue);}return instance;}public static <T> List<T> mapToBean(List<Map<String, Object>> mapList, Class<T> clazz) {if (CollUtil.isEmpty(mapList)) {return Collections.emptyList();}return mapList.stream().map(map -> NcBeanUtil.mapToBean(map, clazz)).collect(Collectors.toList());}private static <T> Map<String, Field> getFieldMap(Class<T> clazz) {if (CACHE.containsKey(clazz)) {return CACHE.get(clazz);}Map<String, Field> fieldMap = new HashMap<>();Field[] fields = ReflectUtil.getFields(clazz);for (Field field : fields) {String name = field.getName();String key = name.toUpperCase(Locale.ROOT);fieldMap.put(key, field);}CACHE.put(clazz, fieldMap);return fieldMap;}}
很巧妙的先使用Bean的field定义转换成大写,与原先的field映射存入map,转换时使用map获取到实际的field并填充转换后的值。
报错
运行以后,效果很好,只是在查询其中几个视图时出现了报错:
SQLRecoverableException: 关闭的连接
cn.hutool.core.convert.ConvertException: SQLRecoverableException: 关闭的连接at cn.hutool.core.convert.impl.StringConverter.clobToStr(StringConverter.java:56)at cn.hutool.core.convert.impl.StringConverter.convertInternal(StringConverter.java:32)at cn.hutool.core.convert.impl.StringConverter.convertInternal(StringConverter.java:22)at cn.hutool.core.convert.AbstractConverter.convert(AbstractConverter.java:58)at cn.hutool.core.convert.ConverterRegistry.convert(ConverterRegistry.java:207)at cn.hutool.core.convert.ConverterRegistry.convert(ConverterRegistry.java:247)at cn.hutool.core.convert.Convert.convertWithCheck(Convert.java:753)at cn.hutool.core.convert.Convert.convert(Convert.java:706)at cn.hutool.core.convert.Convert.convert(Convert.java:677)at cn.hutool.core.convert.Convert.convert(Convert.java:651)at ...mapToBean(NcBeanUtil.java:36)...at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)at ...NcBeanUtil.mapToBean(NcBeanUtil.java:50)...
奇怪的现象发生了,为什么在mapToBean的时候会发生SQL相关的异常?
顺着堆栈,发现是NcBeanUtil
在使用hutool转换时发生异常
public static <T> T mapToBean(Map<String, Object> map, Class<T> clazz) {...Object fieldValue = Convert.convert(fieldType, value);...
}
解决
百思不得其解,这种报错也是第一次见,为什么连接会关闭?
上网查询一番,发现都是说数据库连接断开之类的,但是重复实验很多次,只有特定几个查询会发生这种异常。
于是在转换时添加了日志,查看是什么原因导致的
public static <T> T mapToBean(Map<String, Object> map, Class<T> clazz) {...try {Object fieldValue = Convert.convert(fieldType, value);ReflectUtil.setFieldValue(instance, field, fieldValue);} catch (Exception e) {log.error("Convert异常: {}:{} valueType = {}, fieldType = {}", key, value, value == null ? null : value.getClass(), fieldType);throw e;}...
}
结果出来了
Convert异常: NCMARITALHISTORY:oracle.sql.CLOB@6bc2903b valueType = class oracle.sql.CLOB, fieldType = class java.lang.String
valueType是Clob,fieldType是String,异常是在Clob转String时发生的。
private static String clobToStr(Clob clob) {Reader reader = null;try {reader = clob.getCharacterStream();return IoUtil.read(reader);} catch (SQLException e) {throw new ConvertException(e);} finally {IoUtil.close(reader);}
}
继续在网络上检索,看到有这样一个说法
oracle-解析CLOB格式字段转String
SQL CLOB是 内置类型,它将字符大对象(Character Large Object) 存储为数据库表某一行中的一个列值。默认情况下,驱动程序使用 SQL locator(CLOB)实现 Clob 对象,这意味着 CLOB 对象包含一个指向 SQL CLOB 数据的逻辑指针而不是数据本身。Clob 对象在它被创建的事务处理期间有效。
Clob 对象在它被创建的事务处理期间有效
莫非是查询后连接关闭或者没有开启事务,连接先关闭了再把Clob转String导致报错?
尝试添加了@Transaction
注解
@Override
@Transactional(rollbackFor = Exception.class)
public List<PageData> get=Page(PatientReq req) {return NcBeanUtil.mapToBean(baseMapper.getPage(req), PageData.class);
}
没想到轻松解决了。