业务场景:我们在前端展示表格数据一般有两种,一种是业务的统计问题表单,还有一种就是通过问题表单,转换汇总成结果汇总表单,比如一张发货表单,有发货量,产品,时间,现在需要转换成:产品每个月获取的产品数量。也就是 时间字段 从 ‘行’提到了‘列’,行转列了,那么就涉及到,根据时间的筛选,来动态得到某个产品几个月的发货量,这里的数据处理就需要用存储过程来实现动态的行转列了。
一、表结构数据与需求
首先先看下业务涉及的表格都有哪些,首先有一个产品表,记录产品、项目、编码信息,其次就是一个发货明细表,记录着编码的发货量、发货时间等信息,表结构如下:
产品信息表
PRODUCT | PROJECT | CODE |
A产品 | A项目 | A001 |
A产品 | B项目 | B001 |
B产品 | B项目 | B002 |
这里简要概述下业务逻辑,一个产品会有多个项目,项目会有多个产品,而编码是唯一的,这些逻辑不影响我们做关联,仅做解释。
发货明细表
ITEM_CODE | ACTUAL_SHIPMENT_DATE | QUANTITY | .. |
A001 | 2022-11-01 | 100 | .. |
A001 | 2022-11-05 | 200 | .. |
B001 | 2022-10-01 | 150 | .. |
产品的发货一般都有涉及产品信息,发货地,发货量,发货时间等,我们的需求是求每个月发货量,所以将编码、发货时间和发货量简要展示。
这两个表,最终合并后形式如下:
PRODUCT | PROJECT | CODE | QUANTITY | 2022-11 | 2022-10 | .. |
A产品 | A项目 | A001 | 500 | 100 | 50 | .. |
A产品 | B项目 | B001 | 600 | 50 | 200 | .. |
B产品 | B项目 | B002 | 700 | 100 | 200 | .. |
数据行转列,把时间行数据转换成列头;发货量其实是针对某个编码的发货,SUM字段表示,业务查询的时间范围的某个编码的发货总量,往后是每个动态月份的按月发货量,比如 查询A001编码,22年11月-8月的发货量,那么SUM列就是将4个月发货量求和得到结果值,往后分别是2022-11、2022-10、2022-09、2022-08四列月份,值则为每个月的发货量求和(这里是月内的发货求和)。
二、分析处理思路
这里如果只是静态的明确知道年月有多少的话,直接可以使用case when转行即可,样例如下SQL,对于动态行转列问题难点在于不知道有多少列,也就是年月列,前端的样式组件,应该是没办法处理这种动态列头,组件封装好了下次变化年份就匹配不上了,所以最好从后端数据库入手,数据库有个强大的功能,或许就能满足需求了,存储过程,然后在Mybatis的dao层映射文件调用存储过程的方式来实现,这样做的好处是直接得到了我们想要得数据结构,不需要再进行处理,并且存储过程可以封装复杂的sql语句,供外部直接调用,解决了mybatis无法处理复杂sql语句的问题。存储过程的认识,我这边之前也写了一篇,大家可以了解下,当业务需求在转换数据处理很复杂或者需要动态变化列头的时候,不妨考虑下存储过程
固定的表头时间列的SQL处理如下:
SELECT
product,project,
ITEM_CODE,sum(total) total,
SUM( CASE shipmentDate WHEN '2022-05' THEN total ELSE 0 END ) '2022-05' ,
SUM( CASE shipmentDate WHEN '2022-06' THEN total ELSE 0 END ) '2022-06' ,
SUM( CASE shipmentDate WHEN '2022-07' THEN total ELSE 0 END ) '2022-07'
FROM
(
SELECT
t2.product as product,
t2.project as project,
ITEM_CODE,
sum( QUANTITY ) AS total,
DATE_FORMAT( ACTUAL_SHIPMENT_DATE, "%Y-%m" ) AS shipmentDate
FROM
发货表 t1 INNER JOIN 编码表 t2 on t1.ITEM_CODE = t2.code
where DATE_FORMAT( ACTUAL_SHIPMENT_DATE, "%Y-%m" ) BETWEEN '2022-05' AND '2022-07'
GROUP BY
ITEM_CODE,
DATE_FORMAT( ACTUAL_SHIPMENT_DATE, "%Y-%m" )
ORDER BY
shipmentDate DESC
) temp
GROUP BY
ITEM_CODE
三、解决方案
1.创建存储过程,代码如下:
DELIMITER $$ --重定义分隔符,避开, ;这些在sql中常用的符号,但navicat中创建的存储过程不需要再重定义了,这个客户端已经做了分隔符重定义。
//创建存储过程
CREATE DEFINER=`dbuser`@`%` PROCEDURE `RowToColumnPro`(
IN productName VARCHAR(100),
IN projectName VARCHAR(100),
IN codeName VARCHAR(20),
IN startTime VARCHAR(20),
IN endtTime VARCHAR(20)
)
BEGIN
--1.创建临时表,将编码表与发货表关联,先得到一个按编码,年月分组后的编码发货数据集,性能有一定提升
DROP TEMPORARY TABLE IF EXISTS tempCode;
CREATE TEMPORARY TABLE tempCode(
PRODUCT VARCHAR(100),
PROJECT VARCHAR(100),
`CODE` VARCHAR(100),
NUM INT,
ACTUAL_SHIPMENT_DATE VARCHAR(100)
)
--------------------------
--2.设置5个用户变量存储查询的产品信息动态入参条件,注意要判断下非空的情况,根据前端用户的实际传参值
SET @strWhere=' AND 1=1 '; -- 联表的where条件设置一个用户变量
IF productName<>"" THEN
--find_in_set函数,productName 参数可以是一个列表变量如('A产品','B产品'),用来精确匹配每个值,字段值以英文”,”分隔,不能用in,in是匹配常量值
SET @strWhere1=CONCAT(' AND find_in_set(c.product,"',productName ,'")');
SET @strWhere=CONCAT(@strWhere,@strWhere1); -- 拼接条件
END IF;
IF projectName<>"" THEN
SET @strWhere2=CONCAT(' AND find_in_set(c.project,"',projectName ,'")');
SET @strWhere=CONCAT(@strWhere,@strWhere2);
END IF;
IF codeName<>"" THEN
SET @strWhere3=CONCAT(' AND find_in_set(c.`code`,"',codeName,'")');
SET @strWhere=CONCAT(@strWhere,@strWhere3);
END IF;
IF startTime<>"" THEN
SET @strWhere4=CONCAT(' AND DATE_FORMAT(s.ACTUAL_SHIPMENT_DATE,"%Y-%m-%d")>="',startTime,'"');
SET @strWhere=CONCAT(@strWhere,@strWhere4);
END IF;
IF endTime != "" THEN
SET @strWhere5=CONCAT('AND DATE_FORMAT(s.ACTUAL_SHIPMENT_DATE,"%Y-%m-%d")<="',endTime,'"');
SET @strWhere=CONCAT(@strWhere,@strWhere5);
END IF;
-- SELECT @strWhere; 这个是打印出用户变量的拼接值
------------------------
--3.定义查询sql赋值@strSelect:两表联表+产品信息动态条件:@strWhere+分组排序:发货时间年月、产品、项目、编码, 最后插入到临时表tempCode
SET @strSelect=CONCAT(' INSERT INTO tempCode(PRODUCT,
PROJECT,
`CODE`,
NUM,
ACTUAL_SHIPMENT_DATE) SELECT
c.product AS PRODUCT,
c.project AS PROJECT,
c.`code` AS `CODE`,
SUM(s.QUANTITY) AS NUM,
DATE_FORMAT(s.ACTUAL_SHIPMENT_DATE,"%Y-%m") AS ACTUAL_SHIPMENT_DATE
FROM 发货表 s
INNER JOIN 编码表 c ON s.ITEM_CODE=c.`code`
WHERE 1=1 ',@strWhere,
'GROUP BY DATE_FORMAT(s.ACTUAL_SHIPMENT_DATE,"%Y-%m"),c.product,c.project,c.`code`
ORDER BY DATE_FORMAT(s.ACTUAL_SHIPMENT_DATE,"%Y-%m"),c.product,c.project,c.`code`');
-- SELECT @strSelect;
-------------------
--4.这里先处理执行释放这个插入语句@strSelect,固定流程,可以减少每次执行SQL的语法分析
PREPARE stmt FROM @strSelect; -- 处理动态sql语句
EXECUTE stmt ; -- 执行sql语句
DEALLOCATE PREPARE stmt; -- 释放PREPARE
-- SELECT * FROM tempItem;
------------------------
--5.根据临时表的转换好的产品发货数据,将时间内容动态行转列后存储在@EE用户变量当中
-- \' 斜杆是转义,因外层套了一组单引号了,里面还要用单引号 ,即转义单引号 \' => ' 或者 ''=>'
SET @EE='';
SELECT GROUP_CONCAT(DISTINCT CONCAT('SUM(IF(ACTUAL_SHIPMENT_DATE= \'',ACTUAL_SHIPMENT_DATE,'\',NUM,NULL)) AS "',ACTUAL_SHIPMENT_DATE,'"')) INTO @EE
FROM (SELECT * FROM tempCode) a;
--6.这个做空判断,主要是方便前端,如果查询的编码和时间段内没有数据,则返回"无数据"字段便于前端判空,就不用加载表格组件
IF @EE IS NULL THEN
SET @EE='SUM(IF(ACTUAL_SHIPMENT_DATE= "无数据",NUM,NULL)) AS "无数据",
SUM(IF(ACTUAL_SHIPMENT_DATE= "无数据",NUM,NULL)) AS "无数据",
SUM(IF(ACTUAL_SHIPMENT_DATE= "无数据",NUM,NULL)) AS "无数据"';
END IF;
--7.最终查询sql定义变量,查询临时表,将前面第5点得到的动态时间拼接到查询字段中,然后再同样还得按时间和产品、项目、编码分组,分组后的num字段求和发货量赋值给每个年月,
SET @QQ=CONCAT('SELECT
product,
project,
`code`,
SUM(NUM) AS num,',@EE,
' FROM tempCode
GROUP BY DATE_FORMAT(ACTUAL_SHIPMENT_DATE,"%Y-%m"), PRODUCT,PROJECT,`CODE`');
--8.执行@QQ查询语句变量
PREPARE stmt1 FROM @QQ; -- 处理动态sql语句
EXECUTE stmt1 ; -- 执行sql语句
DEALLOCATE PREPARE stmt1; -- 释放PREPARE
END $$
DELIMITER ;
这过程有些复杂,注释中我拆分成8个步骤分析就很明朗了。
- DELIMITER 定义分隔符, 头设置表示以这个符号表示介绍,最后在END后还需加上,尾部设置; 还原sql原生的分隔符
- DEFINER=`dbuser`@`%` 用户名@主机 , %表示localhost ,设置指定用户访问存储过程权限
- 参数模式包括三种:IN OUT INOUT
IN表示输入参数 OUT表示输出参数 INOUT表示输入参数又表示输出参数 - BEGIN ...END : 主体内容
- SET 定义系统变量和用户变量,用户变量定义可以使用set和select两种,
用户变量以‘@’开头,任意字符串命名。 - CONCAT 该函数是数据库内置函数,该需求中用来拼接入参条件、时间内容行转列、拼接多个sql
- TEMPORARY 临时表,能一定程度提升SQL的执行效率
2.Mybatis映射文件调用存储过程
mapper.java如下:
@Repository
Public interface proDao{
List<Map> getPro(String productName,String String projectName,String codeName,String startTime,String endTime);
}
dao.xml如下:
<select id='getPro' resultType="java.util.Map" statementType="CALLABLE" >
CALL RowToColumnPro(#{productName mode=IN},#{projectName mode=IN},#{codeName mode=IN},#{startTime mode=IN},#{endTime mode=IN})
</select>
语法:call 存储过程名称(#{参数名称,mode=参数模式},#{参数名称,mode=参数模式})
这里如果参数多,也可以定义一个<parameterMap>标签存储多个参数,然后传递在select 标签中,语句中用?传参 :{call saveuser(?, ?, ?)} ,这里注意最外层有个中括号{}
注意,在mybatis中参数模式全部要大写,否则编译时会报错
CALLABLE,告诉mybatis将要执行的是存储过程
接着就是给服务层调用这个dao接口了~,重点核心就在存储过程脚本编写与dao层调用存储过程。掌握起来