本文详述了如何在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上。