大多数情况下,FineReport直接在设计器里使用“数据集查询”,直接写SQL就能满足报表要求,但对于一些复杂的报表,有时候SQL处理并不方便,这时可以把查询结果在应用层做一些预处理后,再传递给报表,即所谓的“程序数据集”,FineReport的帮助文档上给了一个示例:
1 package com.fr.data; 2 3 import java.sql.Connection; 4 import java.sql.DriverManager; 5 import java.sql.ResultSet; 6 import java.sql.ResultSetMetaData; 7 import java.sql.Statement; 8 import java.util.ArrayList; 9 import com.fr.base.FRContext; 10 import com.fr.data.AbstractTableData; 11 import com.fr.base.Parameter; 12 13 public class ParamTableDataDemo extends AbstractTableData { 14 // 列名数组,保存程序数据集所有列名 15 private String[] columnNames = null; 16 // 定义程序数据集的列数量 17 private int columnNum = 10; 18 // 保存查询表的实际列数量 19 private int colNum = 0; 20 // 保存查询得到列值 21 private ArrayList valueList = null; 22 23 // 构造函数,定义表结构,该表有10个数据列,列名为column#0,column#1,。。。。。。column#9 24 public ParamTableDataDemo() { 25 // 定义tableName参数 26 this.parameters = new Parameter[] { new Parameter("tableName") }; 27 // 定义程序数据集列名 28 columnNames = new String[columnNum]; 29 for (int i = 0; i < columnNum; i++) { 30 columnNames[i] = "column#" + String.valueOf(i); 31 } 32 } 33 34 // 实现其他四个方法 35 public int getColumnCount() { 36 return columnNum; 37 } 38 39 public String getColumnName(int columnIndex) { 40 return columnNames[columnIndex]; 41 } 42 43 public int getRowCount() { 44 init(); 45 return valueList.size(); 46 } 47 48 public Object getValueAt(int rowIndex, int columnIndex) { 49 init(); 50 if (columnIndex >= colNum) { 51 return null; 52 } 53 return ((Object[]) valueList.get(rowIndex))[columnIndex]; 54 } 55 56 // 准备数据 57 public void init() { 58 // 确保只被执行一次 59 if (valueList != null) { 60 return; 61 } 62 // 保存得到的数据库表名 63 String tableName = parameters[0].getValue().toString(); 64 // 构造SQL语句,并打印出来 65 String sql = "select * from " + tableName + ";"; 66 FRContext.getLogger().info("Query SQL of ParamTableDataDemo: \n" + sql); 67 // 保存得到的结果集 68 valueList = new ArrayList(); 69 // 下面开始建立数据库连接,按照刚才的SQL语句进行查询 70 Connection conn = this.getConnection(); 71 try { 72 Statement stmt = conn.createStatement(); 73 ResultSet rs = stmt.executeQuery(sql); 74 // 获得记录的详细信息,然后获得总列数 75 ResultSetMetaData rsmd = rs.getMetaData(); 76 colNum = rsmd.getColumnCount(); 77 // 用对象保存数据 78 Object[] objArray = null; 79 while (rs.next()) { 80 objArray = new Object[colNum]; 81 for (int i = 0; i < colNum; i++) { 82 objArray[i] = rs.getObject(i + 1); 83 } 84 // 在valueList中加入这一行数据 85 valueList.add(objArray); 86 } 87 // 释放数据库资源 88 rs.close(); 89 stmt.close(); 90 conn.close(); 91 // 打印一共取到的数据行数量 92 FRContext.getLogger().info( 93 "Query SQL of ParamTableDataDemo: \n" + valueList.size() 94 + " rows selected"); 95 } catch (Exception e) { 96 e.printStackTrace(); 97 } 98 } 99 100 // 获取数据库连接 driverName和 url 可以换成您需要的 101 public Connection getConnection() { 102 String driverName = "sun.jdbc.odbc.JdbcOdbcDriver"; 103 String url = "jdbc:odbc:Driver={Microsoft Access Driver (*.mdb)};DBQ=D:\\FineReport_7.0\\WebReport\\FRDemo.mdb"; 104 String username = ""; 105 String password = ""; 106 Connection con = null; 107 try { 108 Class.forName(driverName); 109 con = DriverManager.getConnection(url, username, password); 110 } catch (Exception e) { 111 e.printStackTrace(); 112 return null; 113 } 114 return con; 115 } 116 117 // 释放一些资源,因为可能会有重复调用,所以需释放valueList,将上次查询的结果释放掉 118 public void release() throws Exception { 119 super.release(); 120 this.valueList = null; 121 } 122 }
这个示例我个人觉得有二个地方不太方便:
1、db连接串硬编码写死在代码里,维护起来不太方便,目前大多数b/s应用,对于数据库连接,通常是利用spring在xml里配置datasource bean,运行时动态注入
2、将查询出的结果,填充到数据集时,采用的是数字索引(见82行),代码虽然简洁,但是可读性比较差
折腾一番后,于是便有了下面的改进版本:
1 package infosky.ckg.fr.data; 2 3 import infosky.ckg.utils.AppContext; 4 import java.sql.Connection; 5 import java.sql.ResultSet; 6 import java.sql.Statement; 7 import java.util.LinkedHashMap; 8 import java.util.LinkedHashSet; 9 import javax.sql.DataSource; 10 import com.fr.base.Parameter; 11 import com.fr.data.AbstractTableData; 12 import com.fr.general.data.TableDataException; 13 14 public class ParameterLinkedHashSetDataDemo extends AbstractTableData { 15 16 private static final long serialVersionUID = 8818000311745955539L; 17 18 // 字段名枚举 19 enum FIELD_NAME { 20 EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY 21 } 22 23 private String[] columNames; 24 25 private LinkedHashSet<LinkedHashMap<String, Object>> rowData; 26 27 public ParameterLinkedHashSetDataDemo() { 28 this.parameters = new Parameter[] { new Parameter("jobId"), 29 new Parameter("minSalary"), new Parameter("maxSalary") }; 30 31 // 填充字段名 32 columNames = new String[FIELD_NAME.values().length]; 33 int i = 0; 34 for (FIELD_NAME fieldName : FIELD_NAME.values()) { 35 columNames[i] = fieldName.toString(); 36 i++; 37 } 38 39 } 40 41 @Override 42 public int getColumnCount() throws TableDataException { 43 return columNames.length; 44 } 45 46 @Override 47 public String getColumnName(int columnIndex) throws TableDataException { 48 return columNames[columnIndex]; 49 } 50 51 @Override 52 public int getRowCount() throws TableDataException { 53 queryData(); 54 return rowData.size(); 55 } 56 57 @Override 58 public Object getValueAt(int rowIndex, int columnIndex) { 59 queryData(); 60 int tempRowIndex = 0; 61 for (LinkedHashMap<String, Object> row : rowData) { 62 if (tempRowIndex == rowIndex) { 63 return row.get(columNames[columnIndex]); 64 } 65 tempRowIndex += 1; 66 } 67 return null; 68 } 69 70 // 查询数据 71 private void queryData() { 72 // 确保只被执行一次 73 if (rowData != null) { 74 return; 75 } 76 77 // 传入的参数 78 String jobId = parameters[0].getValue().toString(); 79 float minSalary = Float.parseFloat(parameters[1].getValue().toString()); 80 float maxSalary = Float.parseFloat(parameters[2].getValue().toString()); 81 82 // 拼装SQL 83 String sql = "select * from EMPLOYEES where JOB_ID='" + jobId 84 + "' and SALARY between " + minSalary + " and " + maxSalary; 85 86 rowData = new LinkedHashSet<LinkedHashMap<String, Object>>(); 87 88 Connection conn = this.getConnection(); 89 try { 90 Statement stmt = conn.createStatement(); 91 // 执行查询 92 ResultSet rs = stmt.executeQuery(sql); 93 while (rs.next()) { 94 // 填充行数据 95 // 注:字段赋值的顺序,要跟枚举里的顺序一样 96 LinkedHashMap<String, Object> row = new LinkedHashMap<String, Object>(); 97 row.put(FIELD_NAME.EMPLOYEE_ID.toString(), 98 rs.getInt(FIELD_NAME.EMPLOYEE_ID.toString())); 99 row.put(FIELD_NAME.FIRST_NAME.toString(), 100 rs.getString(FIELD_NAME.FIRST_NAME.toString())); 101 row.put(FIELD_NAME.LAST_NAME.toString(), 102 rs.getString(FIELD_NAME.LAST_NAME.toString())); 103 row.put(FIELD_NAME.EMAIL.toString(), 104 rs.getString(FIELD_NAME.EMAIL.toString())); 105 row.put(FIELD_NAME.PHONE_NUMBER.toString(), 106 rs.getString("PHONE_NUMBER")); 107 row.put(FIELD_NAME.HIRE_DATE.toString(), 108 rs.getDate(FIELD_NAME.HIRE_DATE.toString())); 109 row.put(FIELD_NAME.JOB_ID.toString(), 110 rs.getString(FIELD_NAME.JOB_ID.toString())); 111 row.put(FIELD_NAME.SALARY.toString(), 112 rs.getFloat(FIELD_NAME.SALARY.toString())); 113 rowData.add(row); 114 } 115 rs.close(); 116 stmt.close(); 117 conn.close(); 118 } catch (Exception e) { 119 e.printStackTrace(); 120 } 121 122 } 123 124 // 获取数据库连接 125 private Connection getConnection() { 126 Connection con = null; 127 try { 128 DataSource dataSource = AppContext.getInstance().getAppContext() 129 .getBean("dataSource", DataSource.class); 130 con = dataSource.getConnection(); 131 } catch (Exception e) { 132 e.printStackTrace(); 133 return null; 134 } 135 return con; 136 } 137 138 // 释放资源 139 public void release() throws Exception { 140 super.release(); 141 this.rowData = null; 142 } 143 144 }
改进的地方:
1、getConnection方法,利用Spring注入datasource,当然为了注入方便,还需要一个辅助类AppContext
1 package infosky.ckg.utils; 2 3 import org.springframework.context.support.AbstractApplicationContext; 4 import org.springframework.context.support.ClassPathXmlApplicationContext; 5 6 public class AppContext { 7 private static AppContext instance; 8 9 private AbstractApplicationContext appContext; 10 11 public synchronized static AppContext getInstance() { 12 if (instance == null) { 13 instance = new AppContext(); 14 } 15 return instance; 16 } 17 18 private AppContext() { 19 this.appContext = new ClassPathXmlApplicationContext( 20 "spring/root-context.xml"); 21 } 22 23 public AbstractApplicationContext getAppContext() { 24 return appContext; 25 } 26 27 }
classes/spring/root-context.xml 里配置db连接
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://www.springframework.org/schema/beans 5 http://www.springframework.org/schema/beans/spring-beans.xsd"> 6 7 <bean id="dataSource" 8 class="org.springframework.jdbc.datasource.DriverManagerDataSource"> 9 <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" /> 10 11 <property name="url" value="jdbc:oracle:thin:@localhost:1521:XE" /> 12 <property name="username" value="hr" /> 13 <property name="password" value="hr" /> 14 </bean> 15 </beans>
2、将原来的数组,换成了LinkedHashSet<LinkedHashMap<String, Object>>,这样db查询结果填充到"数据集"时,处理代码的可读性就多好了(见queryData方法),但也要注意到LinkedHashSet/LinkedHashMap的性能较Array而言,有所下降,正所谓:有所得必有得失。但对于复杂的汇总统计报表,展示的数据通常不会太多,所以这个问题我个人看来并不严重。