随笔 - 7, 文章 - 4, 评论 - 2, 引用 - 0
数据加载中……

杜克的面包店 -- 一个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 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下运行本应用程序,您必须:

  1. 创建下面的目录路径(这里使用的是MS-DOS语法)。
    C:\Development\meloan
  2. 将 *.java 文件复制到meloan目录。
  3. 转到meloan目录,在MS-DOS提示行中输入:
    javac *.java
    以编译所有的Java源文件。
  4. 将目录改变到根(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上。

posted on 2005-01-22 12:26 jacky 阅读(553) 评论(0)  编辑  收藏


只有注册用户登录后才能发表评论。


网站导航: