簡單快速的將數據庫中的表引入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和多媒體編程。