杜克的面包店 -- 一個JDBC訂購系統原型,第2部分
本文詳述了如何在Windows ME上使用Java 2 Platform, Standard Edition 1.3、Forte CE以及Microsoft Access。為了全面認識Forte CE的使用,請參閱Developing the Java 2D Art Applet Using Forte for Java Community Edition。也請參閱杜克的面包店 - 一個JDBC 訂購系統原型,第1部分,以獲取有關配置Microsoft Access的通用JDBC背景信息。
在杜克面包店 - 第1部分中,我創建了一個快速原型,向杜克面包店的擁有人Kate Cookie展示了如何可以使用Java和JDBC技術來創建一個訂購系統。自那時起,Kate就已經參加了一個Java編程班,并決定除了經營她的面包店外還要開始親自編寫Java代碼。因此我同意為她創建一個體系結構,作為她未來開發的起始點。我確定使用ResultSetMetaData來從數據庫中提取列名,以便在多數場合下,數據庫的變更會自動反映到代碼中。我也創建了Order Entry(訂單輸入)和Order Display(訂單顯示)窗口,因此如果添加或刪除了產品,這些改變就會自動在JTable顯示中反映出來。
這個軟件體系結構將程序邏輯與Swing GUI生成代碼分離開來,因此程序邏輯的改變不會嚴重影響到GUI,GUI的改變也不會影響到程序邏輯。這種分離是"模型-視圖-控制器"(Model-View-Controller ,MVC)設計模式所推薦的。設計模式可能是創建可重用和可維護的軟件的關鍵因素。James W. Cooper在他的Java Design Patterns -A Tutorial中很好地闡述了這些技術。我已經選擇了要實現將程序邏輯和GUI呈現相分離的最基本想法。為此,我已經定義了一個小的Controller類來解決這些事情。下面列出了構造函數和main方法。
public Controller() { mod = new Model(); dbm = new DBMaster( mod ); }//End Controller constructor public static void main (String args[]) { new Controller() ; }//End main method |
Model類
Model類包含了所有的程序邏輯并作為五個內部類實現,在各種JFrame擴展的GUI類中,這些內部類可作為JTable.setModel方法調用的參數,以創建JTable呈現用于數據顯示和數據輸入。Model也包含了許多數據庫處理方法,以便同JTable基礎結構配合使用。
首先創建了數據庫Connection對象,然后實例化五個內部類。如下代碼所示:
/* acquire the database connection object */ dbc = getConnectionObj ( "jdbc:odbc:BakeryBook" , "sun.jdbc.odbc.JdbcOdbcDriver" ); /* acquire the inner class objects */ cqtm = new CustQueryTableModel ( dbc ); cdtm = new CustDataTableModel ( dbc ); catm = new CustAddTableModel ( dbc ); cotm = new CustOrderTableModel ( dbc ); chtm = new CustHistTableModel ( dbc ); cqtm.getDefaultResultsAddresses(); cqtm.getDefaultResultsOrders(); |
程序把數據庫Connection對象dbc傳遞給每個內部類的構造函數。Connection對象是通過我編寫的名為getConnectionObj的方法創建的。下面列出了它的代碼,該代碼體現了創建這個對象的標準操作過程,這在"杜克的面包店 - 第1部分"中已作過討論。
public Connection getConnectionObj( String url, String driver ) { try { Class.forName( driver ); Connection db = DriverManager.getConnection( url ); connectionSuccess = true; return db; } catch ( ClassNotFoundException cnfex ) { /* process ClassNotFoundExceptions here */ cnfex.printStackTrace(); return null; } catch ( SQLException sqlex ) { /* process SQLExceptions here */ sqlex.printStackTrace(); return null; } catch ( Exception excp ) { /* process remaining Exceptions here */ excp.printStackTrace(); return null; }//End try-catch }//End getConnectionObj method |
布爾型的connectionSuccess被初始化為false,只有在控制流轉到其中的一個catch塊,才會將其值設為true。
下面列出了兩個方法調用,它們對于本應用程序的功能是非常重要的。
cqtm.getDefaultResultsAddresses(); cqtm.getDefaultResultsOrders();
這些調用為Addresses和Orders表創建了ResultSetMetaData對象。這些元數據對象會在整個應用程序中用到。如下是getDefaultResultsAddresses的代碼,它與getDefaultResultsOrders方法相類似。
public void getDefaultResultsAddresses() { try { statementAddresses = dbc.createStatement(); rsAddresses = statementAddresses.executeQuery ("SELECT * FROM Addresses"); rsAddressesMetaData = rsAddresses.getMetaData(); }//End try catch ( SQLException sqlex ) { jTextArea.append( sqlex.toString() ); }//catch catch ( Exception excp ) { // process remaining Exceptions here jTextArea.append( excp.toString() ); }//End try-catch }//End getDefaultResultsAddresses method |
這個方法創建了ResultSetMetaData對象rsAddressesMetaData。第一步是通過一個代表所有列的SQL *來從Addresses表中檢索數據,從而創建ResultSet對象rsAddresses。然后使用rsAddresses.getMetaData方法調用提取ResultSetMetaData 對象。rsAddressesMetaData 對象可在后面用于從Addresss表中提取列名和其他有用的信息。也創建了另一個類似的對象rsOrdersMetaData 。這兩對象可在整個應用程序中使用,這是通過使用cqtm.getAddressesMetaData方法和 cqtm.getOrdersMetaData方法調用來完成的,這兩個方法調用會返回一個ResultSetMetaData對象。如果在getDefaultResultsAddresses的try-cath邏輯塊中捕獲了一個錯誤,就會在 DBMaster窗口上的JTextArea對象中寫入錯誤信息。
DBMaster類
然后將 Model對象傳遞給DBMaster類(它是一個主Swing窗口),程序就跳轉到其他點以執行所有的其他功能。使用這種體系結構,每個Swing JFrame擴展類就可以通過將單一的 Model對象作為構造函數參數傳遞,來訪問所有的程序邏輯。當單擊按鈕產生其他的各種功能時,就會將Model對象傳遞到其他的JFrame擴展類。一旦GUI類構造函數得到該參數,就會提取這些內部類以供使用。讓我們來看一下從構造函數開始的一些DBMaster代碼。
public DBMaster( Model model ) { mod = model; cqtm = mod.getCustQueryTableModel(); cdtm = mod.getCustDataTableModel(); cotm = mod.getCustOrderTableModel(); chtm = mod.getCustHistTableModel(); catm = mod.getCustAddTableModel(); rsMeta = cqtm.getAddressesMetaData(); |
首先為全部類的作用域創建一個的 Model對象 mod。然后通過標準訪問器(accessor)方法使用mod對象來提取每個內部類對象。下面列出其中的一個存訪問器。
public CustQueryTableModel getCustQueryTableModel() { return cqtm; }
這個訪問器是作為一個標準的過程來實現的(即使不使用對象),以支持可能的功能修改。也從Address表中檢索了 ResultSetMetaData對象rsMeta,以提供有關后面要用到的Address表的信息。
下面列出了DBMaster中內部類對象的定義。
private Model.CustQueryTableModel cqtm; private Model.CustDataTableModel cdtm; private Model.CustOrderTableModel cotm; private Model.CustHistTableModel chtm; private Model.CustAddTableModel catm; |
接下來通過下面的代碼呈現了GUI。
SwingUtilities.invokeLater( new Runnable() { public void run() { initComponents (); setSize ( 750, 600 ); setVisible( true ); mod.setJTextArea(jTextArea1); if (mod.getConnectionSuccess()) jTextArea1.append ("Database Connection Successful\n"); else jTextArea1.append ("Database Connection Failed\n"); }//End run });//End invokeLater anonymous inner class |
SwingUtilities.invokeLater方法發出一個請求,以執行事件隊列中的一個代碼塊,然后從該代碼塊中返回并繼續執行。在本例中,創建了一個擴展了 Runnable的匿名內部類,因此該代碼可在它的run方法的內部執行。這保證了那些代碼是在事件調度線程上執行的,也保證了GUI呈現操作是"線程安全"的。
調用initComponents方法會執行所有的設置代碼,這些代碼是由Forte CE使用GridBagLayout來生成的。使用Forte CE的一般可行辦法是使用AbsoluteLayout來做GUI設計,然后將其轉換成GridBagLayout以生成可移植的代碼。這種辦法工作得不錯,但有時需要調整GridBagLayout的很多復雜屬性。除非您深入理解GridBagLayout,否則下面的這種辦法是比較容易的:在GridBagLayout和AbsoluteLayout間來回轉換并且操縱Swing組件,直到您取得需要的結果。通常,這個過程可以很快地完成。
getConnectionSuccess方法返回一個布爾型的數值,指出數據庫連接是否已經創建。如果是,就在jTextArea1對象中寫入一條消息,該對象是本應用程序的消息中心。其他的各種窗口有一些小的消息區域,但DBMaster中的JTextArea對象jTextArea1是主要的信息庫。
DBMaster窗口
DBMaster窗口看起來像下面這樣。
該窗口提供了兩個選項:Customer Info(客戶信息)和New Customer(新建客戶)。New Customer功能等同于作為另一窗口的一部分存在的一個功能,因此我們現在重點放在Customer Info上。如下代碼將幫我們實現Customer Info功能。Customer Info是由CustQuery對象custQuery來處理的。
if ( custQuery != null ) { /* if CustQuery window is open with data */ /* displayed, then reestablish it */ /* with no data, and kill hide CustData */ /* window, if open */ custQuery.closeCustDataWindow(); custQuery.setVisible(false); custQuery = new CustQuery( mod ); cqtm.setQueryString( null ); cqtm.setColString( getColumnName( 4 ) ); cqtm.setQueryAll( false ); cqtm.tableQuery(); cqtm.fire(); }//End if else { custQuery = new CustQuery( mod ); }//End if-else |
如果custQuery不為null,那么該custQuery窗口就已經處于活動狀態。第一個代碼塊展示了對custQuery.closeCustDataWindow方法的調用,該調用關閉了以前打開的各種被調用窗口(如果有的話),這些窗口可能干擾用戶的視界。然后讓現有的 CustQuery窗口變為不可見,并使用Model對象mod作為構造函數參數來重新實例化custQuery對象。接下來針對CustQueryTableModel對象cqtm執行了一些訪問器方法,該對象輸入查詢變量以執行cqtm.tableQuery方法。在本例中,其意圖是生成一個空的ResultSet對象,以便在呈現CustQuery窗口后,JTable顯示將以空內容的形式出現。下一節我們將看到這種表生成邏輯的一些細節問題。執行cqtm.tableQuery方法后,就會調用cqtm.fire方法激活表中數據。
CustQuery窗口
如下是帶有JTable列表的CustQuery窗口,其中列表是通過點擊Query All按扭得以初始化的。
如果通過單選按扭或單擊Query All按扭來初始化一個查詢,就會產生一個包含0條至完整的記錄列表,且包含來自Address表的所有行的列數據的一個子集的JTable。如果表中數據不為空,那么單擊其中的一行將自動產生另一個窗口(CustData),顯示該客戶記錄的列數值的完整集合,并且提供了其他的各種處理選項。
如下是CustQuery tableQuery方法,它控制著這張表中的數據的生成。
public void tableQuery() { try { if ( queryAll) { /* order by last name, first name */ query = "SELECT * FROM Addresses ORDER BY " + rsAddressesMetaData.getColumnName( 3) + "," + rsAddressesMetaData.getColumnName(2); }//End if else { /* order by last name, first name */ query = "SELECT * FROM Addresses WHERE " + colstring + " = " + "'" + qstring + "'" + " ORDER BY " + rsAddressesMetaData.getColumnName( 3) + "," + rsAddressesMetaData.getColumnName( 2); }//End if-else /* argument list below allows for use of */ /* ResultSet absolute method */ statement = dbc.createStatement (ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); rs = statement.executeQuery( query ); rsMeta = rs.getMetaData(); /* extract column names using ResultSetMetaData */ /* last name, first name, primary phone */ colheads[0] = rsMeta.getColumnName(2); colheads[1] = rsMeta.getColumnName(3); colheads[2] = rsMeta.getColumnName(4); colheads[3] = rsMeta.getColumnName(10); jTextArea.append( "Sending query: " + query + "\n" ); totalrows = new Vector(); while ( rs.next() ) { String[] record = new String[ rsMeta.getColumnCount() - 1 ]; record[0] = rs.getString( colheads[0] ); record[1] = rs.getString( colheads[1] ); record[2] = rs.getString( colheads[2] ); record[3] = rs.getString( colheads[3] ); totalrows.addElement( record ); }//End while loop jTextArea.append( "Query successful\n" ); }//End try catch ( SQLException sqlex ) { jTextArea.append( sqlex.toString() ); } catch ( Exception excp ) { jTextArea.append( excp.toString() ); }//End try-catch }//End tableQuery method |
如果布爾型的queryAll設為true,那么將把SQL字符串變量指派來從Addresses表中提取所有列和所有行,并將姓(last name)作為第一排序,名(first name)作為第二排序。else塊用于處理單擊上圖中三個單選按扭中的一個按鈕時所產生的查詢。文本輸入字段對應于 Last_Name、Primary_Phone和Company_Name。
單擊單選按鈕時所獲取的文本字符串(qstring和colstring)是通過使用set方法在內部類(cqtm)中注冊的。qstring變量是從三個JText字段之一中提取的。colstring變量包含了使用ResultSetMetaData getColumnName方法調用提取的列名。SQL字符串與queryAll例子的不同只在于它添加了WHERE語法,用以限制搜索滿足一定條件的行(通過qstring和colstring選擇值來指定)。
接下來使用下面的語法創建了Statement對象。
statement = dbc.createStatement (ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY);
傳遞給createStatement方法調用的參數啟用了ResultSet的滾動特性。在本應用程序中,我使用的是ResultSet方法absolute,它使得可以通過指定行號來訪問特定的行,而不必在數據中連續地滾動以查找一個指定的行。這種用法會在后面描述到。
然后根據SQL字符串查詢,使用Statement對象來生成一個ResultSet對象,該rs對象用于創建一個ResultSetMetaData對象rsMeta。
rs = statement.executeQuery( query ); rsMeta = rs.getMetaData();
使用ResultSetMetaData對象rsMeta,下面的代碼為JTable的注釋提取了列名。字符串數組變量包含First_Name、Last_Name、Primary_Phone和Company_Name。再請再注意一下,如果Addresses中的列名改變了,本應用程序將接受這些改變而不必修改軟件,因為數據并沒有被硬編碼。
colheads[0] = rsMeta.getColumnName(2); colheads[1] = rsMeta.getColumnName(3); colheads[2] = rsMeta.getColumnName(4); colheads[3] = rsMeta.getColumnName(10);
下面的while循環為JTable生成加載了數據結構。
totalrows = new Vector(); while ( rs.next() ) { String[] record = new String[ rsMeta.getColumnCount() - 1 ]; record[0] = rs.getString( colheads[0] ); record[1] = rs.getString( colheads[1] ); record[2] = rs.getString( colheads[2] ); record[3] = rs.getString( colheads[3] ); totalrows.addElement( record ); }//End while loop jTextArea.append( "Query successful\n" ); |
while循環假定了指針初始位于 ResultSet第一條記錄開始處的前面。如果有記錄可供讀取,那么每次調用next方法將返回一個布爾值true。如果該調用返回false,那么ResultSet的指針就已經移到了盡頭。一旦進入while循環,就會使用ResultSetMetaData來實例化一個字符串數組record,以指出檢索到的列數(如值rsMeta.getColumnCount() - 1所指出的)。因為Java數組是基于0的,因此其值必須加1。然后通過調用getString方法,就可以在該字符串數組(record)中加載ResultSet(rs)中的數值。 包含在colheads字符串數組中的列名用于按列名從rs中提取想要的數據。然后調用addElement方法將這條記錄添加到Vector對象totalrows中。因此我們正在創建的是使用字符串數組作為元素的Vector。當執行fire方法調用時,JTable表示基礎結構將自動呈現表中數據。
我已經提到,在整個應用程序中都會用到ResultSetMetaData。CustQuery類使用這些數據來注釋GUI上的某些數據字段。如果使用Forte CE的Component Inspector,就是從方法的執行中生成字段名。下面描述了實現這種設置的工具。Properties選項卡下的text按鈕使用戶可在其上輸入代碼,以生成屏幕上的文本。在這里,我有意輸入的方法是getColumnName。
getColumnName方法包含CustQuery 類中,下面列出了這個方法。它基本上只是為rsMeta.getColumnName (i)的執行提供了一個方便的環境。
public String getColumnName( int i ) { try { return rsMeta.getColumnName( i ); }//End try catch ( SQLException sqlex ) { jTextArea.append( sqlex.toString() ); return null; }//catch catch ( Exception excp ) { // process remaining Exceptions here jTextArea.append( excp.toString() ); return null; }//End try-catch }//End getColumnName method |
在使用帶有JTables的Forte CE中,數據模型對象通常是使用Component Inspector工具來創建的。
在JTable的Component Inspector選項卡中,單擊model。在字段右邊出現了三個點。單擊三個點將產生上面圖像中描繪的窗口。在Form Connection選項卡中,單擊標示為User Code的單選按扭,然后輸入表示列模型的對象的名稱。在本例中,它是Model的一個內部類--CustQueryTableModel對象cqtm。
在CustQuery類中,我們也使用Component Inspector來為JTable創建一個鼠標單擊事件處理方法。單擊Events選項卡中的mouseClicked按鈕,將產生一個已生成的方法名。下面描繪了Component Inspector窗口。
單擊return將導致Forte CE在CustQuery類中為JTable鼠標單擊處理生成一個空的方法。如下是帶有的表單擊處理代碼的該方法。
private void jTable2MouseClicked( java.awt.event.MouseEvent evt) { tablerow = jTable2.getSelectedRow(); cdtm.setTableRow(tablerow); if (custData != null) { custData.setVisible(false); custData = new CustData( mod ); }//End if else { custData = new CustData( mod ); }//End if-else } |
我使用JTable方法getSelectedRow來提取鼠標單擊事件所選擇的那個表行。也使用了一組方法來將這些數據傳送給CustDataTableModel內部類以備后用。
CustData窗口
如前面的一些例子那樣,如果一個CustData窗口是處于活動的,那么就先使其不可見,然后再重新實例化它。如果沒有活動的CustData窗口,那就實例化一個新的CustData窗口。
下面描繪了CustData窗口。
現在讓我們來看一下CustData類內部發生了什么。
如前面所討論的,對于我的所有JFrame擴展GUI類,大多數CustData構造函數代碼都是標準的。那里有的三個重要的獨特語句。
cdtm.setJTextField( jTextField1 ); cdtm.tableQuery(); cdtm.fire();
set語句將 JTextField對象jTextField1傳送給CustDataTableModel內部類,因此在處理期間,查詢邏輯可向CustData的GUI上的反饋字段寫入信息。
cdtm.tableQuery和cdtm.fire方法調用創建了CustData JTable。下面列出了tableQuery的代碼。
public void tableQuery() {
try {
rs = cqtm.getResultSet();
rsMeta = cqtm.getAddressesMetaData();
totalrows = new Vector();
/* point to the row corresponding */
/* to the |
rs = cqtm.getResultSet;語句用于提取單擊CustQuery JTable中的某個客戶時所生成的對象ResultSet 。這個ResultSet將總是只包含一行的數據。但我將這行的一些列名用作CustData JTable顯示中第一列的一些元素。數據元素用作第二列的元素。這聽起來有點復雜,但隨著我們進一步分析代碼,這一切將會變得明朗起來。
第二條語句是rsMeta = cqtm.getAddressesMetaData;,用于從它前面創建的庫中提取ResultSetMetaData對象。在本例中,該對象也可從rs.getMetaData的執行中生成。在本應用程序中,我們有時并不是那么容易訪問 ResultSet對象來執行這個getMetaData方法。這就是選擇getAddressesMetaData方法的理由。
接下來實例化了一個Vector對象totalrows。然后執行ResultSet 方法rs.absolute(tblrow+1);(JDBC 2.0中新增的功能) 。這將結果集指針移到對應于 JTable單擊行的記錄。注意,JTable 單擊行是基于0的索引,而ResultSet方法是基于1的,因此其值必須增加1。
自動增量索引段(在一個插入操作期間由數據庫軟件生成)是通過語句index = rs.getString(1); 來提取的。該字段的列名是通過下面的方法調用來取得的:colstring = rsMeta.getColumnName(1);。
然后使用下面的代碼塊來生成這張表。
for(int i=2;i<=rsMeta.getColumnCount();i++){ String[] rec = new String[30]; rec[0] = rsMeta.getColumnName(i); rec[1] = rs.getString(i); totalrows.addElement( rec ); }//End for loop |
上面的代碼創建了一個名為rec的字符串數組,其包含有數據對--列名及其值。然后將這對數據連續地添加到Vector對象totalrows中。一旦生成了這個數據結構,它就可用于自動的JTable生成(主要通過getValueAt 方法完成)。下面的列出了getValueAt方法。
public Object getValueAt(int row, int col) { return ((String[])totalrows.elementAt( row)) [col]; }
這個方法首先從 Vector對象totalrows中提取完整的一行(一對字符串值),然后根據整型 [col]從該數組中提取一個值,從而取得實際的字符串元素。
CustData窗口是應用程序程序處理的主要窗口。它提供了下面的一些操作。
- 更新
- 刪除
- 新建客戶
- 下訂單
- 訂單歷史
CustData窗口中的更新
為了從窗口中進行更新,請單擊表中字段使其可以編輯(右邊的列)。如果有字符串的話,光標就定位在現有字符串的后面。然后現有文本可以被替換或添加,另外,只要按下的回車鍵或用鼠標點擊其他字段,編輯過的值可以通過getValueAt方法訪問。如果沒有滿足這個要求,即使在表字段中有了可見的文本,但這些數據也不會輸入,更新將不能正常進行。但應該說明的是,JTable是非常靈活的,可以將其配置來對各種各樣的鍵盤驅動和鼠標驅動的事件作出反應。
如下代碼塊是在單擊Update按扭后觸發的。
/* update button clicked */ cdtm.tableUpdate();
然后執行CustDataTableModel cdtm.tableUpdate方法。下面列出了更新代碼。
public void tableUpdate() { try { int i; strgBuffer = getUpdateStatement ( rsMeta.getColumnCount( ), strgBuffer ); String strgArg = strgBuffer.toString(); pstatUpdate = dbc.prepareStatement( strgArg ); for ( i=0; i < cdtm.getRowCount( ); i++ ) { String tableString; if ( i == 2 ) { tableString = stripOut ( (String)cdtm.getValueAt( i, 1 ) ); } else { tableString = (String)cdtm.getValueAt( i, 1 ); }//End if-else pstatUpdate.setString( i+1, tableString ); }//End for loop pstatUpdate.setString( i+1, index ); pstatUpdate.executeUpdate(); jTextField.setText("Update successful"); jTextArea.append("Update successful\n"); cqtm.tableQuery(); cqtm.fire(); }//End try catch ( SQLException sqlex ) { jTextField.setText( "SQL Error-see DBMaster"); jTextArea.append( sqlex.toString() ); return; } catch ( Exception excp ) { // process remaining Exceptions here jTextField.setText( "Error-see DBMaster"); jTextArea.append( excp.toString() ); return; }//End try-catch }//End method tableUpdate |
tableUpdate方法使用了PreparedStatement對象。這種技術的主要優點是可提高執行速度。在多數場合,如果使用了這種技術,將會馬上把SQL語句發送到DBMS,然后在那里進行編譯。其結果是,PreparedStatement對象包含了一條經過預編譯的SQL語句。 然后通過setXXX方法將值提供給PreparedStatement對象。在本更新操作的情形下,將一個具有如下形式的字符串作為參數傳遞給Connection prepareStatement方法dbc.prepareStatement( strgArg ):
UPDATE Addresses SET First_Name = ?, Last_Name = ?, ? WHERE AddrID = ?
這個字符串是通過調用getUpdateStatement方法來生成的,該方法使用ResultSetMetaData訪問列名來生成SQL字符串。如下是getUpdateStatement方法的代碼。
public StringBuffer getUpdateStatement( int j, StringBuffer preparedSQL ){ preparedSQL = new StringBuffer( 700 ); preparedSQL.append("UPDATE Addresses SET "); try { int i; for ( i=2; i < j ; i++ ) { preparedSQL.append( rsMeta.getColumnName(i)+" = ?, "); }//End for loop preparedSQL.append( rsMeta.getColumnName(i)+" = ? WHERE " + rsMeta.getColumnName(1) + " = ? " ); }//End try catch ( SQLException sqlex ) { /* write to DBMaster msg area */ jTextArea.append( sqlex.toString() ); sqlex.printStackTrace(); }//catch catch ( Exception excp ) { // process remaining Exceptions here /* write to DBMaster msg area */ jTextArea.append( excp.toString() ); excp.printStackTrace(); }//End try-catch return preparedSQL; }//End getInsertStatement method |
該方法首先實例化一個StringBuffer對象,然后使用append方法調用來不斷地添加PreparedStatement SQL命令字符串。這里使用了Address表的所有列。WHERE搜索規則使用來定位由Addresses中第1列的自動編號字段AddrID更新的記錄。像上面一樣,在本應用程序中,列名是通過使用ResultSetMetaData對象來提取的。上面列出的getUpdateStatement 方法將該命令字符串返回到StringBuffer中,然后必須把該StringBuffer轉換成一個字符串對象,并將其傳遞給下面的語句。
String strgArg = strgBuffer.toString(); pstat = dbc.prepareStatement( strgArg );
Connection方法prepareStatement創建了PreparedStatement對象pstat,然后該對象通過下面的for循環為執行做好了準備,該循環為SQL字符串中的問號占位符?提供了數值。
for ( i=0; i < cdtm.getRowCount( ); i++ ) { String tableString; if ( i == 2 ) { tableString = stripOut ( (String)cdtm.getValueAt( i, 1 ) ); } else { tableString = (String)cdtm.getValueAt( i, 1 ); }//End if-else pstatUpdate.setString( i+1, tableString ); }//End for loop pstatUpdate.setString( i+1, index ); |
index字符串是在執行CustQueryTableModel queryTable方法期間創建的。它是Address表的第一列中的惟一自動編號字段。index字符串用來指出要更新的記錄。它的值是通過上面代碼塊中最后一條語句來輸入的:pstatUpdate.setString( i+1, index );。
注意上面代碼中的stripOut方法調用。這是用來移除所有非數字字符的方法。我只將它應用于Primary_Phone列的數值。
public String stripOut( String strg ) { /* strip out non-numeric characters */ String numStrg = new String(); for ( int i = 0; i < strg.length(); i++ ) { if ( strg.charAt(i) >= '0' && strg.charAt(i) <= '9' ) { numStrg += strg.substring(i,i+1); }//End if }//End for loop return numStrg; }//End stripOut method |
這些代碼返回一個字符串值。請記住,當使用String方法substring時,第二個索引必須增加1以便提取一個字符,然后將該字符反復地添加到輸出字符串變量中。
注意到下面這點也是重要的:cdtm.getValueAt方法調用是從JTable字段中提取數值,而不是從數據庫中提取數值。接受了JTable的所有字段后并執行下面的PreparedStatement方法,就會將用戶所做的任何變更輸入到數據庫中。 pstat.executeUpdate();
CustData窗口中的刪除
當用戶單擊CustData GUI上的delete按鈕時,就會執行下面的代碼。
cdtm.custDelete(); setVisible( false );
在執行custDelete方法后,CustData窗口就變得不可見,因此用戶可以返回到CustQuery GUI,然后選擇其他操作。如下是在CustDataTableModel內部類中觸發的一個方法。
public void custDelete() { try { batchError = false; statement = dbc.createStatement(); statement.addBatch( "DELETE FROM Addresses WHERE " + colstring + " = " + index ); statement.addBatch( "DELETE FROM Orders WHERE " + colstring + " = " + index ); batchErrorCodes = statement.executeBatch(); cqtm.setQueryAll( false ); cqtm.setQueryString( null ); cqtm.setColString( rsMeta.getColumnName( 4 ) ); }//End try catch ( SQLException sqlex ) { jTextArea.append( sqlex.toString() ); jTextField.setText( "SQL Error-See DBMaster" ); return; }//End catch catch ( Exception excp ) { // process remaining Exceptions here jTextArea.append( excp.toString( ) ); jTextField.setText( "Error-See DBMaster" ); return; }//End try-catch block batchError = false; for (int i=0; i < batchErrorCodes.length; i++ ) { if ( batchErrorCodes[i] > 0 ) { jTextArea.append( "Delete Successful\n" ); }//End if else if ( i == 1 && batchErrorCodes[i] == 0 ) { jTextArea.append( "No Orders Records to Delete\n" ); } else { jTextArea.append( "Delete Error\n" ); jTextField.setText( "Delete Error" ); batchError = true; }//End if-else if ( !batchError ) jTextField.setText( "Delete Successful" ); tableClear(); cqtm.tableQuery(); cqtm.fire(); }//End for loop }//End custDelete method |
批量更新(Batch Update)是JDBC核心API引入的一個新特性。批量更新可能更加能有效,而且對于事務處理可能是特別有用的,那里一組事務中的一個操作失敗可能要求回滾整組事務。我還沒實現這一點,但我編寫的那些代碼可以容易地適用于這種方案(假定DBMS驅動器支持它)。您可以閱讀White, Fisher等人編寫的JDBC API Tutorial and Reference, Second Edition,以獲取有關該主題的進一步信息。
讓我們來看一下批量更新是如何組裝起來的。
batchError = false; statement = dbc.createStatement();
這些代碼行定義了用于錯誤處理的布爾型 batchError變量,然后按通常的方式--執行Connection對象dbc的createStatement方法--定義了一個Statement對象。然后執行下面的代碼行。
statement.addBatch("DELETE FROM Addresses WHERE " + colstring + " = " + index ); statement.addBatch("DELETE FROM Orders WHERE " + colstring + " = " + index ); batchErrorCodes = statement.executeBatch(); |
addBatch方法調用將SQL命令字符添加到批隊列中。然后executeBatch方法完成了刪除操作。在我的表schema中,AddrID字段是惟一的自動編號字段,它是在向Addresses表中插入一條新記錄時由DBMS軟件生成的。當配置一個訂單時,我在Orders表的第二列中重復了那個值。我給它取了相同的名字AddrID。因此,Addresses的第一列是AddrID,Orders表的第二列也是AddrID。輸入到Orders表中的多張訂單具有來自Addresses表的AddrID名稱。Orders表的第一列叫作CustID。本方案簡化了刪除操作的語法。相同的刪除語法(DELETE FROM <Table> WHERE AddrID = n)適用于兩張表,它會刪除與指定客戶有關的所有記錄。
其余的代碼專門用于處理可能產生的各種錯誤條件。
batchError = false; for (int i=0; i < batchErrorCodes.length; i++ ) { if ( batchErrorCodes[i] > 0 ) { jTextArea.append( "Delete Successful\n" ); }//End if else if ( i == 1 && batchErrorCodes[i] == 0 ) { jTextArea.append( "No Orders Records to Delete\n" ); } else { jTextArea.append( "Delete Error\n" ); jTextField.setText( "Delete Error" ); batchError = true; }//End if-else if ( !batchError ) jTextField.setText( "Delete Successful" ); tableClear(); cqtm.tableQuery(); cqtm.fire(); }//End for loop |
executeBatch方法返回一個整型值的數組,其值對應于執行的操作數。在本例中,對于每個刪除操作返回一個值(即返回兩個值)。
從一個成功刪除操作中期望返回的代碼是1。如是沒有數據可刪除,那就返回0,這里的一種情形是客戶沒有配置任何訂單。這些值被處理后,就會清除表中的字段,然后將消息反饋寫到本地的JtextField和DBMaster中的JTextArea,然后使用前面創建的null查詢參數來調用tableQuery方法,以便在CustQuery GUI上返回一個JTable。然后在CustData類代碼控制下關閉了CustData窗口,它當然取消發送數據到JTextField 區域,但出于完整性考慮,我包含了它。
新建客戶
單擊CustData GUI上的New Cust按鈕將執行下面的代碼。
custAdd = new CustAdd( mod ); catm.tablePopulate(); catm.fire();
這些代碼實例化了CustAdd類,產生了它的窗口,然后使用tablePopulate方法調用來呈現它的JTable。下面是該方法的代碼。
public void tablePopulate() { try { rsMeta = cqtm.getAddressesMetaData(); totalrows = new Vector(); colstring = rsMeta.getColumnName( 1 ); for (int i = 2; i <= rsMeta.getColumnCount(); i++ ) { String[] rec = new String[30]; rec[0] = rsMeta.getColumnName(i); totalrows.addElement( rec ); }//End for loop }//End try catch ( SQLException sqlex ) { jTextArea.append( sqlex.toString() ); }//catch catch ( Exception excp ) { // process remaining Exceptions here jTextArea.append( excp.toString() ); }//End try-catch }//End tableQuery method |
這個方法生成了一個JTable用于數據輸入,其結構與CustData JTable相同。for循環從2開始。因為第一列是自動編號字段AddrID,它是在插入操作期間由DBMS軟件自動生成的。另外,如我們前面看到,我們創建的是存儲了字符串數組對象的相同Vector對象,除了數據列沒有填充數值外。如您可以看到的,每次只將有一個字符串數組元素添加到Vector中。那個元素就是列名。for循環是由getColumnCount方法調用控制的。
一旦CustAdd窗口處于活動狀態,通過輸入數據后單擊return或使用鼠標將焦點轉到一個新的字段上,就會在字段中注冊這些值。然后單擊Create Record按鈕。 Primary_Phone是添加操作中需要輸入數值的僅有的一個字段。下面展示了CustAdd GUI。
單擊Create Record按鈕會執行下面的代碼。
catm.tableInsert(); catm.fire();
讓我們來看一下tableInsert方法。
public void tableInsert() { try { /* my method to build prepared */ /* statement string */ strgBuffer = getInsertStatement( rsMeta.getColumnCount(), strgBuffer ); /* convert StringBuffer to String */ pstrg = strgBuffer.toString(); pstat = dbc.prepareStatement( pstrg ); /* Statement stmt = dbc.createStatement(); */ for ( int i=0; i < getRowCount(); i++ ) { String strgVal = (String)getValueAt( i, 1 ); if( i == 2 ) { if ( strgVal == null ) { jTextField1.setText ( "Primary_Phone required" ); jTextArea.append ( "Primary phone field required\n" ); return; } pstat.setString( i+1, stripOut( strgVal ) ); } else pstat.setString( i+1, strgVal ); }//End for loop pstat.executeUpdate(); cqtm.tableQuery(); cqtm.fire(); jTextField1.setText(""); jTextField1.setText(" Insert successful "); jTextArea.append (" Insert into Addresses successful\n"); } catch ( SQLException sqlex ) { jTextArea.append( sqlex.toString() ); jTextField1.setText ("SQL Error-see DBMaster window" ); } catch ( Exception excp ) { // process remaining Exceptions here jTextArea.append( excp.toString() ); jTextField1.setText( "Error-see DBMaster window" ); }//End try-catch }//End method tableInsert |
getInsertStatement方法調用返回如下形式的一條SQL字符串:
INSERT INTO Addresses ( First_Name, Last_Name, ?) VALUES (?, ?, ?) strgBuffer = getInsertStatement( rsMeta.getColumnCount(), strgBuffer ); /* convert StringBuffer to String */ pstrg = strgBuffer.toString(); pstat = dbc.prepareStatement( pstrg ); |
然后將StringBuffer轉換成一個字符串,并將其作為一個參數進行傳遞,以便創建PreparedStatement對象pstat。然后下面的for循環使用getValueAt方法從表中字段提取輸入的數據。
for ( int i=0; i < getRowCount(); i++ ) { String strgVal = (String)getValueAt( i, 1 ); if( i == 2 ) { if ( strgVal == null ) { jTextField1.setText ( "Primary_Phone required" ); jTextArea.append ( "Primary phone field required\n" ); return; } pstat.setString(i+1, stripOut( strgVal ) ); } else pstat.setString( i+1, strgVal ); }//End for loop |
然后使用pstat.setString方法調用來設置這些問號參數。注意,if( i == 2 )塊用于要求輸入Primary_Phone字段。如我們前面看到,stripOut方法可以移除非數字值。
該操作最終由下面代碼完成。
pstat.executeUpdate(); cqtm.tableQuery(); cqtm.fire(); jTextField1.setText(""); jTextField1.setText(" Insert successful "); jTextArea.append (" Insert into Addresses successful\n"); |
PreparedStatement方法pstat.executeUpdate 在數據庫中輸入新的數據。然后刷新查詢表以反映新數據,并將消息寫到本地的JTextField和DBMaster上的JTextArea。
下訂單
通過單擊CustData窗口底部的Place Order單選按扭,可以實例化一個CustOrder窗口。單擊這個按扭執行了CustData中的如下代碼。
if (custOrder != null) { custOrder.setVisible(false); custOrder = new CustOrder( mod ); } else { custOrder = new CustOrder( mod ); }//End if-else |
我們使用現在大家都應該熟悉的技術來呈現CustOrder窗口。下面展示了這個GUI。
這些語句是由CustOrder構造函數執行的。
cotm.tablePopulate(); cotm.fire();
在本例中,tablePopulate填寫了對應于Orders表中那些列名的一個兩列JTable。讓我們來看一下代碼。
public void tablePopulate() { try { rsMeta = cqtm.getOrdersMetaData(); totalrows = new Vector(); colstring = rsMeta.getColumnName(1); for (int i = 4;i <= rsMeta.getColumnCount();i++){ String[] rec = new String[ rsMeta.getColumnCount( ) - 3 ]; rec[0] = rsMeta.getColumnName( i ); totalrows.addElement( rec ); }//End for loop }//End try catch ( SQLException sqlex ) { jTextArea.append( sqlex.toString() ); }//catch catch ( Exception excp ) { // process remaining Exceptions here jTextArea.append( excp.toString() ); }//End try-catch }//End tablePopulate method |
這個tablePopulate方法與我們前面分析的一個方法類似,但請注意,我們這次是要呈現一些用于輸出的 JTable字段,這些字段對應于Orders表,從第四列開始,一直進行到最后一列,并由rsMeta.getColumnCount方法調用控制。Orders表的第1-3列包含CustID、AddrID和Order_Date,它們不適合于用戶訂單輸入。
當在CustOrder上單擊Place Order按鈕時,會執行下面的代碼。
cotm.tableInsert(); cotm.fire();
下面是tableInsert的代碼清單。
public void tableInsert() { /* get AutoNumber field from Addresses table */ index = cdtm.getIndex(); try { /* fire method to build prepared */ /* statement string */ preparedSQL = getInsertStatement( rsMeta.getColumnCount(), preparedSQL ); /* convert StringBuffer to String */ String stringSQL = preparedSQL.toString(); pstat = dbc.prepareStatement( stringSQL ); /* get current date in SimpleDataFormat */ java.util.Date date = new java.util.Date(); SimpleDateFormat fmt = new SimpleDateFormat("yyyy.MM.dd-HH:mm z"); String dateString = fmt.format( date ); /* AutoNumber index field from Addresses table */ pstat.setString( 1 , index ); pstat.setString( 2, dateString ); /* fill in product values */ for ( int i=0; i < getRowCount(); i++ ) { pstat.setString(i+3,(String)getValueAt(i,1)); }//End for loop pstat.executeUpdate(); jTextArea.append( "Order Successfully Placed\n" ); jTextField.setText( "Order Placed" ); }//End try catch ( SQLException sqlex ) { jTextArea.append( sqlex.toString() ); jTextField.setText( "SQL Error-See DBMaster" ); } catch ( Exception excp ) { // process remaining Exceptions here jTextArea.append( excp.toString() ); jTextField.setText( "Error-See DBMaster" ); }//End try-catch }//End method tableUpdate |
這些代碼也類似于我們前面看到的代碼。由getInsertStatement方法調用生成的SQL字符串具有如下形式:
INSERT INTO Orders ( AddrID, Order_Date, ?) VALUES ( ?, ?, ?.)
下面代碼用于處理日期字符串的生成。
java.util.Date date = new java.util.Date(); SimpleDateFormat fmt = new SimpleDateFormat("yyyy.MM.dd-HH:mm z"); String dateString = fmt.format( date );
使用java.util.Date對象作為參數來調用SimpleDateFormat的format方法,會產生了類似于如下的一個日期字符串。
2001.06.21-10:35 PDT
SimpleDateFormat參數yyyy.MM.dd-HH:mm z指出日期字符串的形式。
yyyy字符串對應于由"."分隔的一個四位年字段,MM指的是一個兩位的月字段,dd指出由"-"分隔的表示月份中某一天的兩位字段,然后HH指出一個兩位的0-23的小時字段,mm顯示分鐘字段,最后z指出時區。
我以這種方式創建日期字段是為了在JTable數據表示期間方便排序。
訂單歷史
通過單擊CustData窗口底部的Order History單選按鈕,可以實例化一個CustOrderHist窗口。單擊這個按扭會執行CustData中的如下代碼。
if (custOrderHist != null) { custOrderHist.setVisible(false); custOrderHist = new CustOrderHist( mod ); } else { custOrderHist = new CustOrderHist( mod ); }//End if-else |
對于本應用程序,CustOrderHist構造函數是標準的,但包含了一些特有的方面。
chtm.tableQuery(); chtm.fire(); TableColumn tcol = jTable2.getColumnModel().getColumn(0); tcol.setPreferredWidth(125); |
首先,執行CustHistTableModel tableQuery方法和它的fire方法。
然后使用setPreferredWidth方法重配置CustHistTableModel JTable,來擴展第一列的寬度以適應日期字符串。寬度設為125個像素。
如下是CustOrderHist窗口。
上面的JTable又是CustDataTableModel對象cdtm的一個呈現,它最初在CustData類中使用。下面的JTable對于這個類來說是新的。它按日期排序,顯示該客戶的訂單,并將從Orders表中提取的產品作為列標題列出。再說明一下,如果產品種類增加了,這張表將自動接受這些改變,因為ResultSetMetaData是用來生成表和產品名稱的。
對于這個GUI沒有什么可能的動作,在窗口創建后就會呈現這些表。如下是CustHistTableModel tableQuery方法。
public void tableQuery() { rsMeta = cqtm.getOrdersMetaData(); index = cdtm.getIndex(); try { String strg = "SELECT * FROM Orders " + " WHERE " + rsMeta.getColumnName(2) + " = "+ index + " ORDER BY " + rsMeta.getColumnName(3); rs = statement.executeQuery( strg ); totalrows = new Vector(); while ( rs.next() ) { String[] rec = new String[rsMeta.getColumnCount()-2]; int j = 0; for (int i=0;i<= rsMeta.getColumnCount();i++) { if ( i>2 ) { rec[j]=rs.getString ( rsMeta.getColumnName(i)); j++; }//End if block }//End for loop totalrows.addElement( rec ); }//End while loop jTextArea.append( "CustHist Query successful\n" ); }//End try catch ( SQLException sqlex ) { jTextArea.append( sqlex.toString() ); }//catch catch ( Exception excp ) { // process remaining Exceptions here jTextArea.append( excp.toString() ); }//End try-catch }//End tableQuery method |
第一步是從Addresses表中提取ResultSetMetaData對象和該客戶的自動編號字段值。
rsMeta = cqtm.getOrdersMetaData(); index = cdtm.getIndex();
然后創建SQL語句。
String strg = "SELECT * FROM Orders " + " WHERE " + rsMeta.getColumnName(2) + " = "+ index + " ORDER BY " + rsMeta.getColumnName(3);
rsMeta.getColumnName(2)訪問的第二列的字段名是AddrID,該字段對應于Addresses表中的同名自動編號字段。這條語句將從Addresses表中選出包含該客戶的自動編號字段的所有列的和所有記錄。如果客戶沒有訂購,那結果就可能為null。
下面的代碼塊完成了這個方法。
rs = statement.executeQuery( strg ); totalrows = new Vector(); while ( rs.next() ) { String[] rec = new String[rsMeta.getColumnCount()-2]; for (int i=0;i<= rsMeta.getColumnCount();i++) { if ( i >2 ) { rec[i-3]=rs.getString ( rsMeta.getColumnName(i)); }//End if block }//End for loop totalrows.addElement( rec ); }//End while loop jTextArea.append( "CustHist Query successful\n" ); |
SQL字符串是由executeQuery方法執行的,然后使用while(rs.next())語法來反復訪問結果集,這我們已經在前面看到過。當結果集為空時,next方法返回false。
其他的代碼是標準的過程,用于為JTable呈現創建Vector數據結構。惟一的竅門是操縱索引變量i,使其跳過Orders的前兩列,它們分別包含了自動編號字段和AddrID字段,后一字段將Orders中的記錄與Addresses中的一個客戶記錄關聯起來了。
結束語
本應用程序為杜克面包店的擁有者Kate Cookie提供了有關Java和JDBC技術的概覽,這對于她在添加和定制訂購系統以滿足她的要求時是有用的。它也提供了一個將GUI生成與處理代碼相分離的體系結構,這使得程序的維護和修改變得更加容易了。既然她已經看到運行中的本應用程序并學習了這個軟件,她就渴望做些編程工作。她告訴我她計劃為Orders表實現刪除/更改功能。此外,還可以感覺到她有一些其他想法。開始編碼吧。
代碼清單
應用程序執行腳注
為了在沒有使用Forte CE下運行本應用程序,您必須:
- 創建下面的目錄路徑(這里使用的是MS-DOS語法)。
C:\Development\meloan - 將 *.java 文件復制到meloan目錄。
- 轉到meloan目錄,在MS-DOS提示行中輸入:
javac *.java
以編譯所有的Java源文件。 - 將目錄改變到根(root)目錄,然后輸入:
C:\>java Development.meloan.Controller
記住,您必須安裝Microsoft Access,而且您必須將它設置來使用包含的BakeryBook.mdb數據庫文件。為獲取這個安裝過程的信息,請參閱杜克的面包店 - 第1部分的Microsoft Access部分。
參考文章
- JDBC API Tutorial and Reference, Second Edition: Universal Data Access for the Java 2 Platform(White、Fisher、Cattell、Hamilton和Hapner,Addison-Wesley)
- The Java Tutorial, Second Edition: Object-Oriented Programming for the Internet (Book/CD) (Campione和Walrath,Addison-Wesley)
- Advanced Java 2 Platform, How to Program, First Edition(Deitel和Deitel,Prentice Hall)
- Java Design Patterns: A Tutorial with CD-ROM (Cooper,Addison-Wesley)
參考URL
- JDBC Data Access API
- Forte Tools
- 杜克的面包店 - 一個JDBC 訂購系統原型,第1部分
- Developing the Java 2D Art Applet Using Forte for Java Community Edition
- Threads and Swing
關于作者
Michael Meloan,他經常為Java Developer Connection撰稿,他的職業生涯從編寫IBM大型機和DEC PDP-11匯編語言開始。他還在繼續使用PL/I, APL 和 C語言編寫代碼。另外,他的小說曾發表在WIRED, BUZZ, Chic, L.A. Weekly和National Public Radio上。