Hacking swing: 一个JDBC表控件模型
2008-01-05 10:26:35 来源:WEB开发网核心提示: 简单快速的将数据库中的表引入Swing之内假如你已经用过数据库了,你很可能已经用过为数据库提供的维护和查询表的工具:命令行工具很适合作简单而直接的工作,Hacking swing: 一个JDBC表控件模型,但是很难完成大量数据的处理工作,在一次查询中写一条返回10或20列的数据就已经
简单快速的将数据库中的表引入Swing之内
假如你已经用过数据库了,你很可能已经用过为数据库提供的维护和查询表的工具:命令行工具很适合作简单而直接的工作,但是很难完成大量数据的处理工作。在一次查询中写一条返回10或20列的数据就已经很困难了-更糟的是由于换行而使每条记录都有很多行的情况,此时你根本无法区分一条记录在哪一行结束,另一条记录在哪一行开始。
假如能够将数据库中表的内容映射到Swing的JTable中是否好一些呢?加上一些JDBC语句,添加到JFrame上,顷刻之间就形成了图形界面。
建立连接
假如你同时用到JDBC和Swing,只需要一句话你就可以抓住要点:使用数据库表的元数据来建立Swing表控件模型。假如你还没用到它们,下面就是你需要了解的背景知识:JDBC提供许多抽象方法来存取数据库。对于一个数据库有效的java代码也应该对其他数据库有效,唯一的不同之处在于JDBC与不同的数据库建立连接时所需要提供的下面的一些字符串也是不同的:
¨ 一个驱动程序类的类名,提供各种不同的java.sql接口的实现。
¨ 一个连接到数据库的URL。这就意味着可能会用到socket,尽管这不是一定的。一些小的可嵌入的数据库就像你的应用程序一样可以存活于Java虚拟机中。
¨ 一个可选的用户名。
¨ 一个可选的密码。
一旦建立了连接,你就可以发送一些命令(创建、删除、或修改表),或者通过该连接,创建SQL语句来对数据库进行查询。你也能通过该连接得到数据库的元数据,例如它所支持的各种特性,某一字符串的最大长度等等。更重要的是,它可以让你知道数据库中都有哪些表,它们都有哪些字段,每个字段的数据类型是什么。
因此,假设连接到了数据库,并且知道该数据库中的一个表的表名,你就可以利用两次查询将表的内容用Java表现出来。第一次查询能得到该表的各字段的元数据并且将字段名和字段的类型构建成一个数组。这些能被适当地而且很好地映射到Java类中,至少支持你想支持的各种类型。第二次查询得到表中所有的数据。对于每行数据,对应字段它都有相应的值。这些数据可以放入一个二维数组中,该数组存放的是整个表的内容。
通过这两次查询,也就完成了要实现AbstractTableModel类中的抽象方法的所有预备:
¨ getRowCount():是你所创建的内容数组contents的长度。
¨ getColumnCount():假如没有内容则为0,否则为数组contents的第一项(该项也是一个数组,因为内容数组是一个二维数组)的长度。
¨ getValueAt():contents[row][col]的值。
AbstractTableModel已经完全的实现了getColumnClass()和getColumnName()两个方法,因此,前者总是返回Object.class,后者返回"A","B","C",等等;你也可以利用第一次查询的字段元数据覆盖这两个方法,以更好的实现它们。
例3-12演示JDBCTableModel如何被实现。
例3-12.由数据库连接生成的Swing表控件模型
import javax.swing.*;
import javax.swing.table.*;
import java.sql.*;
import java.util.*;
/** an immutable table model built from getting
metadata about a table in a jdbc database
*/
public class JDBCTableModel extends AbstractTableModel {
Object[][] contents;
String[] columnNames;
Class[] columnClasses;
public JDBCTableModel (Connection conn, String tableName)
throws SQLException {
super();
getTableContents (conn, tableName);
}
PRotected void getTableContents (Connection conn,
String tableName)
throws SQLException {
// get metadata: what columns exist and what
// types (classes) are they?
DatabaseMetaData meta = conn.getMetaData();
System.out.println ("got meta = " + meta);
ResultSet results =
meta.getColumns (null, null, tableName, null);
System.out.println ("got column results");
ArrayList colNamesList = new ArrayList();
ArrayList colClassesList = new ArrayList();
while (results.next()) {
colNamesList.add (results.getString ("COLUMN_NAME"));
System.out.println ("name: " +
results.getString ("COLUMN_NAME"));
int dBType = results.getInt ("DATA_TYPE");
switch (dbType) {
case Types.INTEGER:
colClassesList.add (Integer.class); break;
case Types.FLOAT:
colClassesList.add (Float.class); break;
case Types.DOUBLE:
case Types.REAL:
colClassesList.add (Double.class); break;
case Types.DATE:
case Types.TIME:
case Types.TIMESTAMP:
colClassesList.add (java.sql.Date.class); break;
default:
colClassesList.add (String.class); break;
};
System.out.println ("type: " +
results.getInt ("DATA_TYPE"));
}
columnNames = new String [colNamesList.size()];
colNamesList.toArray (columnNames);
columnClasses = new Class [colClassesList.size()];
colClassesList.toArray (columnClasses);
// get all data from table and put into
// contents array
Statement statement =
conn.createStatement ();
results = statement.executeQuery ("SELECT * FROM " +
tableName);
ArrayList rowList = new ArrayList();
while (results.next()) {
ArrayList cellList = new ArrayList();
for (int i = 0; i<columnClasses.length; i++) {
Object cellValue = null;
if (columnClasses[i] == String.class)
cellValue = results.getString (columnNames[i]);
else if (columnClasses[i] == Integer.class)
cellValue = new Integer (
results.getInt (columnNames[i]));
else if (columnClasses[i] == Float.class)
cellValue = new Float (
results.getInt (columnNames[i]));
else if (columnClasses[i] == Double.class)
cellValue = new Double (
results.getDouble (columnNames[i]));
else if (columnClasses[i] == java.sql.Date.class)
cellValue = results.getDate (columnNames[i]);
else
System.out.println ("Can't assign " +
columnNames[i]);
cellList.add (cellValue);
}// for
Object[] cells = cellList.toArray();
rowList.add (cells);
} // while
// finally create contents two-dim array
contents = new Object[rowList.size()] [];
for (int i=0; i<contents.length; i++)
contents[i] = (Object []) rowList.get (i);
System.out.println ("Created model with " +
contents.length + " rows");
// close stuff
results.close();
statement.close();
}
// AbstractTableModel methods
public int getRowCount() {
return contents.length;
}
public int getColumnCount() {
if (contents.length == 0)
return 0;
else
return contents[0].length;
}
public Object getValueAt (int row, int column) {
return contents [row][column];
}
// overrides methods for which AbstractTableModel
// has trivial implementations
public Class getColumnClass (int col) {
return columnClasses [col];
}
public String getColumnName (int col) {
return columnNames [col];
}
}
构造函数将实际的工作交给了getTableContents()方法,它负责刚才所提到的两次查询。通过连接得到DatabaseMetaData类的对象,之后调用getColumns()函数得到字段信息。这个方法的参数是catalog,schemaPattern,tableNamePattern,columnNamePattern;这里忽略了catalog,schemaPattern,然而假如你用的是一个复杂的数据库,可能你就需要具体的描述这两项。getColumns()返回一个结果集ResultSet,就像通常JDBC查询得到的结果集一样,对它进行迭代就可以了。
要得到字段名是很轻易的:只需要调用getString("COLUMN_NAME")方法就可以了。但要得到数据类型就要有趣得多,当调用getInt("DATA_TYPE")时会返回一个int型数据,该类型是java.sql.Types类的常数之一。在上例中,我们只是简单地将字符串类型和基本的数字类型恰当的映射到Java类中。TIMESTAMP是SQL中的一个有关时间(日期和时间)的类型,因此,要将它映射到Java的Date类。知道这些数据正确的数据类型将会使你更轻易并且能够更准确的使用相应的getXXX()方法来检索实际的表中的数据。
第二次查询是一个简单的查询语句SELECT * FROM tableName。由于查询中没有WHERE子句的约束,这将会得到表中所有记录组成的结果集ResultSet。我应该不需要提到下面这些,就是假如tableName是一张有数以百万计的记录的表,你生成的表控件模型就将无法存放到内存中。你应该知道这些的,对吗?
此外,你需要对结果集ResultSet进行迭代。每次假如results.next()返回true,则表示还有结果,此前在查询元数据的时候我们就已经清楚地知道表中每个字段信息了。这意味着在查询数据时需要调用getXXX()方法并且将字段名作为参数传入该方法,而且在此前的查询中,我们已经很清楚哪个字段应该使用哪一个getXXX()方法。因为Jtables有基于类的解析机制,它可以将数据中的数字转换成合适的类型(如Integer、Double等等)。你可能需要决定使用TableCellRenderer并通过Format类对表中所有的Double类型的数据进行格式化,即以特定的小数格式来格式化数字,或者将日期转换成"今天"或"在25个小时以前"这类与时间或日期相关的词,并在模型中对这类数据加粗显示会非常有用。
当查询结束后,你需要将ArrayList转换成数组。(数组一般提供能够快速查找到元素的方法)。AbstractTableModel类中的抽象方法的实现在前面已经提到过了,连同getColumnClass()和getColumnName()实现的覆盖和优化,columnNames,columnClasses和由该方法创建的内容数组contents的简单使用也讲完了。
测试
在你说"我不能运行这个例子,我没有数据库"之前,先别急!开源世界有你想要的东西。而且并不像Jboss那样那么大。HSQLDB,它原来的名字可能更被人所知,Hypersonic,这是一个用Java写的JDBC关系数据库引擎。它很小而且能够独自运行或在你的Java虚拟机里面运行。你可以到http://hsqldb.sourceforge.net/上下载HSQLDB。
无论你用的是什么数据库,你都需要一个驱动程序的名字,URL,用户名和密码来建立与数据库的连接。假如你已经拥有一个数据库了,我相信你应该很清楚这些。假如你刚下载了刚才所说的HSQLDB,你将会用到下列的信息:
¨ 驱动程序:org.hsqldb.jdbcDriver
¨ URL:jdbc:hsqldb:文件:testdb
¨ 用户名:sa
¨ 密码:(空)
假设你将HSQL添加为你的应用程序的一部分,这就需要你在classpath中添加上hsqldb.jar文件。你可能注重到,这将会在当前目录中产生一个testdb文件,之后你可以将其删除掉。你也可以给出其他的目录的全路径;具体的信息请参照HSQLDB的相关文档。
这个测试程序所期望的连接数据库的字符串的形式要像属性(properties)文件中的一样,如jdbctable.driver,jdbctable.url,jdbctable.user和jdbctable.pass。为了使之更方便一些,有二个方法将这些信息传入程序中:或者通过系统属性(通常在Java命令后加上参数-D来指定),或者写在一个叫jdbctable.properties的文件中。书上的源代码中有一个设置HSQLDB默认值的例子用的就是第二种方法。
为了测试JDBCTableModel,TestJDBCTable在数据库中生成了一个新的完整的表。该模型得到数据库的连接和这个表的表名以及数据库中的数据。然后测试类只是简单的利用模型创建一个JTable而且把它放入到JFrame中。例3-13给出了这个例子的源代码。
例子3-13.测试基于JDBC的表控件
import javax.swing.*;
import javax.swing.table.*;
import java.sql.*;
import java.util.*;
import java.io.*;
public class TestJDBCTable {
public static void main (String[] args) {
try {
/*
driver, url, user, and pass can be passed in as
system properties "jdbctable.driver",
"jdbctable.url", "jdbctable.user", and
"jdbctable.pass", or specified in a file
called "jdbctable.properties" in current
Directory
*/
Properties testProps = new Properties();
String ddriver = System.getProperty ("jdbctable.driver");
String durl = System.getProperty ("jdbctable.url");
String duser = System.getProperty ("jdbctable.user");
String dpass = System.getProperty ("jdbctable.pass");
if (ddriver != null)
testProps.setProperty ("jdbctable.driver", ddriver);
if (durl != null)
testProps.setProperty ("jdbctable.url", durl);
if (duser != null)
testProps.setProperty ("jdbctable.user", duser);
if (dpass != null)
testProps.setProperty ("jdbctable.pass", dpass);
try {
testProps.load (new FileInputStream (
new File ("jdbctable.properties")));
} catch (Exception e) {} // ignore FNF, etc.
System.out.println ("Test Properties:");
testProps.list (System.out);
// now get a connection
// note care to replace nulls with empty strings
Class.forName(testProps.getProperty
("jdbctable.driver")).newInstance();
String url = testProps.getProperty ("jdbctable.url");
url = ((url == null) ? "" : url);
String user = testProps.getProperty ("jdbctable.user");
user = ((user == null) ? "" : user);
String pass = testProps.getProperty ("jdbctable.pass");
pass = ((pass == null) ? "" : pass);
Connection conn =
DriverManager.getConnection (url, user, pass);
// create db table to use
String tableName = createSampleTable(conn);
// get a model for this db table and add to a JTable
TableModel mod =
new JDBCTableModel (conn, tableName);
JTable jtable = new JTable (mod);
JScrollPane scroller =
new JScrollPane (jtable,
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
JFrame frame = new JFrame ("JDBCTableModel demo");
frame.getContentPane().add (scroller);
frame.pack();
frame.setVisible (true);
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static String createSampleTable (Connection conn)
throws SQLException {
Statement statement = conn.createStatement();
// drop table if it exists
try {
statement.execute ("DROP TABLE EMPLOYEES");
} catch (SQLException sqle) {
sqle.printStackTrace(); // if table !exists
}
statement.execute ("CREATE TABLE EMPLOYEES " +
"(Name CHAR(20), Title CHAR(30), Salary INT)");
statement.execute ("INSERT INTO EMPLOYEES VALUES " +
"('Jill', 'CEO', 200000 )");
statement.execute ("INSERT INTO EMPLOYEES VALUES " +
"('Bob', 'VP', 195000 )");
statement.execute ("INSERT INTO EMPLOYEES VALUES " +
"('Omar', 'VP', 190000 )");
statement.execute ("INSERT INTO EMPLOYEES VALUES " +
"('Amy', 'Software Engineer', 50000 )");
statement.execute ("INSERT INTO EMPLOYEES VALUES " +
"('Greg', 'Software Engineer', 45000 )");
statement.close();
return "EMPLOYEES";
}
}
createSampleTable()方法就是你需要重写的来插入你自定义数据的方法。实际上,由于它返回的是你所创建的表的表名,你需要在数据库中创建许多不同的表来测试模型如何处理他们。或者利用一个循环生成多行数据并且看装载这些数据需要多长时间。
最后,当你运行程序的时候,TestJDBCTable生成了一个带有数据库表的内容的JFrame,如图3-9所示。
图3-9.将数据库中的数据填充到Jtable中
Joshua Marinacci是java.net上" The Java Sketchbook "的专栏作家,范围涉及Java客户端和网络编程。
Chris Adamson是ONJava和java.net的编辑,亚特兰大顾问,主攻Java,Mac操作系统X和多媒体编程。
赞助商链接