本文詳述了如何在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 JTable click */
rs.absolute(tblrow+1);
/* get the autonumber index */
index = rs.getString(1);
colstring = rsMeta.getColumnName(1);
/* create customer data table */
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
jTextArea.append( "Query successful\n" );
}//End try
catch ( SQLException sqlex ) {
/* write to DBMaster msg area */
jTextArea.append( sqlex.toString() );
}//catch
catch ( Exception excp ) {
// process remaining Exceptions here
/* write to DBMaster msg area */
jTextArea.append( excp.toString() );
}//End try-catch
}//End tableQuery method
|
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表實現刪除/更改功能。此外,還可以感覺到她有一些其他想法。開始編碼吧。
代碼清單
enchilada.jar
應用程序執行腳注
為了在沒有使用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部分。
參考文章
參考URL
關于作者
Michael Meloan,他經常為Java Developer Connection撰稿,他的職業生涯從編寫IBM大型機和DEC PDP-11匯編語言開始。他還在繼續使用PL/I, APL 和 C語言編寫代碼。另外,他的小說曾發表在WIRED, BUZZ, Chic, L.A. Weekly和National Public Radio上。