天天看點

Hacking swing: 一個JDBC表控件模型

簡單快速的将資料庫中的表引入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所示。

Hacking swing: 一個JDBC表控件模型

圖3-9.将資料庫中的資料填充到Jtable中

Joshua Marinacci是java.net上" The Java Sketchbook "的專欄作家,範圍涉及Java用戶端和網絡程式設計。

Chris Adamson是ONJava和java.net的編輯,亞特蘭大顧問,主攻Java,Mac作業系統X和多媒體程式設計。