Java数据库编程之【JDBC数据库例程】【自动生成报表】【六】

Java数据库编程【自动生成报表】【六】

  • [15.6 自动生成报表例程](#15.6 自动生成报表例程)

本文介绍一个数据库的自动生成报表例程。它是以数据源为数组的自动生成报表例程AutoReport.java作为基础。现在我们用数据库表做为数据源来实现自动生成报表打印功能。

15.6 自动生成报表例程

【例程15-11】数据源为数据库的自动生成报表例程AutoReport

在本书的前面"类和对象"章节我们已经介绍了,数据源为数组的自动生成报表例程AutoReport.java。现在我们用数据库表做为数据源来实现自动生成报表打印功能。
例程包含数据库环境创建的工具类和报表生成主类两部分:

  • 数据库环境创建工具类DBUtils.java,代码如下:
cpp 复制代码
package autoReport;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DBUtils {
	/***嵌入式derby数据库的三行定义语句***/
	static final String Driver = "org.apache.derby.jdbc.EmbeddedDriver";
	static final String DbProtocol="jdbc:derby:"; //数据库协议 
	//变量DbName其中的";create=true"表示如果数据库不存在,就新建之。
	static final String DbName = "D:\\StudentDB;create=true";
	
	/***访问数据库的用户和密码***/
	public static final String USER="root";
	public static final String PWD="newsky26";
	private static Connection con = null ;
	
	/***创建学生基本信息表student的SQL语句脚本***/
	public static final String CrtStudentTableSQL = 
			"CREATE TABLE student ( " +
			"学号  CHAR(6) PRIMARY KEY," +
			"姓名  VARCHAR(8)  NOT NULL," +
			"身高  REAL  check(身高>=0)," +
			"生日  DATE )" ;

	/***创建课程成绩表courseScore的SQL语句脚本***/
	private static final String CrtCourseScoreTableSQL = 
			"CREATE TABLE courseScore ( " +
			"学号 CHAR(6)  NOT NULL," +
			"语文 INT DEFAULT 0 check(语文>=0 and 语文<=100), " +
			"数学 INT DEFAULT 0 check(数学>=0 and 数学<=100), " +
			"物理 INT DEFAULT 0 check(物理>=0 and 物理<=100), " +
			"历史 INT DEFAULT 0 check(历史>=0 and 历史<=100), " +
			"FOREIGN KEY (学号) REFERENCES student(学号) )" ;
	
	/***查询学生基本信息表SQL语句脚本***/
	private static final String QueryStudentSQL ="SELECT * FROM student"; 
	/***测试(查询)视图View的SQL语句脚本***/
	private static final String QueryViewSQL ="SELECT * FROM viewStu"; 
	/***建立视图viewStu***/
	private static final String CrtViewSQL="CREATE VIEW viewStu" +
			" AS SELECT s.学号,姓名,语文,数学,物理,历史" +
			" FROM student s,courseScore c WHERE s.学号=c.学号";
	
	/***删除学生基本信息表student的SQL语句脚本***/
	private static final String DropStudentTableSQL ="DROP TABLE student"; 
	/***删除课程成绩表courseScore的SQL语句脚本***/
	private static final String DropCourseScoreTableSQL ="DROP TABLE courseScore";
	/***删除视图viewStu的SQL语句脚本***/
	private static final String DropViewSQL ="DROP VIEW viewStu"; 
	
	/***插入信息到学生基本信息表student的SQL语句脚本***/
	public static final String InitStudentTableSQL =
			"INSERT INTO student(学号, 姓名, 身高, 生日)"+ 
			"  VALUES ('200001','高玉宝',1.75,'2002-05-08'),"+
			"  ('200002','赵云',1.72,'2001-06-08'),"+
			"  ('200003','李云龙',1.76,'2000-07-18'),"+
			"  ('200004','钱江',1.70,'2001-10-08'),"+
			"  ('200005','欧阳明月',1.56,'2002-06-01')"; 
	
	/***插入学生课程成绩表courseScore数据***/
	private static final String InitCourseScoreTableSQL =
		"INSERT INTO courseScore(学号,语文,数学,物理,历史) " +
		" VALUES ('200001',95,80,65,70)," +
		" ('200002',78,95,88,76)," +
		" ('200003',82,60,75,85)," +
		" ('200004',84,78,86,85)," +
		" ('200005',82,72,87,75)";
	
	/***查询并打印学生基本信息的方法***/
	public static void Query学生(String sql) {
		try {
			if (con==null) //如果数据库连接不存在,建立数据库连接
				con = connectDB();
			
			Statement stmt = con.createStatement();
			ResultSet rSet = stmt.executeQuery(sql);//执行查询
			while(rSet.next()) {
				String id = rSet.getString("学号");
				String name = rSet.getString("姓名");
				double h = rSet.getDouble("身高");
				String hStr = String.format("%.2f", h);
				Date date = rSet.getDate("生日");
				String day = new SimpleDateFormat("YYYY-MM-dd").format(date);
				System.out.println(id+'\t'+name+'\t'+hStr+'\t'+day);
			}
			stmt.close();
		}catch (SQLException se) {
				se.printStackTrace();
		}
	}

	/***查询并打印视图viewStu信息的方法。测试确认视图***/
	public static void QueryView(String sql) {
		try {
			if (con==null) //如果数据库连接不存在,建立数据库连接
				con = connectDB();
			
			Statement stmt = con.createStatement();
			ResultSet rSet = stmt.executeQuery(sql);//执行查询
			while(rSet.next()) {
				String id = rSet.getString("学号");
				String name = rSet.getString("姓名");
				int cScore = rSet.getInt("语文");
				int pScore = rSet.getInt("物理");
				System.out.println(id+'\t'+name+'\t'+cScore+'\t'+pScore);
			}
			stmt.close();
		}catch (SQLException se) {
				se.printStackTrace();
		}
	}
	
	//建立数据库连接,成功返回true,否则返回false。
	public static Connection connectDB() {
		String url = DbProtocol + DbName;
		Connection con=null;
		try {  //加载驱动程序
			Class.forName(Driver);
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}	
		try {  //连接数据库
			con=DriverManager.getConnection(url, USER, PWD);
		} catch (SQLException e) {
			e.printStackTrace();
		}
		return con;
	}
	
	//根据参数,删除数据库表
	public static void DropTable(String dropTableSQL)
	{	UpdateDB(dropTableSQL);  }
	//根据建表参数,新建数据库表
	public static void CreateDbTable(String createTableSQL)
	{	UpdateDB( createTableSQL );  }
	
	//根据sql参数、user和password,更新数据库表
	public static void UpdateDB(String sql)
	{   
		Connection conn = null;
		conn = connectDB(); //联接数据库

		try(Statement stmt = conn.createStatement();){
			stmt.executeUpdate(sql); //执行更新
		}catch (SQLException se) {
			se.printStackTrace();
		}
	}
	
	public static void main(String[] args) {
		CreateDbTable(CrtStudentTableSQL); //创建学生表
		UpdateDB(InitStudentTableSQL); //向学生表插入数据
		Query学生(QueryStudentSQL); //查询、显示学生基本信息表所有记录
		
		CreateDbTable(CrtCourseScoreTableSQL); //创建课程成绩表
		UpdateDB(InitCourseScoreTableSQL); //向课程成绩表插入数据
		
		CreateDbTable(CrtViewSQL); //创建视图
		QueryView(QueryViewSQL); //从视图中查询

		/***下面是删除表的代码,执行这些代码是为了反复演示建表时才需要***/
		/*说明:由于CourseScore表依赖于学生表,删除时要先删CourseScore表*
		DropTable(DropViewSQL); //删除视图
		DropTable(DropCourseScoreTableSQL); //删除课程成绩表
		DropTable(DropStudentTableSQL); //删除学生表 
		****/
	}
}	 //数据库环境创建工具类DBUtils.java,代码结束。

数据库环境配置说明:main方法中有一部分代码注释了。如果放开注释代码,在程序运行结束时会自动清除程序所创建的数据库表和视图,这是为了反复进行演示建表测试。如果要为自动表格测试准备数据环境,则要恢复注释,然后再编译执行一次。

  • 自动生成报表例程AutoReport.java

1,先来看数据源为数组的版本

【例程5-19】自动生成报表例程AutoReport,数据源为数组的版本

对于如下图的"学生成绩单"表格信息,如果用户有需求提取不同的字段列,只要传入报表参数和报表数据信息,本例程即可自动生成报表打印。

报表参数(class ReportParam)包括:字段名称、字段类型、各字段的打印长度、选定的打印字段表、每页的行数。另外再加报表的数据源(报表数据的信息)。程序可根据参数自动生成打印报表。

cpp 复制代码
class ReportParam { //报表参数
	String[] fieldsName; //所有字段名称
	//字段类型:c表示文本型;n表示数值型
	char[] fieldsType; //所有字段的类型
	int[] fieldsLen; //所有字段的长度
	int[] fieldsSelected;  //选定出报表的字段
	//每页行数:可按需重新设定
	int rowsInPage = 28; //每页行数默认值是28
}  //报表参数定义结束。

与C语言中"汉字是双字节,ASCII码字符是单字节"不同;Java语言用双字节表示一个字符,无论是汉字还是ASCII码字符,而且Java字符串的长度单位也是双字节的。然而,通常情况,打印机的ASCII码字符只占一个字节长度,汉字则占两个字节长度。打印机的处理方式,与C语言是兼容的,但与Java语言不兼容。因此Java程序比C语言需要额外多做一些调整工作。如下的方法用于"统计打印信息中的ASCII码字符个数",在Java程序中是必需的:

cpp 复制代码
    public int getASCIICharCount(String str) {
        if (str == null) {  return 0;  }
           String reg = "[^\\x00-\\x7f]";  //非ASCII码字符的正则表达式
        return str.replaceAll(reg, "").length();
    }	//方法结束。

说明:理解这段代码需要有正则表达式相关知识。调用replaceAll()方法后得到的是一个替换后的新字符串,而原始字符串不受影响。

自动打印报表例程的程序源代码:实例程序:自动打印报表AutoReport.java

cpp 复制代码
package autoReport;
public class AutoReport {
	private char[][] lineSign = { 
		{'┏','━','┳','┓'},{'┃',' ','┃','┃'},
		{'┣','━','╋','┫'},{'┗','━','┻','┛'} };
	/*** hLines[]数组共有4个StringBuilder(64),分别用于保存:
	* hLines[0] 表格头第一行表格线    "┏━━┳━━┓";
	* hLines[1] 表格头第二行字段名称行"┃名称┃名称┃";
	* hLines[2] 表格中间分隔行表格线  "┣━━╋━━┫";
	* hLines[3] 表格最后一行表格线    "┗━━┻━━┛"; ***/
	private StringBuilder[] hLines={null,null,null,null};
	 
	public AutoReport(ReportParam param,String[][]report) 
	{
		mkReportHead(param); //生成报表头
		prnReport(param,report); //打印报表
	}
	
	/***生成报表头的方法***/
	private void mkReportHead( ReportParam param ) {
		int len,k,fieldsNum;
		String fldName;
		
		int[] fields = param.fieldsSelected;
		String[] fieldNames = param.fieldsName;

		//初始化hLines[]
		for (int i = 0; i < hLines.length; i++) 
			hLines[i] = new StringBuilder(64);		
		
		/*** 处理表头、表尾及字段名行和中间分隔线行***/
		fieldsNum = param.fieldsSelected.length;//选定的字段个数
		for (int i = 0; i < fieldsNum; i++) 
		{//switch语句"0:"处理首字段;"1:"处理中间字段,"default:"处理末字段。
			if (i==0) k = 0; //每行第一个字段特殊处理。
			else if (i==fieldsNum-1) k = 99; //末字段,特殊处理。
			else k = 1;   //中间字段处理方法都一样。
			
			len = param.fieldsLen[i]; //第i个字段的长度
			//定制指定长度的表头字段名字符串
			fldName= getFixedLenString(fieldNames[(fields[i]-1)],len,'c');
			switch (k) {
			case 0:  //每行首字段的处理
				//追加制表符的每行行首字符,'┏'、'┃'、'┣'、'┗'
				hLines[0].append(lineSign[0][0]);  //表格头第一行
				hLines[1].append(lineSign[1][0]);  //表格字段名行
				hLines[2].append(lineSign[2][0]); //表格中间分隔行
				hLines[3].append(lineSign[3][0]);  //表格最后一行
				/*** 以上是每行的第一个字段特殊字符处理。***/
				
				/*** 以下是各字段相同处理部分***/
				for (int l = 0; l < len; l++) { //循环len次
					hLines[0].append(lineSign[0][1]);  //表格头第一行
					hLines[2].append(lineSign[2][1]);  //表格中间分隔行
					hLines[3].append(lineSign[3][1]);  //表格最后一行
				}
				//表格字段名行 hLines[1]:设置字段名称。与上面三行处理不同
				hLines[1].append(fldName);
				break;

			case 1:   //每行的中间字段的处理
				//追加制表符的每个字段间制表符,如'┳'、'┃'、'╋'、'┻'等
				hLines[0].append(lineSign[0][2]);  //表格头第一行
				hLines[1].append(lineSign[1][2]);  //表格字段名称行
				hLines[2].append(lineSign[2][2]);  //表格中间分隔行
				hLines[3].append(lineSign[3][2]);  //表格最后一行
				
				/*** 以下是各字段相同处理部分***/
				for (int l = 0; l < len; l++) { //循环len次
					hLines[0].append(lineSign[0][1]);  //表格头第一行
					hLines[2].append(lineSign[2][1]);  //表格中间分隔行
					hLines[3].append(lineSign[3][1]);  //表格最后一行
				}
				//表格字段名行 hLines[1]:设置字段名称。与上面三行处理不同
				hLines[1].append(fldName);
				break;

			default:  //每行的最后一个字段的处理
				//追加制表符的每个字段间制表符,如'┳'、'┃'、'╋'、'┻'等
				hLines[0].append(lineSign[0][2]);  //表格头第一行
				hLines[1].append(lineSign[1][2]);  //表格字段名称行
				hLines[2].append(lineSign[2][2]);  //表格中间分隔行
				hLines[3].append(lineSign[3][2]);  //表格最后一行
				
				/*** 以下是各字段相同处理部分***/
				for (int l = 0; l < len; l++) { //循环len次
					hLines[0].append(lineSign[0][1]);  //表格头第一行
					hLines[2].append(lineSign[2][1]);  //表格中间分隔行
					hLines[3].append(lineSign[3][1]);  //表格最后一行
				}
				//表格字段名行 hLines[1]:设置字段名称。与上面三行处理不同
				hLines[1].append(fldName);
				/*** 以上的处理是各字段相同处理部分***/
				
				/*** 下面的处理是每行最后一个字段特殊处理。***/
				//追加表格的行结束字符:'┓'、'┃'、'┫'、'┛'
				hLines[0].append(lineSign[0][3]);  //表格头第一行
				hLines[1].append(lineSign[1][3]);  //表格字段名行
				hLines[2].append(lineSign[2][3]); //表格中间分隔行
				hLines[3].append(lineSign[3][3]);  //表格最后一行
				break;
			}
		}
	}
	
	/***打印报表的方法:数据源为二维字符串数组report,做了分页处理***/
	private void prnReport(ReportParam param,String[][]report) {
		int len,fieldsNum,rows;
		char type;
		String fixLenStr;
		
		int PgRows = param.pageRows;//每页行数
		int RptLen = report.length; //报表长度
		
		for (int k=0,m=1;k < RptLen;m++) {
			System.out.println("      学  生  成  绩  单");
			//打印表格表头
			System.out.println(hLines[0].toString());
			System.out.println(hLines[1].toString());
			
			/***处理报表数据行***/
			int[] fields = param.fieldsSelected;
			fieldsNum = param.fieldsSelected.length;//选定的字段个数
			StringBuilder dataLine = new StringBuilder(64);
			//循环打印表格中间分隔行和数据行
			rows = (PgRows<RptLen-k) ? PgRows:RptLen-k;
			for (; rows>0; rows--,k++) {
				/***表格数据行的生成***/
				dataLine.delete(0, dataLine.capacity());//清空dataLine数据
				for (int i = 0; i < fieldsNum; i++) { //每行中的各个字段的处理
					dataLine.append(lineSign[1][0]); //插入表格字符
					
					//定制指定长度的字符串
					len = param.fieldsLen[i]; //第i个字段的长度
					type = param.fieldsType[i];//第i个字段的类型
					fixLenStr = getFixedLenString(report[k][fields[i]-1],len,type);
					dataLine.append(fixLenStr);
				}
				dataLine.append(lineSign[1][3]); //插入行尾表格字符
				
				System.out.println(hLines[2].toString()); //打印表格中间分隔行
				System.out.println(dataLine.toString()); //打印数据行
			}
			//打印表格最后一行: 表格封底线行。
			System.out.println(hLines[3].toString());
			//打印页码
			for (int i = 0; i < hLines[3].toString().length(); i++) {
				System.out.print(' ');
			}
			System.out.println("第"+m+"页");
		}
	}	

	/*** 定制指定长度的字符串方法
	 * @param str 字段信息;
	 * @param len 字段列指定的打印长度;
	 * @param type 数据类型。c左对齐;n右对齐;
	 * @return 指定长度的字符串。
	 * ***/
	public String getFixedLenString(String str,int len, char type) 
	{
		if (str==null||str.length()==0) str="";
		int num = getASCIICharCount(str);//单字节字符数
/***计算需要补空格的长度***/
		int k = 2*(len-str.length())+num;//需要补空格的长度
		StringBuilder sBuilder = new StringBuilder(64);
		/***字符串小于指定打印长度的,用空格(' ')作为填充字符***/
		if (type=='c') { //左对齐,右补空格
			sBuilder.append(str);
			for (int i = 0; i < k; i++) sBuilder.append(' ');
		} 
		else {  //右对齐,左补空格
			for (int i = 0; i < k; i++) sBuilder.append(' ');
			sBuilder.append(str);
		}
		return sBuilder.toString();
	}
	
    /*** 计算字符串中单字节字符出现的次数
     * @param str
     * @return
     ****/
    public int getASCIICharCount(String str) {
        if (str == null) {  return 0;  }
        //非ASCII码字符的正则表达式
        String reg = "[^\\x00-\\x7f]"; 
        return str.replaceAll(reg, "").length();
    }
	
	public static void main(String[] args) {
		char[] fieldsType = {'c','c','n','n','n','n','n','n'};
		String[] fieldNames = {"姓名","学号","语文","数学","英语","物理","生物","历史"};
		int[] fieldsLen = {4,2,2,2,2,2,2,2};

		String[][] report = {
			{"常昊","0801","88","100","68","85","80","65"},
			{"刘国梁","0802","76","75","89","76","88","90"},
			{"常遇春","0601","90","68","78","86","92","70"},
			{"戚继光","0602","82","86","88","75","78","86"}};
		
		//选定出报表的字段,用序号表示: 1 代表字段"姓名",4代表字段"数学",依次类推。
		//选择前5个字段:"姓名","学号","语文","数学","英语"
		int[] fields01 = {1,2,3,4,5};
		int[] fields02 = {1,2,3,4,5,6,7,8}; //选择所有字段。
		int[] fields03 = {1,2,3,4,6,8}; //随机选择字段。
		
		//参数设定
		ReportParam param = new ReportParam();
		param.rowsInPage = 3; //重新设定每页行数
		param.fieldsLen =fieldsLen;
		param.fieldsType = fieldsType;
		param.fieldsName = fieldNames;
		
		param.fieldsSelected = fields01;
		new AutoReport(param, report);
		param.fieldsSelected = fields02;
		new AutoReport(param, report);
		param.fieldsSelected = fields03;
		new AutoReport(param, report);
	}
}

class ReportParam { //报表参数
	String[] fieldsName; //所有字段名称
	//字段类型:c表示文本型;n表示数值型
	char[] fieldsType; //所有字段的类型
	int[] fieldsLen; //所有字段的长度
	int[] fieldsSelected;//选定出报表的字段
	//每页行数:可按需重新设定
	int rowsInPage = 28; //每页行数默认值是28
}         /*** 实例程序:自动打印报表AutoReport.java 结束。***/

说明 :Eclipse控制台(Consols)的打印效果不理想。请把控制台(Consols)的测试结果复制到文本编辑器(如UltraEdit)中查看。下面这个是程序测试结果的一部分:

2,把上面的自动制表例程进行一下改造,数据源使用数据库的版本

自动生成报表例程,只需在原来的自动生成报表例程AutoReport.java中新增一个构造器方法和一个prnDbReport()方法,其他的代码都可复用。新增的代码如下:

cpp 复制代码
	/***数据源为数据库的构造器方法***/
	public AutoReport(ReportParam param) 
	{
		mkReportHead(param); //生成报表头
		prnDbReport(param); //打印数据源为数据库的报表
}

	/***打印报表的方法:数据源为数据库结果集rSet,无标题,也未进行分页***/
	private void prnDbReport(ReportParam param) {
		int len,fieldsNum;
		char type;
		String fixLenStr,str;
		
		//打印表格表头
		System.out.println(hLines[0].toString());
		System.out.println(hLines[1].toString());
		
		/***处理报表数据行***/
		fieldsNum = param.fieldsSelected.length;//选定的字段个数
		StringBuilder dataLine = new StringBuilder(64);
		
		//循环打印表格中间分隔行和数据行  数据源为数据库
		String sql ="SELECT * FROM viewStu";
		Connection con = DBUtils.connectDB(); //连接数据库 
		try {
			Statement stmt = con.createStatement();
			ResultSet rSet = stmt.executeQuery(sql);//执行查询
			while(rSet.next()) {
				System.out.println(hLines[2].toString()); //打印表格中间分隔行
				dataLine.delete(0, dataLine.capacity()); //清空dataLine数据
				for (int i = 0; i < fieldsNum; i++) { //每行中的各个字段的处理
					dataLine.append(lineSign[1][0]); //插入表格字符
					
					//定制指定长度的字符串
					len = param.fieldsLen[i]; //第i个字段的长度
					type = param.fieldsType[i];//第i个字段的类型
					if (type=='c') { //'c'型
						str = rSet.getString(i+1);
					} else { //'n'型
						str = ""+rSet.getInt(i+1);
					}
					fixLenStr = getFixedLenString(str,len,type);
					dataLine.append(fixLenStr);
				}
				dataLine.append(lineSign[1][3]); //插入行尾表格字符
				System.out.println(dataLine.toString());
			}
			
			//打印表格最后一行: 制表符表格封底线行。
			System.out.println(hLines[3].toString());
		} catch (SQLException se) {
			se.printStackTrace();
		}
}	//prnDbReport()方法结束。

在AutoReport.java代码文件头部需增加以下导入语句:

cpp 复制代码
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import autoReport.DBUtils;

自动生成报表例程AutoReport.java中的main方法需要更新为:

cpp 复制代码
	public static void main(String[] args) {
		/***数据源为数据源为数据库结果集rSet的测试方案***/
		char[] fieldsType = {'c','c','n','n','n','n'};
		String[] fieldNames = {"学号","姓名","语文","数学","物理","历史"};
		int[] fieldsLen = {3,4,2,2,2,2};
		
		int[] fields01 = {1,2,3,4};
		int[] fields02 = {1,2,3,4,5,6};
		//参数设定
		ReportParam param = new ReportParam();
		param.pageRows = 3; //重新设定每页行数
		param.fieldsLen =fieldsLen;
		param.fieldsType = fieldsType;
		param.fieldsName = fieldNames;
		param.fieldsSelected = fields01;
		new AutoReport(param);
		param.fieldsSelected = fields02;
		new AutoReport(param);		
}	//main方法结束。

编译运行AutoReport,测试结果如下(控制台显示有点问题,这是复制到文本编辑器UltraEdit的显示效果):

如要增加表格头和分页效果,请参考数据源为二维字符串数组的方法。