行转列--将多行数据转成Table结构

发布时间 2023-11-17 07:03:25作者: 小白=>龙

功能描述

21年做的一个功能,涉及到将行数据转化成列数据。边查边做,一点一点的尝试着做好。当时感觉有点吃力。完成之后本想记录,但一直拖延至今。最近再次接手与这个功能相关的业务,整理了之前写的代码,趁此机会记录下来。

功能界面

界面中是一个三级结构:L1级【Test Sample】, L2级【ABV, ABW,CO2,O2, pH】,L3级【白色表格中的数据:Date,Actual, Entered By】。

02-Mutil-Line.png

数据表

为了实现页面显示的三级结构,表中添加了Parent ID字段存数据关系。L1级ParentID为NULL,L2级ParentID存的是L1级的ID,L3级ParentID存L2级ID。如果想更容易查询出L1 或 L2级数据,可以为它们单独添加标识符字段(项目中,我是单独设置了L1 & L2的标识字段,这里省略了)。

03-Table-Structure.png

以上是这个功能的界面和数据表。下面是这个功能的一次改进,也是这篇想要记录的内容。

改进:

现在想做的:在手机上依然显示左侧的效果。在PC和PAD大屏幕上显示右侧的效果。这就涉及到了行转列,将平面数据转成合并为多行多列Table表格数据。

01.png

分析:

  1. 左侧 → 转化→ 右侧效果(重点)。
  • 对应关系:

    • L1 转化后对应Table标题

    • L2 转化后对应Table的多个列标题

    • L3 转化后作为数据填充到Table数据区域 【核心:时间 L3级每一行时间一致的会合并到Table中的同一行当中。不一致会分成多行】如下图:

03-L3-Relation.png

  1. 右侧Table 相关操作 (作为补充,可以忽略):
  • 右侧 → 转化 → 左侧: 只要ParentID正确就可以。

  • 表格中的操作 【重点】

    • 新增:每一行有多少列就需要保存多少条记录 【ParentID = 列标题.ID。这些记录的时间是一致的

    • 修改:不支持。【项目中是用来做Logs记录的不支持在界面中更改数据】

    • 删除:删除的一行是数据库中的多条记录,传多个ID,ids

    • 查询:只要保证1是正确的,刷新页面,数据就可以正常回显。

思路:

image.png

如果我们可以将用到的所有数据查询出来,转化成一个可以画出Table的数据结构。前端页面获取到这个数据结构使用双层for循环将Table画出来。那么,这个问题是不就可以解决了呢?

  • 两个问题:

    1. 可以画出Table的数据结构长什么样呢?

    2. 如何转化构建这种数据结构呢?

问题1,结合上面的分析,好像容易想到数据结构长什么样。至少需要包括这么几点:Table名字、列标题列表List、时间列表List、数据字典【一个列标题 + 一个时间,可以从数据字典中查一个数据?】。如下图:

image.png

问题2,如何转化构建这种数据结构呢?

  • Table名字:查询L1级数据

  • 列标题列表List:查询出L2级数据,列表 【使用ParentID = L1.ID查询】

  • 时间列表List:查询出L3级数据,GROUP BY Date,列表 【ParentID IN L2.IDs】

  • 数据字典: 查询出L3级数据,字典 【怎么转上图左侧中的Dict部分呢?】

实现:行转列-构建Table数据结构

下面是数据表,假设表名叫Logs

image.png

后端:

核心代码,使用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 = " + 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 myQCLogRs = SQL2Rs(sqlB.ToString());

        // 2. 构建数据结构。 构建出Dictionary<列,Dictionary<时间行, 数据对象>>(JAVA:Map<列,Map<时间行, 数据对象>>)的数据结构 
        Dictionary<string, object> TempDateLogsDict = new();
        int PrevParentID = -1;
        while (!myQCLogRs.EOF())
        {
            int ParentID = myQCLogRs.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 = myQCLogRs.Item("Date");
            LogDict.Add("ID", myQCLogRs.ItemInt("ID"));
            LogDict.Add("ParentID", myQCLogRs.ItemInt("ParentID"));
            LogDict.Add("DateDisplay", Date);
            LogDict.Add("ActualValue", myQCLogRs.Item("ActualValue"));
            LogDict.Add("EnteredBy", myQCLogRs.Item("UpdatedBy"));
            LogDict.Add("Name", myQCLogRs.Item("Name"));
            TempDateLogsDict.Add(Date, LogDict);

            PrevParentID = ParentID;
            myQCLogRs.MoveNext();

            if (myQCLogRs.EOF())
            {
                // 2.3 将最后一列的<时间,数据>字典 放到 <列,<时间,数据>>字典中 
                DataDict.Add(PrevParentID, TempDateLogsDict);
            }
        }
        myQCLogRs.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中。

思路:

  1. 时间数据SQL中已包含,只需在SQL中加上列信息。即MyColumnLogs相关的SELECT语句。

  2. 可以使用字典或Set对列数据和时间数据去重。

  3. 将去重后的数据转成List。

前端:

核心代码,使用到了Vue模板语法。使用双层for循环将table画出来即可。

<table >
    <thead>
        <tr>
            <input type="hidden" id="id" :value="id">
            <th></th>
            <th>Date</th>
            <!-- 横向循环填充列标题 -->
            <template v-for="myQCLog in Data.ColumnList">
                <th>
                    <input type="hidden" id="ID" name="ID" :value="myQCLog.ID">
                    <p>
                        <span>{{myQCLog.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>

总结:

行数据与列数据相互转化可能需要考虑两点:

  1. 对应关系

  2. 多行要合并成一行多列的数据时间要一致。依照哪个字段转化,同为一行的哪个字段就需要一致。