行转列--将多行数据转成多行多列的Table结构
功能描述
21年做的一个功能,涉及到将行数据转化成列数据。边查边做,一点一点的尝试着做好。当时感觉有点吃力。完成之后本想记录,但一直拖延至今。最近再次接手与这个功能相关的业务,整理了之前写的代码,趁此机会记录下来。
功能界面
界面中是一个三级结构:L1级【Test Sample】, L2级【ABV, ABW,CO2,O2, pH】,L3级【白色表格中的数据:Date,Actual, Entered By】。
数据表
为了实现页面显示的三级结构,表中添加了Parent ID字段存数据关系。L1级ParentID为NULL,L2级ParentID存的是L1级的ID,L3级ParentID存L2级ID。如果想更容易查询出L1 或 L2级数据,可以为它们单独添加标识符字段(项目中,我是单独设置了L1 & L2的标识字段,这里省略了)。
以上是这个功能的界面和数据表。下面是这个功能的一次改进,也是这篇想要记录的内容。
改进:
现在想做的:在手机上依然显示左侧的效果。在PC和PAD大屏幕上显示右侧的效果。这就涉及到了行转列,将平面数据转成合并为多行多列Table表格数据。
分析:
- 左侧 → 转化→ 右侧效果(重点)。
-
对应关系:
-
L1 转化后对应Table标题
-
L2 转化后对应Table的多个列标题
-
L3 转化后作为数据填充到Table数据区域 【核心:时间 L3级每一行时间一致的会合并到Table中的同一行当中。不一致会分成多行】如下图:
-
- 右侧Table 相关操作 (作为补充,可以忽略):
-
右侧 → 转化 → 左侧: 只要ParentID正确就可以。
-
表格中的操作 【重点】
-
新增:每一行有多少列就需要保存多少条记录 【ParentID = 列标题.ID。这些记录的时间是一致的】
-
修改:不支持。【项目中是用来做Logs记录的不支持在界面中更改数据】
-
删除:删除的一行是数据库中的多条记录,传多个ID,ids
-
查询:只要保证1是正确的,刷新页面,数据就可以正常回显。
-
思路:
如果我们可以将用到的所有数据查询出来,转化成一个可以画出Table的数据结构。前端页面获取到这个数据结构使用双层for循环将Table画出来。那么,这个问题是不就可以解决了呢?
-
两个问题:
-
可以画出Table的数据结构长什么样呢?
-
如何转化构建这种数据结构呢?
-
问题1,结合上面的分析,好像容易想到数据结构长什么样。至少需要包括这么几点:Table名字、列标题列表List、时间列表List、数据字典【一个列标题 + 一个时间,可以从数据字典中查一个数据?】。如下图:
问题2,如何转化构建这种数据结构呢?
-
Table名字:查询L1级数据
-
列标题列表List:查询出L2级数据,列表 【使用ParentID = L1.ID查询】
-
时间列表List:查询出L3级数据,GROUP BY Date,列表 【ParentID IN L2.IDs】
-
数据字典: 查询出L3级数据,字典 【怎么转上图左侧中的Dict部分呢?】
实现:行转列-构建Table数据结构
下面是数据表,假设表名叫Logs
后端:
核心代码,使用C#实现。删除了业务和敏感代码,只保留了核心的逻辑处理部分。
internal static Dictionary<string, object> Get_Data(int SampleID)
{
Dictionary<string, object> ResultDict = new();
Dictionary<int, object> DataDict = new();
List<Dictionary<string, object>> DateList = new();
// 1. 查询SQL。 两点:a,3级数据的关联关系; b, 先按列分组,再按行(时间)分组
StringBuilder sqlB = new();
sqlB.Length = 0;
sqlB.AppendLine("SELECT ");
sqlB.AppendLine(" MyDataLogs.ID ");
sqlB.AppendLine(" , MyDataLogs.ParentID ");
sqlB.AppendLine(" , MAX(MyDataLogs.Name) AS Name ");
sqlB.AppendLine(" , MAX(MyDataLogs.ActualValue) AS ActualValue ");
sqlB.AppendLine(" , MAX(MyDataLogs.Date) AS Date ");
sqlB.AppendLine(" , MAX(MyDataLogs.UpdatedBy) AS UpdatedBy ");
sqlB.AppendLine("FROM Logs AS MyDataLogs "); // L3级数据 【数据】
sqlB.AppendLine("INNER JOIN Logs AS MyColumnLogs ON MyDataLogs.ParentID = MyColumnLogs.ID "); // L3.ParentID = L2.ID 【数据 -> 列标题】
sqlB.AppendLine("WHERE MyColumnLogs.ParentID = " + BC_Fmt.FDataInt(SampleID) + " "); // L2.ParentID = L1.ID 【列标题 -> Table】
sqlB.AppendLine("GROUP BY ");
sqlB.AppendLine(" MyDataLogs.ParentID "); // 先横向按标题分组
sqlB.AppendLine(" , MyDataLogs.Date "); // 再纵向按时间分组
sqlB.AppendLine("ORDER BY ");
sqlB.AppendLine(" MyDataLogs.ParentID ");
sqlB.AppendLine(" , MyDataLogs.Date ");
sqlB.AppendLine(" , MyDataLogs.ID ASC ");
sqlB.AppendLine(";");
BC_Recordset myLogRs = SQL2Rs(sqlB.ToString());
// 2. 构建数据结构。 构建出Dictionary<列,Dictionary<时间行, 数据对象>>(JAVA:Map<列,Map<时间行, 数据对象>>)的数据结构
Dictionary<string, object> TempDateLogsDict = new();
int PrevParentID = -1;
while (!myLogRs.EOF())
{
int ParentID = myLogRs.ItemInt("ParentID");
if (PrevParentID != ParentID)
{
// 2.2 新的一列开始,将上一列的<时间,数据>字典 放到 <列,<时间,数据>>字典中
if (PrevParentID != -1)
{
DataDict.Add(PrevParentID, TempDateLogsDict);
}
TempDateLogsDict = new();
}
/*
2.1 将每一列的数据转化为 时间 -> 数据的形式
列1:{
时间1: { 数据对象 },
时间2:{ 数据对象 },
....
}
*/
Dictionary<string, object> LogDict = new();
string Date = myLogRs.Item("Date");
LogDict.Add("ID", myLogRs.ItemInt("ID"));
LogDict.Add("ParentID", myLogRs.ItemInt("ParentID"));
LogDict.Add("DateDisplay", Date);
LogDict.Add("ActualValue", myLogRs.Item("ActualValue"));
LogDict.Add("EnteredBy", myLogRs.Item("UpdatedBy"));
LogDict.Add("Name", myLogRs.Item("Name"));
TempDateLogsDict.Add(Date, LogDict);
PrevParentID = ParentID;
myLogRs.MoveNext();
if (myLogRs.EOF())
{
// 2.3 将最后一列的<时间,数据>字典 放到 <列,<时间,数据>>字典中
DataDict.Add(PrevParentID, TempDateLogsDict);
}
}
myLogRs.Close();
// 3. 构建返回数据结构
ResultDict.Add("ColumnList", Get_ColumnList(SampleID)); // 获取列List数据
ResultDict.Add("DataDict", DataDict);
ResultDict.Add("SampleID", SampleID);
ResultDict.Add("SampleName", SampleName);
ResultDict.Add("DateList", Get_DateList(SampleID)); // 获取时间行List数据
return ResultDict;
}
整合:可以将获取列List和时间行List的逻辑整合到上述SQL中。
思路:
-
时间数据SQL中已包含,只需在SQL中加上列信息。即MyColumnLogs相关的SELECT语句。
-
可以使用字典或Set对列数据和时间数据去重。
-
将去重后的数据转成List。
前端:
核心代码,使用到了Vue模板语法。使用双层for循环将table画出来即可。
<table >
<thead>
<tr>
<input type="hidden" id="id" :value="id">
<th></th>
<th>Date</th>
<!-- 横向循环填充列标题 -->
<template v-for="myLog in Data.ColumnList">
<th>
<input type="hidden" id="ID" name="ID" :value="myLog.ID">
<p>
<span>{{myLog.Name}}</span>
</p>
</th>
</template>
<th>Entered By</th>
</tr>
</thead>
<tbody>
<!-- 纵向循环 每一行 -->
<template v-for="myDate in Data.DateList">
<tr>
<td>
<span >Delete btn</span>
</td>
<td class="ecp-field" style="padding:3px 5px;">
{{myDate.DateDisplay}}
</td>
<!-- 横向循环 每一列 -->
<template v-for="myColumn in Data.ColumnList">
<td>
<template v-if="Data.DataDict[myColumn.ID] && Data.DataDict[myColumn.ID][myDate.Date]">
<span>
<!-- 使用 列 + 行 -> 取出每一个元素数据 -->
{{(Data.DataDict[myColumn.ID][myDate.Date]).ActualValue}}
</span>
</template>
</td>
</template>
<td class="ecp-field" style="padding:3px 5px;">
{{myDate.EnteredBy}}
</td>
</tr>
</template>
</tbody>
</table>
总结:
行数据与列数据相互转化可能需要考虑两点:
-
对应关系
-
多行要合并成一行多列的数据,时间要一致(依照哪个字段转化,同为一行的哪个字段的值就需要一致)。
本文来自博客园,作者:小白=>龙,转载请注明原文链接:https://www.cnblogs.com/shishaolong/p/17836501.html