自定义数据源在报表开发中比较常见,往往由于数据库使用受限(权限问题无法使用存储过程、数据库压力过大、计算逻辑过于复杂等)或需要在应用层进行数据加工都需要进行自定义数据源。而开发人员通过高级语言(如Java)实现报表自定义数据源时却存在诸多不便。
以Java为例,首先缺乏对批量数据计算的基础支持,完全没有针对记录集合(即对象数组)的过滤、分组、连接等计算;其次,不支持表达式参数,不支持把表达式作为参数传递进函数内部再计算的机制,表达式参数在记录集合上的运算中大量需要,过滤、分组、排序、连接等运算的参数都必须使用表达式,不能使用表达式参数将难以描述这些运算;再次,不支持动态数据结构,程序中用到的数据结构必须在编译前设计好,不可以由某个语句临时产生一个数据结构,而实际运算过程中的动态数据结构要求比比皆是。我们经常需要新的数据结构用以描述返回的结果集,有时还需要现有数据结构上添加计算字段以方便计算。没有动态数据结构的支持,所谓的批量结构化数据运算就成了空话。
下面通过一个例子进一步说明。
某网络平台需要监测查看一定周期内的用户状况,需要为运营部门出具日报、周报、月报、年报等报表,每类报表中均包含本期与上期、上上期数据比较,故涉及数据量较大。这里以其日报为例(月报年报只是统计周期不同),报表格式如下:
报表分为两部分,上半部分为用户明细数据,由于用户较多报表中只显示按本期在线时长排序后的前十名和后十名;下半部分为本期数据与上期、上上期的比较结果。
这里以润乾报表自定义数据源为例(主要代码,完整代码见附注)。
获取报表参数
Mapmap = ctx.getParamMap(false);
if (map != null) {
Iteratorit = map.keySet().iterator();
while (it.hasNext()) {
// 分别取得参数
Stringkey = it.next().toString();
data_date= map.get(key).toString();
}
}
执行数据库sql取数
Stringsql ="select a.useridauserid,a.first_logout_time,b.userid buserid,b.onlinetime bonlinetime,b.accountbaccount,"
+"c.userid cuserid,c.onlinetime conlinetime,c.accountcaccount,d.userid duserid,d.onlinetime donlinetime,d.account daccount,"
+"from"
+"(select v.userid, v.first_logout_time"
+"from t_dw_zx_valid_account v"
+"where v.standard_7d_time is not null) a,"
+"(select userid, sum(onlinetime) onlinetime, max(account)"
+"from t_dw_zx_account_status_day"
+"where logtime >= to_date('"+start_time_tm+"','yyyy-mm-dd hh:mi:ss')"
+"and logtime <"+end_time_tm+"','yyyy-mm-dd hh:mi:ss')"
+"group by userid"
+"having max(account) is not null) b,"
+"(select userid, sum(onlinetime) onlinetime, max(account)"
+"from t_dw_zx_account_status_day"
+"where logtime >= to_date('"+start_time_lm+"','yyyy-mm-dd hh:mi:ss')"
+"and logtime < to_date('"+start_time_tm+"','yyyy-mm-ddhh:mi:ss')"
+"group by userid"
+"having max(account) is not null) c,"
+"(select userid, sum(onlinetime) onlinetime, max(account)"
+"from t_dw_zx_account_status_day"
+"where logtime >= to_date('"+start_time_lm_1+"','yyyy-mm-dd hh:mi:ss')"
+"and logtime < to_date('"+start_time_lm+"','yyyy-mm-ddhh:mi:ss')"
+"group by userid"
+"having max(account) is not null) d"
+"where a.userid = b.userid(+)"
+"and a.userid = c.userid(+)"
+"and a.userid = d.userid(+))"
+"order by b.onlinetime desc";
为了降低程序的复杂度,数据初步加工(分组、过滤、排序)仍然使用sql完成。
获取列名
for(int i=0;i<colCount;i++){
colName.add(rsmd.getColumnName(i+1));//列名
type= rsmd.getColumnType(i+1);
}
读取表数据,将其存入List
while (rs.next()) {
List<Object>rowData = newArrayList<Object>();
for(int i=0;i<colCount;i++){
rowData.add(rs.getObject(i+1));
System.out.println("rowData"+i+"="+rowData.get(i));
}
data.add(rowData);
}
构造数据集ds1
DataSet ds1 = new DataSet("ds1");
for (int i = 0; i <colName.size(); i++) {
ds1.addCol(colName.get(i));// 设置数据集的字段
}
Rowrr = null;
遍历List计算汇总值
for(inti=0;i<data.size();i++){
List<Object>row_data = data.get(i);
boolean flag1=false;
boolean flag2=false;
boolean flag3=false;
boolean flag4=false;
boolean flag5=false;
for(intj=0;j<row_data.size();j++){
Objectsingle_data = row_data.get(j);
Stringstr_single_data = single_data.toString();
/****************计算汇总值************************/
if(j==3 &&single_data!=null){//buserid is not null
flag1=true;
}elseif(j==6 &&single_data!=null){//cuserid is not null
flag2=true;
}elseif(j==9 &&single_data!=null){//duserid is not null
flag3=true;
}elseif(j==2){
if(str_single_data.compareTo(start_time_lm)>=0&& str_single_data.compareTo(start_time_tm)<0){
flag4=true;//
}
if(str_single_data.compareTo(start_time_lm)<0){
flag5=true;
}
}
if (flag2&&flag3){
count1++;
}
if(flag3&&!flag1){
count2++;
}
if(flag4){
count3++;
}
if(flag1&&flag4){
count4++;
}
if(!flag3&&flag2&&flag5){
count5++;
}
if(!flag1&&!flag3&&flag2&&flag5){
count6++;
}
}
前十名数据
if(i<=10){
// 设置数据集中的数据
rr= ds1.addRow();
for (int j = 0; j<row_data.size(); j++) {
rr.setData(j+ 1, row_data.get(j)); }
}
}
后十名
for(inti=0;i<data.size();i++){
List<Object>row_data = data.get(i);
if(i>data.size()-10){
// 设置数据集中的数据
rr= ds1.addRow();
for (int j = 0; j<row_data.size(); j++) {
rr.setData(j+ 1, row_data.get(j));
}
}
}
这里还不包括获取数据库连接、计算前n天和后n天日期的代码;而且数据的分组排序操作还都由数据库(sql)完成,结果还是得写一段冗长的代码,Java在处理结构化数据时的缺点通过这里例子可以更深刻的感受到,如开篇所讲。
下面我们看集算器实现的方案:
A4-A6:进行数据过滤
A7-A9:按userid分组
A10:将以上结果集进行关联
A11:基于A10进行过滤后按在线时长排序
A12:新序表,用于读取前后十名记录
A13-A14:通过序号分别取前后十名记录
A15-A20:计算汇总值
A22:将前十名、后十名记录以及汇总值分别以不同结果集通过集算器JDBC返回给报表
这里看到,相对Java存在的缺点,在集算器中均可得到较好解决。集算器脚本可以分步编写,按照自然思维实现业务逻辑代码,较为清晰,代码长度急剧缩短,使得我们在一个屏幕内即可读完整个程序代码,这对修改维护很有帮助;另外,对于有序运算的有效支持使得取前后十名(A13、A14)非常容易(不必像Java必须写个循环完成)。这样既不给数据库增加压力,又不会像使用Java代码那样出现种种不便。
集算器只做数据计算,不负责展现,但集算器提供的JDBC可以为应用程序提供数据源支持,这里数据展现工作仍然由报表工具(以润乾报表为例)完成。
数据集设置
采用存储过程数据集接收集算器返回的三个结果集;类存储过程的调用方式,其中”cusInfo”为集算器脚本名称。
报表模板及表达式
报表工具只需根据集算器的输出结果进行简单的展现即可。
package com.runqian.report4.dataset;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.runqian.report4.dataset.DataSet;
import com.runqian.report4.dataset.IDataSetFactory;
import com.runqian.report4.dataset.Row;
import com.runqian.report4.usermodel.Context;
import com.runqian.report4.usermodel.DataSetConfig;
publicclass UserStatus implements IDataSetFactory {
public DataSetcreateDataSet(Context ctx, DataSetConfig dsc, boolean isinput) {
Stringdata_date = "";
int count1 = 0;
int count2 = 0;
int count3 = 0;
int count4 = 0;
int count5 = 0;
int count6 = 0;
// 取得参数列表并分别取得它的参数名与值,宏与之类似
Mapmap = ctx.getParamMap(false);
if (map != null) {
Iteratorit = map.keySet().iterator();
while (it.hasNext()) {
// 分别取得参数
Stringkey = it.next().toString();
data_date= map.get(key).toString();
}
}
data_date+=" 00:00:00";
Stringstart_time_tm = data_date;
Stringend_time_tm = getSpecifiedDayBefore(data_date,1);
Stringstart_time_lm = getSpecifiedDayAfter(data_date,1);
Stringstart_time_lm_1 = getSpecifiedDayAfter(data_date,2);
// 获取系统数据源
StringdatasourceName = dsc.getDataSourceName();
if (datasourceName == null || "".equals(datasourceName))
// 判断用户是否对数据集设置了数据源名,如果没有,则直接读取系统默认的数据源
datasourceName= ctx.getDefDataSourceName();
Connectioncon;
List<String>colName = newArrayList<String>();//列名
List<List<Object>>data = newArrayList<List<Object>>();
try {
con= ctx.getConnectionFactory(datasourceName).getConnection();
System.out.println("得到的数据源是:" + con);
Statementstmt = con.createStatement();
// Stringsql ="select * from temp0528 order by bonlinetimedesc";
Stringsql ="select a.userid auserid,a.first_logout_time,b.useridbuserid,b.onlinetime bonlinetime,b.account baccount,"
+"c.userid cuserid,c.onlinetime conlinetime,c.accountcaccount,d.userid duserid,d.onlinetime donlinetime,d.account daccount,"
+"from"
+"(select v.userid, v.first_logout_time"
+"from t_dw_zx_valid_account v"
+"where v.standard_7d_time is not null) a,"
+"(select userid, sum(onlinetime) onlinetime, max(account)"
+"from t_dw_zx_account_status_day"
+"where logtime >= to_date('"+start_time_tm+"','yyyy-mm-dd hh:mi:ss')"
+"and logtime <"+end_time_tm+"','yyyy-mm-dd hh:mi:ss')"
+"group by userid"
+"having max(account) is not null) b,"
+"(select userid, sum(onlinetime) onlinetime, max(account)"
+"from t_dw_zx_account_status_day"
+"where logtime >= to_date('"+start_time_lm+"','yyyy-mm-dd hh:mi:ss')"
+"and logtime < to_date('"+start_time_tm+"','yyyy-mm-ddhh:mi:ss')"
+"group by userid"
+"having max(account) is not null) c,"
+"(select userid, sum(onlinetime) onlinetime, max(account)"
+"from t_dw_zx_account_status_day"
+"where logtime >= to_date('"+start_time_lm_1+"','yyyy-mm-dd hh:mi:ss')"
+"and logtime < to_date('"+start_time_lm+"','yyyy-mm-ddhh:mi:ss')"
+"group by userid"
+"having max(account) is not null) d"
+"where a.userid = b.userid(+)"
+"and a.userid = c.userid(+)"
+"and a.userid = d.userid(+))"
+"order by b.onlinetime desc";
ResultSetrs = stmt.executeQuery(sql);//待修改,有参数
ResultSetMetaDatarsmd = rs.getMetaData();
int colCount = rsmd.getColumnCount();//列数
for(int i=0;i<colCount;i++){
colName.add(rsmd.getColumnName(i+1));//列名
}
while (rs.next()) {
List<Object>rowData = newArrayList<Object>();
for(int i=0;i<colCount;i++){
rowData.add(rs.getObject(i+1));
System.out.println("rowData"+i+"="+rowData.get(i));
}
data.add(rowData);
}
con.close();
}catch(Exception ex) {
ex.printStackTrace();
}
//构造数据集
DataSetds1 = newDataSet("ds1");
for (int i = 0; i <colName.size(); i++) {
ds1.addCol(colName.get(i));// 设置数据集的字段
}
Rowrr = null;
for(inti=0;i<data.size();i++){
List<Object>row_data = data.get(i);
boolean flag1=false;
boolean flag2=false;
boolean flag3=false;
boolean flag4=false;
boolean flag5=false;
for(intj=0;j<row_data.size();j++){
Objectsingle_data = row_data.get(j);
Stringstr_single_data = single_data.toString();
/****************计算汇总值************************/
if(j==3 &&single_data!=null){//buserid is not null
flag1=true;
}elseif(j==6 &&single_data!=null){//cuserid is not null
flag2=true;
}elseif(j==9 &&single_data!=null){//duserid is not null
flag3=true;
}elseif(j==2){
if(str_single_data.compareTo(start_time_lm)>=0&& str_single_data.compareTo(start_time_tm)<0){
flag4=true;//
}
if(str_single_data.compareTo(start_time_lm)<0){
flag5=true;
}
}
if (flag2&&flag3){
count1++; //有效用户持续活跃数
}
if(flag3&&!flag1){
count2++; //上期是持续活跃的有效用户本期流
}
if(flag4){
count3++;
}
if(flag1&&flag4){
count4++;
}
if(!flag3&&flag2&&flag5){
count5++;
}
if(!flag1&&!flag3&&flag2&&flag5){
count6++;
}
}
/****************前十名************************/
if(i<=10){
// 设置数据集中的数据
rr= ds1.addRow();
for (int j = 0; j<row_data.size(); j++) {
rr.setData(j+ 1, row_data.get(j));
System.out.println("[top10]设置数据集的数据第" + i + "行第" + j + "列的值:"+ rr.getData(j+1));
}
}
}
for(inti=0;i<data.size();i++){
List<Object>row_data = data.get(i);
/****************后十名************************/
if(i>data.size()-10){
// 设置数据集中的数据
rr= ds1.addRow();
for (int j = 0; j<row_data.size(); j++) {
rr.setData(j+ 1, row_data.get(j));
}
}
}
System.out.println("count1~6="+count1+";"+count2+";"+count3+";"+count4+";"+count5+";"+count6);
/********************汇总值************************/
rr= ds1.addRow();
rr.setData(1,"持续活跃数");
rr.setData(2,count1);
rr.setData(3,"上期持续活跃用户本期流失数");
rr.setData(4,count2);
rr.setData(5,"上期新增有效用户数");
rr.setData(6,count3);
rr= ds1.addRow();
rr.setData(1,"上期新增的有效用户本期流失");
rr.setData(2,count4);
rr.setData(3,"上期有效用户回流数");
rr.setData(4,count5);
rr.setData(5,"上期回归的有效用户本期流失");
rr.setData(6,count6);
return ds1;
}
/**
* 获得指定日期的前n天
*
* @param specifiedDay
* @return
* @throws Exception
*/
publicstatic String getSpecifiedDayBefore(String specifiedDay,int n) {
Calendar c = Calendar.getInstance();
Date date = null;
try {
date = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(specifiedDay);
} catch (ParseException e) {
e.printStackTrace();
}
c.setTime(date);
int day = c.get(Calendar.DATE);
c.set(Calendar.DATE, day - n);
String dayBefore = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(c
.getTime());
return dayBefore;
}
/**
* 获得指定日期的后n天
*
* @param specifiedDay
* @return
*/
publicstatic String getSpecifiedDayAfter(String specifiedDay,int n) {
Calendar c = Calendar.getInstance();
Date date = null;
try {
date = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(specifiedDay);
} catch (ParseException e) {
e.printStackTrace();
}
c.setTime(date);
int day = c.get(Calendar.DATE);
c.set(Calendar.DATE, day + n);
String dayAfter = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")
.format(c.getTime());
return dayAfter;
}
}