💡 一句话结:
实现传感器和深度及采集的数值动态对应,将不规则的数据转变成固定列头的一行行数据。
🔑 关键信息点:
- 通过传感器编号和深度将传感器对应的数值与时间建立关联。
- 使用SpringBoot+MyBatis框架实现动态查询不同参数条件下的传感器数据。
- 通过动态SQL实现各级数据项的对应查询,动态生成不同条件对应的字段。
- 利用HashMap和Set等数据结构处理数据并建立传感器编号和深度的对应关系。
- 提供两个版本的代码实现,一个使用Map,一个使用Set,实现对应关系和数据处理。
对应关系分析(掺杂业务逻辑)
如图 简明来讲就是实现传感器和深度及采集的数值动态对应
因为硬件设备的限制, 在数据采集及通过MQ到库中时数据是一个传感器对应一个深度对应一个该传感器的采集值, 我们要做的是列出所有传感器, 以时间为索引, 时间和传感器对应上一个值简单来说就是把一列列不规则的数据, 转变成固定列头的一行行数据
数据字段返回(简单以json格式展示)
析
:这样就对应上标题了tableData有多组, 但是每一组的数值还和日期时间树的结点能对应上, 可以变向理解为tabledata中含有子树(截图比较抽象)
实现
思路: 使用springboot+mybatis框架
mybatis可以动态接收不同参数 (传感器点位,传感器深度, 传感器类型[温湿度]等) 筛选后的传感器编号,并查询匹配传感器对应的数据
返回格式的转化以及查询后格式的完善在业务层完成
效果比对: 上面为原格式没有明显对应关系,下面为转换后的
Dao层(mapper)
首先是先从SQL实现各级数据项对应, 并实现使用mybatis框架动态接收符合条件的各组传感器
注意使用的resultType不是直接使用的resultMap
这儿需要使用动态SQL, 先使用传感器编号查询接口
, 查询出符合条件的传感器编号, 然后动态赋值
到case…when中的相关字段, 动态生成不同条件对应的case…when字段进行第二次具体数值的获取查询
<select id="" resultType="map">
select
to_char(detection_time , 'yyyy/MM/dd') as 日期,
to_char(detection_time , 'hh24:mi:ss') as 时间
<choose>
<when test="sensorCodes != null and sensorCodes.size() != 0">
<foreach item="sensorCode" collection="sensorCodes" separator="">
<if test=" sensorCode != null and sensorCode != ''">
,max(case when sensor_code = '${sensorCode}' then humidity end) as "${sensorCode}"
</if>
</foreach>
</when>
<otherwise>
,'没有与之对应的传感器编号'
</otherwise>
</choose>
FROM structure_internal_humidity_info
WHERE 1=1
<if test="startDate != null">
AND date_format(detection_time, '%Y-%m-%d %H:%i:%s') <![CDATA[ >= ]]> ${startDate}
</if>
<if test="endDate != null">
AND date_format(detection_time, '%Y-%m-%d %H:%i:%s') <![CDATA[ <= ]]> ${endDate}
</if>
<if test="observationPointId != null">
AND observation_point_id = ${observationPointId}
</if>
<if test="sensorCodes != null and sensorCodes.size() > 0">
AND sensor_code IN
<foreach item="sensorCode" collection="sensorCodes" open="(" separator="," close=")">
'${sensorCode}'
</foreach>
</if>
GROUP BY
detection_time
ORDER BY
detection_time
<!--<if test=" pageSize != null and pageSize != ''">-->
<!-- limit #{pageSize}-->
<!--</if>-->
<!--<if test="pageIndex != null and pageIndex != ''">-->
<!-- offset #{pageIndex}-->
<!--</if>-->
</select>
为什么注掉了offset分页, 因为业务代码中使用了mybatis分页, 这样更灵活的解决了分页, 但是不可避免的可能会有Map类或者ArrayList类中会有方法触发意料之外的bug, 留存注释备用! [ 稳妥起见还是不注释掉使用数据库原生的分页不容易踩坑 ]
这儿牵扯到一个子接口
根据参数动态查询对应传感器sensorCode 要实现的就是根据参数动态匹配相关的传感器数据, 可以理解为根据不同的位置坐标作为查询条件, 获取不同位置的一组传感器编号
业务实现
分两个版本分别使用java的Map和Set
作为主要数据结构进行数据处理hashMap返回, 逻辑图及各数据结构之间的转换后续出图传过来, 时间有限先码上
版本一
未使用Set
{
//public CommonPage<Map<String, Object>> queryNewStandardlist(CommonQuery query) {
List<SensorDeviceInfoPO> sensorCodes = new ArrayList<SensorDeviceInfoPO>();
if (!(query.getMonitoringDataTypes().equals("STRUCTURE_INTERNAL_HUMIDITY_INFO"))){
sensorCodes = sensorDeviceInfoMapper.querySensorCodeByDeviceid( query);
}else {
throw new BizException(BizCodeEnum.BIZ_FAILURE, "无效的类型");
}
List<String> sensorsList = new ArrayList<String>();
List<BigDecimal> depthList = new ArrayList<BigDecimal>();
//声明一个变量存储深度和传感器编号,key为传感器编号,value为传感器对应的深度
Map<String,BigDecimal> sensorDepthMap = new HashMap<String,BigDecimal>();
for (SensorDeviceInfoPO sensor : sensorCodes) {
String sensorCode = sensor.getSensorCode();
BigDecimal depth = sensor.getDepth();
// 因为有传感器编号为空的情况所以添加非空判断去除编号为空的传感器从而对齐横列和纵列表格数据(后续 如果数据库更新后没有此情况可删除此段代码中的判断)
if ( ! (sensorCode.isEmpty() || sensorCode.isBlank() ) && ! (depth == null || depth.compareTo(BigDecimal.ZERO) == 0) ){
//用于结果集查询
sensorsList.add(sensorCode);
depthList.add(depth);
//用于列头传感器和深度值匹配处理
sensorDepthMap.put(sensorCode,depth);
}
}
query.setSensorCodes(sensorsList);
int total = 0;
//TODO 另一种逻辑如果,没有分页条件则不查询总条数(需要优化)
if (query.getPageIndex() != null || query.getPageSize() != null) {
QueryBackUp backupQuery = new QueryBackUp(query.getPageIndex(), query.getPageSize()); // 使用Query类的拷贝构造函数创建备份对象
query.setPageIndex(null);
query.setPageSize(null);
total = structureInternalHumidityInfoMapper.queryStandardColumn(query).size();
query.setPageIndex(backupQuery.getPageIndex());
query.setPageSize(backupQuery.getPageSize());
}
query.setStartDate(
Optional.ofNullable(query.getStartDate()).orElse(Date.from(LocalDate.now().minusDays(2).atStartOfDay().toInstant(ZoneOffset.ofHours(8)))));
query.setEndDate(
Optional.ofNullable(query.getEndDate()).orElse(Date.from(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.ofHours(8)))));
query.setPageSize(Optional.ofNullable(query.getPageSize()).orElse(16));
query.setPageIndex(Optional.ofNullable(query.getPageIndex()).orElse(1));
// Page<Map<String, String>> standardResult = PageHelper.startPage(query.getPageIndex(), query.getPageSize()).doSelectPage(() -> {
// Page<Map<String, String>> standardResult = PageHelper.startPage(query.getPageIndex(), query.getPageSize()).doSelectPage(
// new PageRowBounds<Map<String, String>>() {
// @Override
// public List<Map<String, String>> doResult(List<Map<String, String>> list) {
// return list != null ? list : Collections.emptyList();
// }
// });
// List<Map<String, String>> mapperResult =
// Page<Map<String, String>> standardResult = PageHelper.startPage(query.getPageIndex(), query.getPageSize()).doSelectPage(() ->
// structureInternalHumidityInfoMapper.queryStandardColumn(query));
// structureInternalHumidityInfoMapper.queryStandardColumn(query);
// if (mapperResult == null) {
// mapperResult = Collections.emptyList();
// }
// });
List<Map<String, String>> standardResult = structureInternalHumidityInfoMapper.queryStandardColumn(query);
//表格内容用于返回
// standardResult.getResult();
List<String> columnSensorCode = new ArrayList<String>();
List<BigDecimal> columnDepth = new ArrayList<>();
List<List<String>> tableData = new ArrayList<List<String>>();
if (!standardResult.isEmpty()) {
/* 列头排序及传感器与深度动态匹配及传感器与深度动态匹配及传感器与深度动态匹配BEGIN */
//列头处理, 以及处理列头因为数据结构差异y引发的列头汉字有时候不是排在第一个的问题
Map<String, String> columnCodeMap = standardResult.get(0);
//做到编号与深度匹配
for (Entry<String, String> columnCode : columnCodeMap.entrySet()) {
//传感器编号
Object columnCodeValue = columnCode.getKey();
//传感器深度
String columnDepthValue = String.valueOf(sensorDepthMap.get(columnCode.getKey()));
if ( ! (columnDepthValue.isEmpty() || columnDepthValue.isBlank() || columnDepthValue.equals("") || columnDepthValue == null
|| columnCodeValue.equals("") || columnCodeValue == null) ){
columnSensorCode.add(String.valueOf(columnCodeValue));
//sensorDepthMap.put(columnCodeValue,columnDepthValue);
}
}
columnSensorCode.sort(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.hashCode ()- o2.hashCode();
}
});
// Check if the first element is not Chinese
if (! (columnSensorCode.get(0).equals("日期"))) {
// Reverse the list
Collections.reverse(columnSensorCode);
// Swap the first and second elements
String temp = columnSensorCode.get(0);
columnSensorCode.set(0, columnSensorCode.get(1));
columnSensorCode.set(1, temp);
}
/* 列头排序及传感器与深度动态匹配及传感器与深度动态匹配及传感器与深度动态匹配END */
for (Map<String, String> rowDataMap : standardResult) {
//遍历结果中的每一行Map
//rowDataMap = standardResult.get(i);
//声明一个List存储每一行Map中的目标值
List<String> rowData = new ArrayList<String>();
//循环遍历Map 根据列名动态 取得目标值使值与列名匹配
for (Object columnName : columnSensorCode) {
//todo finish bigDecimal 处理
Object rowDataValue = rowDataMap.get(columnName);
//Object depthValue = rowDataMap.get()
rowData.add(String.valueOf(rowDataValue));
if (0 == tableData.size()){
columnDepth.add(sensorDepthMap.get(columnName));
}
}
//用于返回塞值
tableData.add(rowData);
}
Map<String, Object> result = new HashMap<String, Object>();
result.put("columnSensorCode", columnSensorCode);
result.put("columnDepth", columnDepth);
result.put("tableData", tableData);
CommonPage<Map<String, Object>> standardResultPage = new CommonPage<Map<String, Object>>();
standardResultPage.setPageIndex(query.getPageIndex());
standardResultPage.setPageSize(query.getPageSize());
standardResultPage.setTotal(total);
standardResultPage.setData(result);
//System.out.println("result = " + result);
return standardResultPage;
}else {
throw new BizException(BizCodeEnum.BIZ_FAILURE, "无效的类型, 湿度结果为空");
}
}
版本二
使用了Set
{
//public Map<String, Object> queryStandardTableList(CommonQuery query) {
CommonPage<Map<String, Object>> resultPage = new CommonPage<>();
List<SensorDeviceInfoPO> sensorCodes = sensorDeviceInfoMapper.querySensorCodeByDeviceid(query);
//去除List中的空字符串
List<String> sensorCodeList = new ArrayList<String>();
Map<String,BigDecimal> sensorCodeDepMap = new HashMap<>();
for(SensorDeviceInfoPO sensorCode:sensorCodes) {
String sensorCodeStr = sensorCode.getSensorCode();
BigDecimal sensorCodeDep = sensorCode.getDepth();
if (!(sensorCodeStr.isEmpty() && String.valueOf(sensorCodeDep).isEmpty() && sensorCodeDep == null && sensorCodeStr == null) ){
sensorCodeList.add(sensorCodeStr);
sensorCodeDepMap.put(sensorCodeStr,sensorCodeDep);
}
}
// 使用临时变量保存原始的分页参数,避免影响查询结果
int originPageIndex = Optional.ofNullable(query.getPageIndex()).orElse(1);
int originPageSize = Optional.ofNullable(query.getPageSize()).orElse(18);
// 设置传感器代码
query.setSensorCodes(sensorCodeList);
// 获取结果集总数
int total = structureInternalTemperatureInfoMapper.queryStandardTableTempData(query).size();
// 设置查询参数的默认值
query.setPageIndex(originPageIndex);
query.setPageSize(originPageSize);
// 设置部分参数默认值
query.setStartDate(
Optional.ofNullable(query.getStartDate())
.orElseGet(() -> Date.from(LocalDate.now().minusDays(2).atStartOfDay().toInstant(ZoneOffset.ofHours(8))))
);
query.setPageSize(Optional.ofNullable(query.getPageSize() ).orElse(18));
query.setPageIndex(Optional.ofNullable(query.getPageIndex()).orElse(1));
//查询结果集
List<Map<String, String>> dynamicResults = structureInternalTemperatureInfoMapper.queryStandardTableTempData(query);
//使用mybatis 分页插件
// AtomicReference<List<Map<String, String>>> dynamicResultsMapper = null;
// Page<Map<String, String>> dynamicResults = PageHelper.startPage(query.getPageIndex(), query.getPageSize()).doSelectPage( () ->{
// dynamicResultsMapper.set(structureInternalTemperatureInfoMapper.queryStandardTableTempData(query));
// });
//动态处理映射属性中的动态列名
List<String> columnHeaders = new ArrayList<>();
List<BigDecimal> depths = new ArrayList<>();
List<List<String>> tableData = new ArrayList<>();
Set<String> uniqueSensorCodeSet = new HashSet<>();
if (!dynamicResults.isEmpty()) {
//动态列头
// 拼接dynamicResults.get(0)和dynamicResults.get(1)的去重值
uniqueSensorCodeSet.addAll(dynamicResults.get(0).keySet());
uniqueSensorCodeSet.addAll(dynamicResults.get(1).keySet());
/*列头处理begin*/
/* 根据传感器编码动态匹配目标值 动态结果集列头 */
// 拼接dynamicResults.get(0)和get(1)中的不重复值作为列头, 将去重后的值作为sensorCodeRow的键
Map<String, String> newSensorCodeRow = new HashMap<>();
for (String uniqueKey : uniqueSensorCodeSet) {
newSensorCodeRow.put(uniqueKey, uniqueKey);
}
for (Entry<String, String> entry : newSensorCodeRow.entrySet()) {
String columnName = entry.getKey();
// if (columnName.contains((CharSequence) dynamicResults.get(0)) || columnName.contains((CharSequence) dynamicResults.get(1))) {
uniqueSensorCodeSet.add(columnName);
// }
}
// 将不重复的列名添加到columnHeaders中
columnHeaders.addAll(uniqueSensorCodeSet);
//对列头进行排序从而对tableData进行排序
// columnHeaders.sort(new Comparator<String>() {
// @Override
// public int compare(String o1, String o2) {
// return o1.hashCode() - o2.hashCode();
// }
// });
// 使用jdk11的排序三目运算符简洁写法但是有时会把汉字排到最后, 和上面两种排序方法哪种生效使用哪一个
columnHeaders.sort(String::compareTo);
// 解决上述汉字后排问题,使用上面这种写法有时候会导致汉字排到后面面,所以使用jdk8的排序方法针对汉字进行排序让日期排第一个时间排第二个
if (! (columnHeaders.get(0).equals("日期"))) {
// Reverse the list
Collections.reverse(columnHeaders);
// Swap the first and second elements
String temp = columnHeaders.get(0);
columnHeaders.set(0, columnHeaders.get(1));
columnHeaders.set(1, temp);
}
//根据排序后的传感器编号取得深度值的数据
for (String senorCode : columnHeaders) {
depths.add(sensorCodeDepMap.get(senorCode));
}
/*列头处理end*/
//表格内容
for (Map<String, String> rowDataMap : dynamicResults) {
List<String> rowData = new ArrayList<>();
//按照列头具体数据动态抽取表格数据
for (String columnName : columnHeaders) {
String cellData = rowDataMap.get(columnName);
if (cellData == null) {
rowData.add(""); // 如果当前行数据中没有对应的列数据,添加空字符串
} else {
rowData.add(cellData);
}
}
tableData.add(rowData);
}
Map<String , Object> result = new HashMap<>();
result.put("columnHeaders", columnHeaders);
result.put("depth",depths);
result.put("tableData", tableData);
resultPage.setPageIndex(query.getPageIndex());
resultPage.setPageSize(query.getPageSize());
resultPage.setTotal(total);
resultPage.setData(result);
//return structureInternalTemperatureInfoMapper.queryStandardTableTempData(sensorCodes,startDate,endDate,observationPointId);
// return (CommonPage<List<StructureInternalTemperatureInfoPO>>)
return resultPage;
}else{
throw new BizException(BizCodeEnum.BIZ_FAILURE,"暂无内部温度数据");
}
}
析:
这两段代码都处理了参数筛选传感器将传感器编号传给SQL , 接受SQL返回的数据并将列头 ( 动态的传感器编号组和传感器对应的深度 ) 数据: (传感器检测的数值 ) 包装成不同的树结点返回给接收方
上面提到的有没有用到Set的使用与否, 主要处理的就是列头传感器与传感器深度的对应这儿的列头与数据看成Excel表格中的表头和表头对应的数据
个人目前感觉Set和其他数据集混用的好一点, 浅显一点的理解就是可以少声明一个变量