上周给以前做的模块新增了个报表统计的功能,感觉有点意思,写出来分享下,也方便以后回顾。
其实报表统计一般是要用到插件的,但是我这个是用html拼成的,虽然是low了点,但是要做好坑还是很多的。
本来想详细说一下业务和代码的,给sql都加上注释,但是发现业务这东西说清楚太麻烦了,所以就大概描述一下吧,最重要的还是最后面的一条sql。
用到的表为A01(人员基本信息表)、A11(人员培训信息表)、B00(机构信息表);A01为主表,查近五年的培训信息
首先做完第一个页面大概是长成这样子的
这种查询以前查过类似的,所以做的比较快
这是拼接html的后台代码
@Action
public String getBmPxxs() {
HttpServletRequest request = ActionContext.getActionContext().getHttpServletRequest();
Map map = new HashMap();
String B00 = request.getParameter("B00");// 查询的部门id
String year = request.getParameter("year");// 当前年
map.put("B00", B00);
map.put("year", year);
List
这块主要就是把查出来的list动态拼接成table,下面是查询的sql语句
这种统计类sql以前写的时候还是想了比较久的,第一次写的时候本来想找组长帮忙的,但是他发烧请假了,然后自己边想边写就写出来了,回头看其实挺简单的,主要就是一个行转列的操作,oracle中有个函数可以直接转,但是我没转成功,就直接用最原始的case when来写了,也比较容易理解吧。然后group by这个东西还是要慎用 (如果在返回集字段中,字段要么就要包含在Group By语句的后面,作为分组的依据;要么就要被包含在聚合函数中),然后就是个递归函数 START WITH...CONNECT BY PRIOR...来进行节点控制。
下面是导出功能的实现
@Action
public void exportBMhtml() throws Exception {
HttpServletRequest request = ActionContext.getActionContext().getHttpServletRequest();
HttpServletResponse response = ActionContext.getActionContext().getHttpServletResponse();
Map map = new HashMap();
// 获取制定的excel
String sourceSrc = System.getProperty("resourceFiles.location") + File.separator + "jypx" + File.separator;
// 获取模板名称
String templateName = request.getParameter("templateName");
// 获取需要导出的文件名称
String fileName = request.getParameter("fileName");
// 获取文件类型
String fileType = request.getParameter("fileType");
// 获取模板路径
String excelFilePath = sourceSrc + templateName;
try {
fileName = fileName + "." + fileType;
InputStream in = new FileInputStream(new File(excelFilePath));
Workbook wb = new Workbook(in);
Worksheet worksheet = wb.getWorksheets().get(0);
Cells cells = worksheet.getCells();
String B00 = request.getParameter("B00");
String year = request.getParameter("year");
map.put("B00", B00);
map.put("year", year);
List
上面这些都没啥好说的,就是一些和业务相关的逻辑,比较简单。
页面是长这样的
开始看到这个页面我是自闭的,有三个难点,第一是动态合并单元格,第二个是动态拼接合计行,第三就是要查出人员没有年份的培训记录,就是说16年即使这个人没有培训记录但是要查出来;
第一个难点和第二个难点我想的是把每个人都查一遍放到一个list里面再拼接,而不是一次查所有人,然后再在java里面做些逻辑处理就行了,然后第三点问了我旁边的sql大神,只要建张年份临时表,然后再cross join (笛卡尔积) A01表,再left jion A11表,最后再加些限制条件就OK了,反正他刚说的时候我也是很懵,我还以为他没听懂我要干嘛,没想到写出来的结果确实是我想要的,然后就开始动手写了,这种循环查每个人的培训信息的思路sql会很简单,但是java逻辑比较复杂。下面是拼成html的java代码
public String getRyPxxs2() {
HttpServletRequest request = ActionContext.getActionContext().getHttpServletRequest();
Map map = new HashMap();
String B00 = request.getParameter("B00");// 查询的部门id
String year = request.getParameter("year");// 当前年
map.put("B00", B00);
map.put("year", year);
StringBuffer sbtest = new StringBuffer();
// html表头
sbtest.append("");
sbtest.append("");
sbtest.append("姓名 ");
sbtest.append("年份 ");
sbtest.append("培训班 ");
sbtest.append("开始日期 ");
sbtest.append("结束日期 ");
sbtest.append("天数 ");
sbtest.append("学时 ");
sbtest.append(" ");
// 存放人员A00
List> A00_list = this.bdsoftMybatisUtils.selectList("jypx.cxtj.pxxxhz.queryA00", map);
Map map1 = new HashMap();
// html填充数据
for (int i = 0; i < A00_list.size(); i++) {
map1.put("A00", A00_list.get(i).get("A00"));
map1.put("year", year);
// 存放人员培训信息
List> rypxxx_list = this.bdsoftMybatisUtils.selectList("jypx.cxtj.pxxxhz.queryrypxxx",map1);
// 统计天数
int day = 0;
for (int j = 0; j < rypxxx_list.size(); j++) {
day += Integer.parseInt(rypxxx_list.get(j).get("天数").toString());
// 取到第一条数据时,合并姓名单元格
if (j == 0) {
sbtest.append("");
sbtest.append("" + rypxxx_list.get(0).get("姓名") + " ");
sbtest.append("" + rypxxx_list.get(j).get("年份") + " ");
sbtest.append("" + (rypxxx_list.get(j).get("培训班") == null ? "-" : rypxxx_list.get(j).get("培训班")) + " ");
sbtest.append("" + (rypxxx_list.get(j).get("开始日期") == null ? "-" : rypxxx_list.get(j).get("开始日期")) + " ");
sbtest.append("" + (rypxxx_list.get(j).get("结束日期") == null ? "-" : rypxxx_list.get(j).get("结束日期")) + " ");
sbtest.append("" + rypxxx_list.get(j).get("天数") + " ");
sbtest.append("" + rypxxx_list.get(j).get("学时") + " ");
sbtest.append(" ");
} else {
sbtest.append("");
sbtest.append("" + rypxxx_list.get(j).get("年份") + " ");
sbtest.append("" + (rypxxx_list.get(j).get("培训班") == null ? "-" : rypxxx_list.get(j).get("培训班")) + " ");
sbtest.append("" + (rypxxx_list.get(j).get("开始日期") == null ? "-" : rypxxx_list.get(j).get("开始日期")) + " ");
sbtest.append("" + (rypxxx_list.get(j).get("结束日期") == null ? "-" : rypxxx_list.get(j).get("结束日期")) + " ");
sbtest.append("" + rypxxx_list.get(j).get("天数") + " ");
sbtest.append("" + rypxxx_list.get(j).get("学时") + " ");
sbtest.append(" ");
}
}
// 个人培训信息遍历完后添加合计行
sbtest.append("");
sbtest.append("" + (Integer.parseInt(year) - 4) + "—" + year + "年合计" + " ");
sbtest.append(" ");
sbtest.append(" ");
sbtest.append(" ");
sbtest.append("" + day + " ");
sbtest.append("" + (day * 8) + " ");
sbtest.append(" ");
// 清空list
rypxxx_list.clear();
}
sbtest.append("
");
return sbtest.toString();
}
下面是导出功能的后台代码
@Action
public void exportRYhtml() throws Exception {
HttpServletRequest request = ActionContext.getActionContext().getHttpServletRequest();
HttpServletResponse response = ActionContext.getActionContext().getHttpServletResponse();
Map map = new HashMap();
// 获取制定的excel
String sourceSrc = System.getProperty("resourceFiles.location") + File.separator + "jypx" + File.separator;
// 获取模板名称
String templateName = request.getParameter("templateName");
// 获取需要导出的文件名称
String fileName = request.getParameter("fileName");
// 获取文件类型
String fileType = request.getParameter("fileType");
// 获取模板路径
String excelFilePath = sourceSrc + templateName;
try {
fileName = fileName + "." + fileType;
InputStream in = new FileInputStream(new File(excelFilePath));
Workbook wb = new Workbook(in);
Worksheet worksheet = wb.getWorksheets().get(0);
Cells cells = worksheet.getCells();
String B00 = request.getParameter("B00");
String year = request.getParameter("year");
map.put("B00", B00);
map.put("year", year);
List> A00_list = new ArrayList>();
A00_list = this.bdsoftMybatisUtils.selectList("jypx.cxtj.pxxxhz.queryA00", map);
Map map1 = new HashMap();
// 存放表头
List list_BT = new ArrayList<>();
list_BT.add("姓名");
list_BT.add("年份");
list_BT.add("培训班");
list_BT.add("开始日期");
list_BT.add("结束日期");
list_BT.add("天数");
list_BT.add("学时");
// excel表头
cells.get(0, 0).putValue((Integer.parseInt(year) - 4) + "—" + year + "年人员参训情况汇总表");
cells.merge(0, 0, 1, 7);// 合并行,列(起始行,起始列,合并行数,合并列数)
cells.get(1, 0).putValue(list_BT.get(0));
cells.setColumnWidth(0, 15); // 设置行宽
cells.get(1, 1).putValue(list_BT.get(1));
cells.setColumnWidth(1, 22); // 设置行宽
cells.get(1, 2).putValue(list_BT.get(2));
cells.setColumnWidth(2, 45); // 设置行宽
cells.get(1, 3).putValue(list_BT.get(3));
cells.setColumnWidth(3, 18); // 设置行宽
cells.get(1, 4).putValue(list_BT.get(4));
cells.setColumnWidth(4, 18); // 设置行宽
cells.get(1, 5).putValue(list_BT.get(5));
cells.setColumnWidth(5, 13); // 设置行宽
cells.get(1, 6).putValue(list_BT.get(6));
cells.setColumnWidth(6, 13); // 设置行宽
// 填充数据起始行数
int startCol = 2;
// 填充数据
for (int i = 0; i < A00_list.size(); i++) {
map1.put("A00", A00_list.get(i).get("A00"));
map1.put("year", year);
// 存放人员培训信息
List> rypxxx_list = this.bdsoftMybatisUtils.selectList("jypx.cxtj.pxxxhz.queryrypxxx", map1);
// 统计天数
int day = 0;
for (int j = 0; j < rypxxx_list.size(); j++) {
day += Integer.parseInt(rypxxx_list.get(j).get("天数").toString());
for (int k = 0; k < 7; k++) {
// 当循环到个人的第一条培训信息
if (j == 0 && k == 0) {
cells.get(startCol, k).putValue(rypxxx_list.get(j).get(list_BT.get(k)));
// 合并单元格
cells.merge(startCol, 0, rypxxx_list.size() + 1, 1);// 合并行,列(起始行,起始列,合并行数,合并列数)
} else if (k != 0) {
cells.get(startCol, k).putValue(rypxxx_list.get(j).get(list_BT.get(k)));
}
}
startCol += 1;
}
cells.get(startCol, 1).putValue((Integer.parseInt(year) - 4) + "—" + year + "年合计");
cells.get(startCol, 5).putValue(day);
cells.get(startCol, 6).putValue(day * 8);
startCol += 1;
}
// 调整样式
for (int i = 0; i < startCol; i++) {
for (int j = 0; j < 7; j++) {
AddBorder(cells, i, j);
AddBorders(cells, i, j);
}
}
response.addHeader("Content-Disposition",
"attachment;filename=" + new String(java.net.URLEncoder.encode(fileName, "UTF-8")));
response.addHeader("Content-Type", "application/octet-stream; charset=UTF-8");
wb.save(response.getOutputStream(), SaveFormat.XLSX);
} catch (Exception e) {
throw e;
}
}
这个是sql
写完运行测试,然后问题就来了,点开这个页面的时候发现啥都没有,看了下前端和后台都没报错,然后我就纳闷了,怎么没数据呢,好歹报个错啊,惆怅了十几秒后数据出来了,原来是因为加载太慢了,可以看到上面的代码查询的时候用了三层for循环,从数据库里查了几千次,运行完需要十几秒的时间。这样肯定是不行的,不然客户打开还以为死机了,所以我想加个类似这种提示框,但是加上去之后这个框怎么也去不掉(恶心的ext.js)。没办法了,只能硬着头皮去重构优化sql让它查快一点,想让它查快就不能查几千次,只能一次性把数据查出来,但是这样的话,之前提到的第一个难点和第二个难点就很难处理了。
虽然不好写,但是经过大神的指点后还是能看到希望的。查询结果为单个list的话,得知道什么时候开始合并单元格,以及要合并多少行的问题只能在sql里面解决,所以写起来就很复杂了,以下是优化后的java代码和sql
// 加载人员培训信息html
@Action
public String getRyPxxs() {
HttpServletRequest request = ActionContext.getActionContext().getHttpServletRequest();
Map map = new HashMap();
String B00 = request.getParameter("B00");// 查询的部门id
String year = request.getParameter("year");// 当前年
int year_4 = Integer.parseInt(year) - 4;
map.put("B00", B00);
map.put("year", year);
map.put("heji", year_4 + "年-" + year + "合计");
StringBuffer sbtest = new StringBuffer();
// html表头
sbtest.append("");
sbtest.append("");
sbtest.append("姓名 ");
sbtest.append("年份 ");
sbtest.append("培训班 ");
sbtest.append("开始日期 ");
sbtest.append("结束日期 ");
sbtest.append("天数 ");
sbtest.append("学时 ");
sbtest.append(" ");
// 存放人员培训信息
List> pxxx_list = this.bdsoftMybatisUtils.selectList("jypx.cxtj.pxxxhz.queryRyInfoForPxxx",
map);
// html填充数据
for (int i = 0; i < pxxx_list.size(); i++) {
String rn = "0";
if (pxxx_list.get(i).get("RN") != null) {
rn = pxxx_list.get(i).get("RN").toString();
}
// 跨行数
int rowspan = Integer.parseInt(pxxx_list.get(i).get("记录条数").toString()) + 1;
// 取到第一条数据时,合并姓名单元格
if ("1".equals(rn)) {
sbtest.append("");
sbtest.append("" + pxxx_list.get(i).get("姓名") + " ");
sbtest.append("" + pxxx_list.get(i).get("年份") + " ");
sbtest.append("" + (pxxx_list.get(i).get("培训班") == null ? "-" : pxxx_list.get(i).get("培训班")) + " ");
sbtest.append("" + (pxxx_list.get(i).get("开始日期") == null ? "-" : pxxx_list.get(i).get("开始日期")) + " ");
sbtest.append("" + (pxxx_list.get(i).get("结束日期") == null ? "-" : pxxx_list.get(i).get("结束日期")) + " ");
sbtest.append("" + pxxx_list.get(i).get("天数") + " ");
sbtest.append("" + pxxx_list.get(i).get("学时") + " ");
sbtest.append(" ");
} else {
sbtest.append("");
sbtest.append("" + pxxx_list.get(i).get("年份") + " ");
sbtest.append("" + (pxxx_list.get(i).get("培训班") == null ? "-" : pxxx_list.get(i).get("培训班")) + " ");
sbtest.append("" + (pxxx_list.get(i).get("开始日期") == null ? "-" : pxxx_list.get(i).get("开始日期")) + " ");
sbtest.append("" + (pxxx_list.get(i).get("结束日期") == null ? "-" : pxxx_list.get(i).get("结束日期")) + " ");
sbtest.append("" + pxxx_list.get(i).get("天数") + " ");
sbtest.append("" + pxxx_list.get(i).get("学时") + " ");
sbtest.append(" ");
}
}
sbtest.append("
");
return sbtest.toString();
}
// 培训信息导出
@Action
public void exportRYhtml() throws Exception {
HttpServletRequest request = ActionContext.getActionContext().getHttpServletRequest();
HttpServletResponse response = ActionContext.getActionContext().getHttpServletResponse();
Map map = new HashMap();
// 获取制定的excel
String sourceSrc = System.getProperty("resourceFiles.location") + File.separator + "jypx" + File.separator;
// 获取模板名称
String templateName = request.getParameter("templateName");
// 获取需要导出的文件名称
String fileName = request.getParameter("fileName");
// 获取文件类型
String fileType = request.getParameter("fileType");
// 获取模板路径
String excelFilePath = sourceSrc + templateName;
try {
fileName = fileName + "." + fileType;
InputStream in = new FileInputStream(new File(excelFilePath));
Workbook wb = new Workbook(in);
Worksheet worksheet = wb.getWorksheets().get(0);
Cells cells = worksheet.getCells();
String B00 = request.getParameter("B00");
String year = request.getParameter("year");
int year_4 = Integer.parseInt(year) - 4;
map.put("B00", B00);
map.put("year", year);
map.put("heji", year_4 + "年-" + year + "合计");
List> pxxx_list = this.bdsoftMybatisUtils.selectList("jypx.cxtj.pxxxhz.queryRyInfoForPxxx", map);
// 存放表头
List list_BT = new ArrayList<>();
list_BT.add("姓名");
list_BT.add("年份");
list_BT.add("培训班");
list_BT.add("开始日期");
list_BT.add("结束日期");
list_BT.add("天数");
list_BT.add("学时");
// excel表头
cells.get(0, 0).putValue((Integer.parseInt(year) - 4) + "—" + year + "年人员参训情况汇总表");
cells.merge(0, 0, 1, 7);// 合并行,列(起始行,起始列,合并行数,合并列数)
cells.get(1, 0).putValue(list_BT.get(0));
cells.setColumnWidth(0, 15); // 设置行宽
cells.get(1, 1).putValue(list_BT.get(1));
cells.setColumnWidth(1, 22); // 设置行宽
cells.get(1, 2).putValue(list_BT.get(2));
cells.setColumnWidth(2, 45); // 设置行宽
cells.get(1, 3).putValue(list_BT.get(3));
cells.setColumnWidth(3, 18); // 设置行宽
cells.get(1, 4).putValue(list_BT.get(4));
cells.setColumnWidth(4, 18); // 设置行宽
cells.get(1, 5).putValue(list_BT.get(5));
cells.setColumnWidth(5, 13); // 设置行宽
cells.get(1, 6).putValue(list_BT.get(6));
cells.setColumnWidth(6, 13); // 设置行宽
// 填充数据
for (int i = 0; i < pxxx_list.size(); i++) {
String rn = "0";
if (pxxx_list.get(i).get("RN") != null) {
rn = pxxx_list.get(i).get("RN").toString();
}
// 合并行数
int rowspan = Integer.parseInt(pxxx_list.get(i).get("记录条数").toString()) + 1;
for (int j = 0; j < 7; j++) {
// 当循环到个人的第一条培训信息
if (j == 0 && "1".equals(rn)) {
cells.get(i + 2, j).putValue(pxxx_list.get(i).get(list_BT.get(j)) == null ? ""
: pxxx_list.get(i).get(list_BT.get(j)).toString());
// 合并单元格
cells.merge(i + 2, 0, rowspan, 1);// 合并行,列(起始行,起始列,合并行数,合并列数)
} else if (j != 0) {
cells.get(i + 2, j).putValue(pxxx_list.get(i).get(list_BT.get(j)) == null ? ""
: pxxx_list.get(i).get(list_BT.get(j)).toString());
}
}
}
// 调整样式
for (int i = 0; i < pxxx_list.size() + 2; i++) {
for (int j = 0; j < 7; j++) {
AddBorder(cells, i, j);
AddBorders(cells, i, j);
}
}
response.addHeader("Content-Disposition",
"attachment;filename=" + new String(java.net.URLEncoder.encode(fileName, "UTF-8")));
response.addHeader("Content-Type", "application/octet-stream; charset=UTF-8");
wb.save(response.getOutputStream(), SaveFormat.XLSX);
} catch (Exception e) {
throw e;
}
}
写完这条sql有种身体被掏空的感觉,查询时间也缩短到了一秒以内,整体思路就是t1(培训信息字表)left join t2(个人记录条数表),连完之后再union all t3(统计行表) ,这条sql能正常运行出来其中也是蛮多坑的,以前以为union的时候字段名相同就行了,现在才知道类型也要一样,所以字段基本都进行了to_char()处理,然后最精妙的还是
ROW_NUMBER () OVER (PARTITION BY ... ORDER BY ...) 函数的运用,这个函数确实帮了很大的忙。
写完上面那条sql后我都在怀疑是不是我写出来的,因为第二天我就看不懂我写的是什么了,更不想细看或者去改动,可能是看到那么多select就感觉太复杂了,懒得看,其实静下心看还是能看出来每个select在干嘛,我也不想写这么多查询,只是每个查询都是必要的,可能还有精简优化的余地我没发现。想了下这几个月我进步最大的还是写sql吧,越写越感叹于sql的博大精深,其实我感觉写sql也可以用面向对象来理解,说简单点就是把每张表当个整体,想要什么就查什么,无非就是一些连接操作和一些函数的运用,一层不行就套两层,两层不行就再套一层,套完之后验证一下数据的正确性,看是不是自己想要的结果,最后再当成一张表进行操作,不用理里面的查询是怎么实现的,虽然到最后可能自己都看不懂在干什么,但是只要思路清晰,写起来就得心应手,水到渠成了。