杜克的面包店 -- 一个JDBC订购系统原型,第1部分
作者:Michael Meloan
1999年12月
本文是Monica Pawlan的"JDK 1.2 Roadmap: Putting It All Together"一文的展开,它讲述了一个使用SQL命令和JDBC API的快速原型项目背后的故事。这篇文章覆盖了数据库表的定义和Swing组件的使用,而且在整个过程中给出了详尽的代码和大量的屏幕抓图。
在"JDK 1.2 Roadmap: Putting It All Together"这篇文章里,我们认识了 "杜克的面包店"的拥有人和操作者Kate Cookie。Kate曾经请程序员Madhavi Rao来为"杜克的面包店"设计了软件架构。在完成这项工作后,Madhavi去参与一个网上零售商的大规模Java开发项目,因为Kate还需要一个原型系统,Madhavi就把这个工作推荐给了我。
经过与Kate的几次协商,她描述了她的经营情况和目标,我们决定采用这样一种方法:我先拼装出一个快速原型系统,这样她就可以开始使用这个程序,以后再根据她的需求进展提出一套更加具体的要求。
Kate说她最初想要关注的是在她生意兴隆的店铺里的一些特制产品。这些产品将通过特快专递的方式邮寄给客户。具体地说,她选择了全麦烤面包、小胡萝卜饼、奶奶面包和百吉饼。她想要这个程序能够让她输入客户的数据,而且当客户的数据输入以后,她要能够通过输入客户的电话号码再点一下Find按钮就列出这个客户的信息。她还告诉我说她正在使用一台装有Microsoft Access软件的Windows PC。我提醒她说,使用什么平台并没有太大关系,因为Java技术的继承性,而且由于使用JDBC API,我们将可以使用标准的SQL命令来处理数据。如果以后我们想要使用另外一个数据库产品来代替Micosoft Access的话,我们只需要对数据库设置语句做很小的一点改动就可以很容易实现。Kate听了我的话很兴奋,于是我就开始着手工作了。
定义数据库环境
第一步首先要定义数据库环境。我决定使用一个叫做BakeryBook的数据库里的两张表,这两张表分别是Addresses和Orders。这两张表逻辑上通过一个ID字段相链接,这个ID字段在Addresses表中是主键,而在Orders表里就是名为LinkAddrTbl的第二个字段。当生成订单的时候,来自Addresses表的ID字段就被包含到Orders表中,这样订单数据就可以与客户数据相联系了。这两张表在Microsoft Access面板里描述如下。
Addresses表的字段描述,其中ID是主键
这张表里面除了ID字段是自动编号的长整数外,其他所有字段都是文本型的。自动编号字段可以为每一个写入的记录自动生成一个唯一的数字。正因为这个原因,如果不再有其他方面考虑的话,主键使用自动编号字段是自然的选择。图中的小钥匙图标表示ID字段是这个Addresses表的主键。
当Kate在电话订购中输入客户的数据时,Addresses表将是她操作的对象,也将是她预期使用这个软件的主要途径。她计划通过晚上的货物空运来配送她的新鲜面包。这个原型软件使用一个电话号码作为搜索关键词来获得已经加入到数据库里的客户信息。一旦客户的名字、地址和其他信息显示到屏幕上,Kate将能够通过点击一个单选按钮来产生一个弹出窗口,存储一个订单,然后为四种产品中的每一种(如全麦烤面包或百吉饼)输入订单编号,这样就生成了一张订单。
Orders 表的字段描述如下:
订购信息被写入到这张Orders 表中,其中字段CustID是主键。LinkAddrTbl是一个长整数,用于指回到Addresses 表中的主记录。字段wheat、 cake、 naan和 bagel也是长整数,用来存储Addresses表里某个客户的订购信息。
JDBC-ODBC
ODBC是Microsoft 定义的一个API。在Sun开发Java JDBC API之前,ODBC是应用最广泛的用于访问关系数据库的编程接口。但ODBC使用的是C语言接口,这就使其在安全性、实现、健壮性和可移值性方面都表现出很多缺点。由于广泛使用了指针,要想精确地把ODBC的基于C的API转换成Java API可能会很困难。但是ODBC可以在Java平台下使用,在JDBC API的帮助下,以JDBC-ODBC桥的形式就可以很好地实现这一点。
桥
JDBC API是使用SQL的首选接口。它非常易于使用,因为程序员不必担心内存管理或字节调整这样的问题。JDBC建立于ODBC基础之上,而不是从头开始的。JDBC-ODBC桥本身就是定义在 sun.jdbc.odbc.JdbcOdbcDriver中的一个JDBC驱动。这个桥定义了作为JDBC子协议的odbc。在将来,随着大量纯Java JDBC驱动程序的开发,将使得这种JDBC-ODBC桥接方式变得多余了,但是在本文中我们需要用它来连接Microsoft Access。
Microsoft 推出了超越ODBC的一些新API,例如OLE(Object Linking and Embedding)DB,ADO(Active X Data Objects)和RDS(Remote Data Service)。OLE DB和ADO也是面向对象的可以执行SQL命令的数据库接口。然而OLE DB 是一种为工具而不是为开发人员设计的低层接口。ADO要更新些,更类似JDBC API一些,但是它不是纯的Java。RDS提供了类似于JDBC API的RowSet工具的功能,但是RDS并不是用Java编程语言写的,而且不可移植。
两层模型和三层模型
在两层模型里,Java applet或应用程序将直接与数据源对话。这就要求有一个可以与数据源进行对话的JDBC驱动程序,比如Access数据库,。用户的SQL命令被送往数据库或其他数据源,然后这些语句的执行结果又被传回给用户。
数据源可以位于另一台计算机上,用户通过网络连接到上面。这就叫做客户机/服务器配置,其中用户的计算机为客户机,提供数据库的计算机为服务器。网络可以是 intranet,也可以是 Internet。
在三层模型中,命令先是被发送到服务的"中间层",然后由它将命令发送给数据源。数据库对 SQL 语句进行处理并将结果送回到中间层,中间层再将结果送回给用户。这种三层模型提供了对于各种更新的更大控制,而且它还简化了应用程序的部署。在许多情况下,这种三层模型还可以提供性能上的优势。然而对于我们"杜克的面包店",两层模型就可以工作得很好了。
在windows 98 下配置Micosoft Access
Kate Cookie正在使用一台运行Windows 98的机器,而这台机器上还安装了Micosoft Access。所以我们需要按照以下几步通过JDBC-ODBC桥来进行通信,并且连接到名为BakeryBook的数据库。
在Windows的控制面板里,双击ODBC数据源。就会出现下面的窗口。
"杜克的面包店"数据库名为BakeryBook.mdb。选择这个条目,然后点击"Add"按钮。这时将会出现另一个标题为"Create New Data Source"的窗口。选择"Microsoft Access Driver"条目,然后点击"Finish"按钮。
这时将会出现下面这样的窗口。"杜克的面包店"数据库的名字是BakeryBook。为了找到这个数据库所在的目录路径,点击"Select"按钮,它会让您查找一个目录。当输入了数据库的名字、路径和描述文字以后,点击"Advanced"按钮。
接下来就会出现下面这个窗口。
出于演示的目的,我们输入默认的登录名"anonymous",密码为"guest"。点击"ok"关闭这个对话框,并在剩下的所有ODBC窗口中点击"OK"按钮。这就结束了配置过程。
现在我们要准备开始看一些代码了!
代码预排
我使用JFC(Java Foundation Classes)开发了一个原型GUI。它们由以下几方面组成:Abstract Window Toolkit (AWT), Accessibility API, 2D API, 对拖放功能的增强支持, 和Swing组件。
Swing组件经常被认为是轻量级的组件。因为它们是完全用Java语言写的,所以总的来说,它们并不因为主平台强加于GUI的复杂性而显得笨重。 因为下面这两个原因,我们通常不需要重量级的组件。
- 不同平台上的同等组件不必以同样的方式来实现其功能。
- 每个组件的外观(look-and-feel)是与主机操作系统密切相关的。
下面回顾一下将Swing组件和老的AWT组件区别开来的一些重要特征。
- Swing提供了大量的新组件,例如表、树、滑块、进度条、内建框架和文本组件。
- Swing组件能够在它们上面放置工具条提示。工具条提示就是当鼠标移动到组件所在区域时所弹出来的说明性文字。工具条提示可以为我们提供关于该组件的一些额外信息。
- 键盘事件可以绑定到组件,并定义它们将如何对各种各样的键击做出响应。
- 还有一些用来呈现您自已的轻量级Swing组件的调试支持。
Swing的出现伴随着一种默认的叫做"Metal"的look-and-feel(L&Fs),它包含了现在主流L&Fs的一些最好的图形元素。Java编程语言使得它很容易实现一些其他的L&Fs,比如类似于Windows 或Motif外观,但是对于这个应用程序来说Metal将是最合适的。
下面的代码片段来自于叫做DukeBakery的主类。这里您可以看到基本GUI组件的创建过程。
public class DukeBakery extends JFrame {
private DataPanel screenvar;
private JTextArea msgout;
private Connection dbconn;
public DukeBakery() {
super( "DUKE'S BAKERY" );
// Set up GUI environment
Container p = getContentPane();
screenvar = new DataPanel();
msgout = new JTextArea( 8, 40 );
p.setLayout( new FlowLayout() );
p.add( new JScrollPane(
screenvar ) );
p.add( new JScrollPane(msgout) );
|
语句super( "DUKE'S BAKERY" );的执行调用了super类Jframe的构造函数,并将字符串"DUKE'S BAKERY"放置到窗口的标题栏上。然后我们就得到了一个容器对象p,它创建一个用来连接组件以供显示的内容窗格(content pane)。接下来一个DataPanel对象被实例化。这是我自已定义的一个类,我们将在下一节中详细分析它的代码。同时,一个有8字符行和40字符列的JtextArea被实例化。语句p.setLayout( new FlowLayout() );定义了内容窗格(content pane)的布局方案。可以有很多种布局方案,例如BoxLayout, BorderLayout, GridLayout等等。它们每一种都有特别的强度,这依赖于组件和设计因素的混合。FlowLayout将组件按照从左上角到右下角的方式排列。在这个程序段的最后两条语句里,
p.add( new JScrollPane(
screenvar ) );
p.add( new JScrollPane(
msgout ) );
|
我们使用容器的add方法来将screenvar和msgout 对象放在内容窗格(content pane)上。这两个对象都是在一个JscrollPane对象里,这样就可以在当输出超过了定义的边界时自动打开垂直滚动条和水平滚动条。在msgout的情况下,这是非常重要的,因为它是作为一个消息窗口来使用的,而且它将在面包店接受订单的过程中收到状态信息。Kate可以检查这个字段以证实交易正在如期望中那样展开。
现在让我们来看看看datapanel类。
class DataPanel extends JPanel {
JTextField id, first, last,
address, home, city, state,
zip,
country, email, fax;
JLabel lfirst, llast, laddress,
lhome, lcity, lstate, lzip,
lcountry, lemail, lfax;
public DataPanel() {
// Label panel
JPanel labelPanel =
new JPanel();
labelPanel.setLayout(
new GridLayout( 10, 1 ) );
lfirst = new JLabel(
"First Name:", 0 );
labelPanel.add( lfirst);
llast = new JLabel(
"Last Name:", 0 );
labelPanel.add( llast);
lhome = new JLabel(
"Phone:", 0 );
labelPanel.add( lhome);
laddress = new JLabel(
"Address:", 0 );
labelPanel.add(
laddress);
lcity = new JLabel(
"City:", 0 );
labelPanel.add( lcity);
lstate = new JLabel(
"State:", 0 );
labelPanel.add( lstate);
lzip = new JLabel(
"Zip Code:", 0 );
labelPanel.add(
lzip);
lcountry = new JLabel(
"Country:", 0 );
labelPanel.add( lcountry);
lemail = new JLabel(
"Email:", 0 );
labelPanel.add( lemail);
lfax = new JLabel(
"Fax Number:", 0 );
labelPanel.add( lfax);
// TextField panel
JPanel screenvarPanel =
new JPanel();
screenvarPanel.setLayout(
new GridLayout( 10, 1 ) );
id = new JTextField( 20) ;
first = new JTextField( 20 );
screenvarPanel.add( first );
last = new JTextField( 20 );
screenvarPanel.add( last );
home = new JTextField(
"Enter
number-click Find", 20);
screenvarPanel.add( home );
address =
new JTextField( 20 );
screenvarPanel.add( address );
city = new JTextField( 20 );
screenvarPanel.add( city );
state = new JTextField( 20 );
screenvarPanel.add( state );
zip = new JTextField( 20 );
screenvarPanel.add( zip );
country = new JTextField( 20 );
screenvarPanel.add( country );
email = new JTextField( 20 );
screenvarPanel.add( email );
fax = new JTextField( 20 );
screenvarPanel.add( fax );
// Accessibility Section -
//relate labels and text
// fields for use by assistive
//technologies
lfirst.setLabelFor( first );
llast.setLabelFor( last );
lhome.setLabelFor( home );
laddress.setLabelFor( address );
lcity.setLabelFor( city );
lstate.setLabelFor( state );
lzip.setLabelFor( zip );
lcountry.setLabelFor( country );
lemail.setLabelFor( email );
lfax.setLabelFor( fax );
setLayout( new GridLayout( 1, 2 ) );
add( labelPanel );
add( screenvarPanel );
}
}
|
这个类虽然有大量的语句,但是实际上它是很简单的。Jlabels为每个输入数据的label产生实例,如"First Name:","Last Name:","phone:",等等。第二个参数"0"使文本字段居中。然后这些对象被加到JPanel object labelPanel。而语句abelPanel.setLayout(new GridLayout( 10, 1 ) );将居中的label以10行1列的阵列显示在面板上。
在这个类的第二个部分,定义了另一个叫作screenvarPanel 的Jpanel对象。在下面这个语句之后
screenvarPanel.setLayout(
new GridLayout( 10, 1 ) );,
10 JTextField
|
这些对象根据前面的10 个相应的Jlabel对象进行定义。它们被以10行1列的形式加到screenvarPanel对象,用作数据的输入和显示。
在第三个部分里,setLabelFor方法被调用来将Jlabel对象与TextField对象相联系。这个方法允许一些辅助的技术,如为盲人设计的语音输出读屏功能,用来讲解GUI组件之间的联系。
在这个类的结尾,语句setLayout( new GridLayout( 1, 2 ) );创建了一个新的1行2列的表格。这是与两个包含10行标签10行文本域的面板相符合的。这些面板被看作是独立的条目,并且被加到DataPanel对象,用下面的语句来对Jpanel进行扩展。
add( labelPanel );
add( screenvarPanel );
|
当DataPanel对象被实例化后,它被装入一个JscrollPane对象并将其加到容器c中。
// Set up database connection
try {
String url = "jdbc:odbc:BakeryBook";
Class.forName( "
sun.jdbc.odbc.JdbcOdbcDriver" );
dbconn =
DriverManager.getConnection( url );
msgout.append(
"Connection successful\n" );
}
catch (
ClassNotFoundException cnfex ) {
// process ClassNotFoundExceptions
//here
cnfex.printStackTrace();
msgout.append( "
Connection unsuccessful\n" +
cnfex.toString() );
}
catch ( SQLException sqlex ) {
// process SQLExceptions here
sqlex.printStackTrace();
msgout.append( "
Connection unsuccessful\n" +
sqlex.toString() );
}
catch ( Exception excp ) {
// process remaining Exceptions
//here
excp.printStackTrace();
msgout.append( excp.toString() );
}
|
字符串url被定义为数据库的名字,BakeryBook。然后用下面这行代码加载JDBC-ODBC桥驱动程序。
Class.forName(
"sun.jdbc.odbc.JdbcOdbcDriver" );
|
接下来我们用下面的语句将JDBC-ODBC驱动程序连接到BaeryBook数据库。
dbconn =
DriverManager.getConnection( url );
|
对这个方法的调用产生了Cnnection对象dbconn,它将在整个过程中被用来访问数据库。如果我们已经定义了一个用户登录名和密码,而不是用前面所说的在Microsoft Access ODBC 数据源注册过程中用的"anonymous"和"guest",我们必需将用户名和密码作为第二和第三个参数输入给getConnection方法和DriverManager。对msgout.append( "Connection successful\n" );的调用将导致写一条成功的消息到JtextArea对象msgout。
剩下的代码负责捕捉各种各样的异常,例如ClassNotFoundException,它表示语句Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver" );创建JDBC-ODBC驱动失败,表示系统配置有问题。
其余的catch语句负责处理SQL异常和一般异常。还可以从这些catch结构中提取更多的信息,详见White,Fisher等写的文章"JDBC API Tutorial and Reference 2nd Edition"。
通过完成这个GUI,类DukeBakery被封装起来。下面的语句就用来做这件事情。
// Complete GUI
ButtonPanel controls =
new ButtonPanel(
dbconn, screenvar, msgout);
p.add( controls );
RadioButtons rb =
new RadioButtons(
dbconn, screenvar, msgout );
p.add ( rb );
setSize( 500, 475 );
show();
|
这段代码实例化了ButtonPanel,这是一组对应于命令find,add,update和clear的按钮。然后ButtonPanel对象被加到内容窗格p。接下来RadioButtons对象被实例化并被加到内容窗格中。窗口的尺寸由setsize方法根据x和y的像素值创建。最后,由show()方法把它们显示到屏幕上。
让我们快速看一下生成ButtonPanel 和 RadioButtons类的代码。
class ButtonPanel extends JPanel {
public ButtonPanel(
Connection dbc, DataPanel scv,
JTextArea msg ) {
setLayout(
new GridLayout( 1, 4 ) );
JButton findName =
new JButton( "Find" );
findName.addActionListener
(new FindRecord(
dbc, scv, msg));
add( findName );
JButton addName =
new JButton( "Add" );
addName.addActionListener
(new AddRecord(
dbc, scv, msg ) );
add( addName );
JButton updateName =
new JButton(
"Update" );
updateName.addActionListener(
new UpdateRecord(
dbc, scv, msg ) );
add( updateName );
JButton clear =
new JButton( "Clear" );
clear.addActionListener(
new Clearscreenvar(
scv ) );
add( clear );
}
}
class RadioButtons extends JPanel {
public RadioButtons(
Connection dbc, DataPanel scv,
JTextArea msg ) {
JRadioButton place =
new JRadioButton(
"Place Order",false );
DukeOrder dkord =
new DukeOrder( dbc, scv, msg );
place.addActionListener(
dkord );
add( place );
OrderHist ohist =
new OrderHist(
dbc, scv, msg );
JRadioButton review =
new JRadioButton(
"Order History", false );
review.addActionListener( ohist );
add( review );
TotalHist tothist =
new TotalHist(
dbc, scv, msg );
JRadioButton stats =
new JRadioButton(
"Total Orders", false );
stats.addActionListener(
tothist );
add( stats );
ButtonGroup radioGrp =
new ButtonGroup();
radioGrp.add( place );
radioGrp.add( review );
radioGrp.add( stats );
}
}
|
Connection(数据库)、DataPanel(屏幕变量)和JtextArea(消息窗口)对象被发送到类构造器,因此它们能够在按钮被按下时为处理传递对象。
在ButtonPanel里,Jbutton对象分别为四个按钮(Find、Add、Update和Clear)进行实例化。当按钮被按下时,一个action listener方法被用来建立处理进程。在下一节,我们将看到用来处理按钮事件的代码。
RadioButtons类与ButtonaPanel类非常类似。它们之间真正的区别仅仅在于前者使用JradioButton而后者使用Jbutton。另外,我使用ButtonGroup类来创建具有互斥性的三种按钮,具体地说,如果一个按钮被按下,则其他按钮就变成非活动的了。
因为这些类都对Jpanel进行了扩展,对add(…)方法的调用就会跳转到Jpanel,然后被加到DukeBakery的内容窗格p中。
现在是时候来看看当我们运行这个程序时主窗口看起来是什么样的了。我已经做了一次Find操作,因此我们在JtextFields中已经有了一些数据。
我在Phone域输入(111)111-1111,以此来查找前面预定义好的试验数据。非数字字符将被自动从phone域里消除掉,以保证数据的完整性。
让我们来看看FindRecord类,是它实现了接口AcrionListener。
class FindRecord
implements ActionListener {
private DataPanel screenvar;
private JTextArea msgout;
private Connection dbconn;
public FindRecord(
Connection dbc, DataPanel scv,
JTextArea msg ) {
dbconn = dbc;
screenvar = scv;
msgout = msg;
}
public void actionPerformed(
ActionEvent e ) {
try {
// strip out non-numerics
//from home phone
String numstrg = new String();
for (int i = 0;
i < screenvar.home.gettext().length(
); i++){
if ( screenvar.home.gettext(
).charat(i)>='0' &&
screenvar.home.getText(
).charAt(i)<='9') {
numstrg +=
screenvar.home.gettext(
).substring(i,i+1);
}
}
if ( !numstrg.equals( "" ) ) {
statement statement =
dbconn.createstatement();
string query = "SELECT *
FROM addresses " +
"WHERE homephone = '" +
numstrg + "'";
msgout.append(
"\nSending query: " +
dbconn.nativesql(
query ) + "\n" );
resultset rs =
statement.executequery(
query );
display( rs );
statement.close();
}
else
screenvar.home.settext(
"Enter home phone
then press Find" );
}
catch ( sqlexception sqlex ) {
msgout.append( sqlex.tostring()
+sqlex.getmessage() );
}
}
// display results of query
public void display(
resultset rs ) {
try {
rs.next();
int recordnumber = rs.getint( 1 );
if ( recordnumber != 0 ) {
screenvar.id.settext(
string.valueof(recordnumber) );
screenvar.first.settext(
rs.getstring( 2 ) );
screenvar.last.settext(
rs.getstring( 3 ) );
screenvar.address.settext(
rs.getstring( 4 ) );
screenvar.city.settext(
rs.getstring( 5 ) );
screenvar.state.settext(
rs.getstring( 6 ) );
screenvar.zip.settext(
rs.getstring( 7 ) );
screenvar.country.settext(
rs.getstring( 8 ) );
screenvar.email.settext(
rs.getstring( 9 ) );
screenvar.home.settext(
rs.getstring( 10 ) );
screenvar.fax.settext(
rs.getstring( 11 ) );
}
else
msgout.append(
"\nNo record found\n" );
catch ( sqlexception sqlex ) {
msgout.append(
"\n*** Phone Number" +
"Not In Database ***\n" );
}
}
}
|
当Find按钮在GUI里被按下时,程序控制流程就跳转到actionPerformed方法。它首先进行的操作是从电话号码字符串中除掉非数字的数据。让我们看看它的代码。
String numstrg = new String();
for (int i = 0;
i < screenvar.home.gettext(
).length(); i++){
if (
screenvar.home.gettext(
).charat(i)>='0' &&
screenvar.home.getText(
).charAt(i)<='9') {
numstrg +=
screenvar.home.gettext(
).substring(i,i+1);
}
}
|
其中关键的方法是charAt,它被用来过滤掉所有除了0到9外的字符。然后通过反复使用substring方法连续增加单个字符来创建一个名为numstring的文本字符串。需要注意的是,substring方法需要第二个参数为i+1,以挑出位于第i个位置的字符。
现在让我们来看一些SQL代码。
Statement statement =
dbconn.createStatement();
String query = "
SELECT * FROM addresses " +
"WHERE homephone = '" +
numstrg + "'";
msgout.append(
"\nSending query: " +
dbconn.nativeSQL(
query ) + "\n" );
ResultSet rs =
statement.executeQuery(
query );
display( rs );
statement.close();
|
一个叫做query的字符串被创建为包含SQL查询语句,在我们的例子里它有下面的形式
SELECT * FROM addresses
WHERE homephone = '1111111111'
|
其中"*"号表示在Addresses表里,家庭电话与单引号内的字符串相符合的所有列都被选择了。记住我们的数据库叫做BakeryBook,其中包含两张表,addresses 和orders。
当查询建立起来后,它将被executeQuery方法执行,得到ResultSet对象rs。现在让我们看看rs在我的显示方法中是如何使用的。
public void display( ResultSet rs ) {
try {
rs.next();
int recordNumber = rs.getInt( 1 );
if ( recordNumber != 0 ) {
screenvar.id.setText
(String.valueOf(
recordNumber) );
screenvar.first.setText(
rs.getString( 2 ) );
screenvar.last.setText(
rs.getString( 3 ) );
screenvar.address.setText(
rs.getString( 4 ) );
screenvar.city.setText(
rs.getString( 5 ) );
screenvar.state.setText(
rs.getString( 6 ) );
screenvar.zip.setText(
rs.getString( 7 ) );
screenvar.country.setText(
rs.getString( 8 ) );
screenvar.email.setText(
rs.getString( 9 ) );
screenvar.home.setText(
rs.getString( 10 ) );
screenvar.fax.setText(
rs.getString( 11 ) );
}
else
msgout.append(
"\nNo record found\n" );
}
catch ( SQLException sqlex ) {
msgout.append
( "\n*** Phone Number" +
"Not In Database ***\n" );
}
}
|
ResultSet next()方法是处理表数据的非常重要的方法。当查询被执行后,光标指到结果数据集的第一行上面。在这个例子里,因为电话号码必需要保证是唯一的(正如在定义数据库时所指出的那样),我们将只有一个结果记录。经过测试确认记录ID不为零后,我们用screenvar.first.setText( rs.getString( 2 ) );这种形式的语句从结果行里提出数据。这个操作从数据库里提取第二列,即是first name域,然后将其存储到我们实例化为screenvar的DataPanel里首先定义的JtextFiels中。这样,数据库里的所有列都被提取出来并放置到它们相应的JTextFields中。
另一个主要的顶级类,AddRecord和UpdateRecord在结构上是与FindRecord非常相似的。它们之间主要的区别在于它们的SQL命令字符串。让我们看看AddRecord类的命令字符串。
String query = "INSERT INTO addresses (" +
"firstname,
lastname, address, city, " +
"state, postalcode, country, " +
"emailaddress, homephone, faxnumber" +
") VALUES ('" +
screenvar.first.getText() + "', '" +
screenvar.last.getText() + "', '" +
screenvar.address.getText() + "', '" +
screenvar.city.getText() + "', '" +
screenvar.state.getText() + "', '" +
screenvar.zip.getText() + "', '" +
screenvar.country.getText() + "', '" +
screenvar.email.getText() + "', '" +
numstrg + "', '" +
screenvar.fax.getText() + "')";
|
这段SQL命令列出了数据库中列的名称,例如firstname, lastname, 与在Microsoft AccessAddresses表里所定义的列名称相一致。这些名称不必与Java类DataPanel里定义的名称一样,正如您可以看到接下来的各种getText()调用。单引号里的值是从用户提供的数据条目域提取出来的文本字符串。它们被getText()方法解析为文本字符串。
现在让我们看看UpdateRecord类的SQL字符串。
String query =
"UPDATE addresses SET " +
"firstname='" +
screenvar.first.getText() +
"', lastname='" +
screenvar.last.getText() +
"', address='" +
screenvar.address.getText() +
"', city='" + screenvar.city.getText() +
"', state='" +
screenvar.state.getText() +
"', postalcode='" +
screenvar.zip.getText() +
"', country='" +
screenvar.country.getText() +
"', emailaddress='" +
screenvar.email.getText() +
"', homephone='" + numstrg +
"', faxnumber='" +
screenvar.fax.getText() +
' WHERE id=" + screenvar.id.getText();
|
这段语法使得Addresses表的名字与从GUI输入的值相符合,并且将业务号输入到Addresses表的ID字段,这在之前的Find操作过程中就被读入到screenvar.id数据项中了。
配置订单
这时候,我们可以点击主窗口底部的"Place Order"单选按钮,就会弹出下面这个新的窗口。
在这个窗口里只需要简单地在每一栏中填入想要的数字,然后点击"Order"按钮就可以了。这个界面的设计让您必需为每一栏填入有效的数字,如果是空的或是非数字字符都将不会被接受,而且还会在数据输入域出现错误提示信息。
下面这个类将在"Order"按钮被按下时被一个简单的叫作DukdeOrder的类(在本文结尾可以看到它的完整代码清单)所调用。其中的obox对象是一个Jpanel类,它为数据条目建立JtextFields域。
class PlaceOrder
implements ActionListener {
private DataPanel screenvar;
private JTextArea msgout;
private Connection dbconn;
private OrderBox obox;
public PlaceOrder(
Connection dbc, DataPanel scv,
JTextArea msg, OrderBox ob ) {
dbconn = dbc;
screenvar = scv;
msgout = msg;
obox = ob;
}
public void actionPerformed(
ActionEvent e ) {
boolean inputerror = false;
int iwheat=0; int icake=0;
int inaan=0; int ibagel=0;
try {
Statement statement =
dbconn.createStatement();
try {
iwheat=Integer.parseInt(
obox.inwheat.getText());
}
catch(
NumberFormatException nfe ) {
inputerror = true;
obox.inwheat.setText
("Error--enter integer value");
}
try {
icake=Integer.parseInt(
obox.incake.getText() );
}
catch(
NumberFormatException nfe ) {
inputerror = true;
obox.incake.setText
("
Error--enter integer value");
}
try {
inaan=Integer.parseInt(
obox.innaan.getText() );
}
catch(
NumberFormatException nfe ) {
inputerror = true;
obox.innaan.setText
("Error--enter integer value");
}
try {
ibagel=Integer.parseInt(
obox.inbagel.getText());
}
catch(
NumberFormatException nfe ) {
inputerror = true;
obox.inbagel.setText
("Error --
enter integer value");
}
java.util.Date date =
new java.util.Date();
SimpleDateFormat fmt =
new SimpleDateFormat (
"yyyy.MM.dd-HH:mm z");
String dtstr = fmt.format(date);
if( !screenvar.id.getText(
).equals("") ) {
if ( !inputerror &&
(iwheat != 0 ||
icake != 0 ||
inaan != 0 ||
ibagel != 0) ) {
String query =
"INSERT INTO orders " +
"(LinkAddrTbl,
OrderDate,wheat,cake,naan,bagel)" +
"VALUES ("+
screenvar.id.getText() + "," +
"'" + dtstr + "'," +
String.valueOf(iwheat) + "," +
String.valueOf(icake) + "," +
String.valueOf(inaan) + "," +
String.valueOf(ibagel) + ")";
msgout.append(
"\nSending query: " +
bconn.nativeSQL( query )
+ "\n" );
int result =
statement.executeUpdate(
query );
if ( result == 1 )
msgout.append(
"\nOrder Placed\n" );
else {
msgout.append(
"\nInsertion failed\n" );
screenvar.first.setText( "" );
screenvar.last.setText( "" );
}
}
else
msgout.append
( "\nEnter at least one numeric value " +
", then press Order\n" );
}
else
msgout.append(
"\n *** Find data before
issuing order ***\n");
statement.close();
}
catch ( SQLException sqlex ) {
msgout.append(
sqlex.toString() );
}
// clear out order input boxes
//after database write
if (!inputerror) {
obox.inwheat.setText("");
obox.incake.setText("");
obox.innaan.setText("");
obox.inbagel.setText("");
}
}
}
|
输入的数据被下面这样的代码过滤为有效的整数。
try {
iwheat=Integer.parseInt(
obox.inwheat.getText(
));
}
catch(
NumberFormatException nfe ) {
inputerror = true;
obox.inwheat.setText
(
"Error--enter integer value");
}
|
输入的订单被Integer方法parseInt所解析。如果它产生了一个错误,catch块就会截获这个错误并在数据输入域显示一个错误提示信息,以让用户试图再次输入数据。
在填写第二张订购表单之前,下面这段代码将向操作系统询问日期和时间。
java.util.Date date =
new java.util.Date();
SimpleDateFormat fmt =
new SimpleDateFormat (
"yyyy.MM.dd-HH:mm z");
String dtstr = fmt.format(date);
|
这种日期格式会产生一个形如"1999.11.20-23:22 PST."的日期时间字符串。用Java编程语言也可以产生甚至更详细的日期和时间格式,但是这种紧凑的形式正是我所想要的,作为一个数据库条目和表单输出而言,这就已经足够了。
下面的逻辑结构
if ( !inputerror &&
(iwheat != 0 ||
icake != 0 ||
inaan != 0 ||
ibagel != 0) ) {
|
表明,如果输入没有错误并且输入的数据不全为0,则会执行SQL插入操作。SQL操作的细节非常像我们前面所分析过的AddRecord操作。只是还会多有一个逻辑结构来防止发布一个没有首先在Addresses表的有效主记录上执行"Find"操作的订单。
订购历史记录
如果Kate有一个老客户打电话来了,她将会输入他的电话号码然后点击"Find"按钮来浏览这个客户的地址记录。然后她将点击"Place Order"按钮来输入他需要订购的货物数量。接下来,她可能想要看看这个客户的订购历史记录。为了实现这点,她只要点击"Order History"单选按钮,就会弹出一个看起来像下面这样的窗口。
这张表单是用Jtable类创建的。让我们来查看一下它的代码。
class OrderHist
extends JFrame implements
ActionListener {
private Connection dbconn;
private DataPanel screenvar;
private JTextArea msgout;
private boolean firsttime = true;
private Container c;
private JScrollPane jspane;
private QueryTableModel qtbl;
private JTable jtbl;
public OrderHist(
Connection dbc,
DataPanel scv,
JTextArea msg ) {
super(
"DUKE'S BAKERY --
ORDER HISTORY" );
dbconn = dbc;
screenvar = scv;
msgout = msg;
}
public void actionPerformed(
ctionEvent e) {
if (
!screenvar.id.getText().equals("") ) {
// Set up Table GUI
if ( firsttime ) {
c = getContentPane();
c.setLayout( new FlowLayout() );
qtbl = new
QueryTableModel(
dbconn, screenvar, msgout );
qtbl.query();
jtbl = new JTable( qtbl );
TableColumn tcol =
jtbl.getColumnModel(
).getColumn(0);
tcol.setPreferredWidth(125);
jspane = new JScrollPane( jtbl );
c.add( jspane );
setSize( 500, 500 );
firsttime = false;
}
else {
qtbl.query();
qtbl.fire();
TableColumn tcol =
jtbl.getColumnModel(
).getColumn(0);
tcol.setPreferredWidth(125);
}
show();
}
else
msgout.append(
"\n ***Find data before " +
"generating Order History***\n");
}
}
class QueryTableModel extends
AbstractTableModel {
Connection dbconn;
DataPanel screenvar;
JTextArea msgout;
Vector totalrows;
String[] colheads =
{"Date & Time", "Wheat Loaf",
"Carrot Cake", "Naan Bread",
"Bagel"};
public QueryTableModel(Connection dbc,
DataPanel scv, JTextArea msg ){
dbconn = dbc;
screenvar = scv;
msgout = msg;
totalrows = new Vector();
}
public String getColumnName(int i)
{ return colheads[i]; }
public int getColumnCount(
) { return 5; }
public int getRowCount() {
return totalrows.size(
); }
public Object getValueAt(
int row, int col) {
return ((
String[
])totalrows.elementAt(
row))[col];
}
public boolean isCellEditable(
int row, int col) {
return false;
}
public void fire() {
fireTableChanged(null);
}
public void query() {
try {
Statement statement =
dbconn.createStatement();
String query =
"SELECT * FROM Orders " +
"WHERE LinkAddrTbl =" +
screenvar.id.getText() +
" ORDER BY OrderDate";
msgout.append(
"\nSending query: " +
dbconn.nativeSQL(
query ) + "\n" );
ResultSet rs =
statement.executeQuery(
query );
totalrows = new Vector();
while ( rs.next() ) {
String[
] record =
new String[5];
for( int i = 0; i <
5; i++ ) {
record[i] =
rs.getstring(
i + 3 );
}
totalrows.addelement(
record );
}
msgout.append(
"\nQuery successful\n" );
statement.close();
}
catch ( sqlexception sqlex ) {
msgout.append(
sqlex.tostring() );
}
}
}
|
这种类里有两个特别强大的工具。第一个是为数据输出使用的JTable,第二个是用AbstractTableModel来描述和更新包含于Jtable里的数据。
抽象表模型
这个类实现了在TableModel接口里的许多方法。用来描述您的表单里的数据的关键方法必需由程序员来实现。
它们是:
* public int getRowCount();
* public int getColumnCount();
* public Object getValueAt(
int row, int col);
|
还有一些方法被用来描述列的头,并且决定表格单元是否可以编辑。在这个程序里,我将从QueryTableModel内部来访问数据库,它扩展了AbstractTableModel。描述方法如下:
public String getColumnName(int i)
{ return colheads[i]; }
public int getColumnCount() {
return 5; }
public int getRowCount() {
return totalrows.size(); }
public Object getValueAt(
int row, int col) {
return ((String[
])totalrows.elementAt(row))[col];
}
public boolean isCellEditable(
int row, int col) {
return false;
}
public void fire() {
fireTableChanged(null);
}
|
其中GetColumnName方法从矩阵colheads里提取出各列的头字符串。
由getColumnCount方法提取出来的列数被明确地设置为5,因为我不想让表单里的每一列都显示在屏幕上。如果我这样做了,我可以使用JDBC的meta数据方法getColumnCount来提取一个整数,这个整数会被返回给程序员定义的getColumnCount方法。
Vector totalrows包含了由SQL数据库查询获得的列数据的String矩阵。每个单独的列都由一个String矩阵组成,其元素为[0]到[4],分别与5列数据相对应。然后这些String矩阵(每个都将代表JTable输出的一行)会被添加到Vector totalrows。由getRowCount方法返回的totalrows的大小反映了将被输出给Jtable的行数。
GetValueAt方法允许QueryTableModel为JTable创建单独的元素或表格单元。totalrows.elementAt(row)方法从Vector totalrows提取一个特定的行,这行稍后将被发送给String矩阵,并允许通过[col]指标从该行提取某个特定的列。
布尔方法isCellEditable对所有的查询都返回false值,以确保表单不会被用户交互修改,这是我们在显示一个查询表单时所想要的。
这些与JTable类协同工作的方法允许表单被重绘,而且也允许在fire方法执行后自动更新表单。Fire方法在数据库查询修改了表单后被执行。
SQL代码被包含在QueryTableModel's类的查询方法里。这个查询字符串有些新东西,让我们来看看它的语法。
String query = "
SELECT * FROM Orders " +
"WHERE LinkAddrTbl =" +
screenvar.id.getText() +
" ORDER BY OrderDate";
|
这是个简单的SELECT命令,用"*"号来选择所有的列,但是现在我们要根据OrderDate来对由年、月、日和24小时时间构成的结果记录进行排序。ORDER BY语法保证数据在显示面板上是按日期升序排列的。
让我们看看给Vector totalrows加载数据的While循环。
totalrows = new Vector();
while ( rs.next() ) {
String[] record = new String[5];
for( int i = 0; i < 5; i++ ) {
record[i] = rs.getstring(
i + 3 );
}
totalrows.addelement( record );
}
|
数据库的列数据被加载到来自rs.getString 的第3到第7个位置的String矩阵记录里。注意数据库的列自动编号系统是从1开始的,这与Java编程标准里从0开始的原则是大不相同的。当完整的一行数据被加载到记录里后,String矩阵就被使用addElement方法添加到Vector totalrows。
现在我们将会看到QueryTableModel对象是如何被用来创建JTable的:
if ( firsttime ) {
c = getContentPane();
c.setLayout( new FlowLayout() );
qtbl = new
QueryTableModel(
dbconn, screenvar, msgout );
qtbl.query();
jtbl = new JTable( qtbl );
TableColumn tcol =
jtbl.getColumnModel(
).getColumn(0);
tcol.setPreferredWidth(125);
jspane = new JScrollPane(
jtbl );
c.add( jspane );
setSize( 500, 500 );
firsttime = false;
}
|
通过给QueryTableModel发送数据库连接对象、屏幕变量对象和消息输出对象,QueryTableModel被实例化。然后QueryTableModel的query方法被执行,给Vector totalrows填充数据。在实例化一个JTable 对象的过程中,QueryTableModel对象被用作一个参数。然后一个TableColumn对象被用来与JTable对象协同工作来设置第一列的宽度为125像素。接下来,JTable对象被封装到一个JscrollPane并且添加到内容窗窗格(content pane)里。当fire方法执行的时候,JTable被用新的数据自动更新,如下面的代码所示。
else {
qtbl.query();
qtbl.fire();
TableColumn tcol =
jtbl.getColumnModel(
).getColumn(0);
tcol.setPreferredWidth(125);
}
show();
|
第一列被重建为125像素宽,而其他列会自动收缩以适应新的宽度。
我们注意到,这里还有一个布尔开关用来控制一个代码块的首次执行和随后的终止。这种情况发生在当窗口由单选按钮激发的时候。对于DukeBakery的主窗口,只要程序正在运行,其GUI就总是停留在屏幕上,而且当这个窗口被关闭的时候整个程序就被终止了。单选按钮控制的窗口一旦它的操作完成了就会被关闭,而应用程序还继续运行。当单选按钮再次被点中时,这个窗口又会出现。
当组件被加到内容窗格里时,即使窗口被关掉了它们还会持续运行。一般来说,当窗口再次被打开时,只需要执行show()方法就可以了。在这个类里,我们要做一些与query()方法和fire()方法相关的额外工作。
所有订单
如果Kate想要得到关于每项产品被Orders 表里所有客户订购的总数的报告,她可以点击第三个单选按钮,就会弹出一个包含以下信息的小窗口。
下面就是产生这些数据的类:
class TotalHist extends JFrame
implements ActionListener {
private Connection dbconn;
private DataPanel screenvar;
private JScrollPane orderpane;
private JTextArea msgout;
private JTextField outwheat,
outcake, outnaan, outbagel;
private boolean firsttime = true;
public TotalHist(
Connection dbc, DataPanel scv,
JTextArea msg ) {
super(
"DUKE'S BAKERY --
TOTAL ORDERS" );
dbconn = dbc;
screenvar = scv;
msgout = msg;
}
public void actionPerformed(
ActionEvent e) {
// Set up GUI environment
if ( firsttime ) {
Container c = getContentPane();
c.setLayout(new GridLayout(4,2) );
JLabel pwheat = new JLabel(
"Total number of
Wheat Loaves ordered:" );
c.add( pwheat );
outwheat = new JTextField( 10 );
c.add( outwheat );
pwheat.setLabelFor( outwheat );
JLabel pcake = new JLabel(
"Total number of Carrot
Cakes ordered:");
c.add( pcake);
outcake = new JTextField( 10 );
c.add( outcake );
pcake.setLabelFor( outcake );
JLabel pnaan = new JLabel(
"Total number of Naan
Bread ordered:" );
c.add( pnaan );
outnaan = new JTextField( 10 );
c.add( outnaan );
pnaan.setLabelFor( outnaan );
JLabel pbagel = new JLabel(
"Total number of
Bagels ordered:" );
c.add( pbagel );
outbagel = new JTextField( 10 );
c.add( outbagel );
pbagel.setLabelFor( outbagel );
setSize( 550, 130 );
firsttime = false;
}
show();
try {
Statement statement =
dbconn.createStatement();
String query =
"SELECT * FROM Orders";
msgout.append(
"\nSending query: " +
dbconn.nativeSQL(
query ) + "\n" );
ResultSet rs =
statement.executeQuery( query );
display( rs );
msgout.append(
"\nQuery successful\n" );
statement.close();
}
catch ( SQLException sqlex ) {
sqlex.printStackTrace();
}
}
// Display results of query
public void display( ResultSet rs ) {
int accwheat=0; int acccake=0;
int accnaan=0; int accbagel=0;
try {
while ( rs.next() ) {
// compute totals date,
//wheat,carrot,
// naan,bagel from database
accwheat += rs.getInt(4);
acccake += rs.getInt(5);
accnaan += rs.getInt(6);
accbagel += rs.getInt(7);
}
}
catch ( SQLException sqlex ) {
msgout.append(
sqlex.toString() );
}
outwheat.setText(
String.valueOf(accwheat));
outcake.setText(
String.valueOf(acccake));
outnaan.setText(
String.valueOf(accnaan));
outbagel.setText(
String.valueOf(accbagel));
}
}
|
这个GUI是用GridLayout manager构造的。语句c.setLayout(new GridLayout(4,2) );决定了这些组件将以4行2列的方式排列在Jframe上。左边的列由Jlabel项组成,而右边的列则处理相应的JtextField项,以列出总和数据。
其中重要的处理循环如下:
while ( rs.next() ) {
// compute totals date,wheat,carrot,
// naan,bagel from database
accwheat += rs.getInt(4);
acccake += rs.getInt(5);
accnaan += rs.getInt(6);
accbagel += rs.getInt(7);
}
|
这个循环遍历了包含有Orders 表里所有记录的ResultSet rs。数据的值被添加到整型累加器,然后那些值被用形如这样的语句插入到JtextFields里:
outbagel.setText(String.valueOf(accbagel));
结束语
Kate Cookie将有机会把这个应用程序拿到外面去展示一下,看看她是多么喜欢这个程序的用户界面和它的基本数据库设计。她有一些客户数据要输入,所以当有订购电话打来的时候,她可以创建一个新的记录,或者通过使用电话码作为唯一的关键字段调出客户的数据。在回电话的时候,她只要按一下单选按钮就可以立即看到客户订购的历史记录。
我现在正在为她的应用程序做一些新的功能。她将需要通过客户的姓氏来进行查询,这样查询到的结果可能就不是唯一的了,这就要求程序必需有扫描多个记录的能力。她还将需要能够删除记录。另外,她可能还需要为每个订购记录增加一个状态字段来显示这个订单是否已经被处理,货物是否已经被配送。
当下次再重复做这个程序时(这将在"杜克的面包店,第2部分"里介绍),我将使用像Join那样的更高级的SQL概念。而且程序的执行效率也会通过使用SwingWorker而得到优化,具体说就是使用一个后台进程在屏幕后执行一些耗时的操作。Kate和我可能还会给出一些这个产品的数字化图像。我们已经有大量的APIs可以用来探索最终的Duke's Bakery应用程序。
代码清单
DukeBakery.java
参考文章
这个程序架构的灵感来自于下面这本书中一个叫做AddressBook.java的例子: "Java-How to Program-3rd Edition," Deitel & Deitel, Prentice Hall 1999。
Eckstein, Loy & Wood, "Java Swing," O'Reilly 1998 。
Lemay & Cadenhead, "Java 2 in 21 Days," Sams 1999 。
White, Fisher, Cattell, Hamilton, Hapner, "JDBC API Tutorial and Reference, 2nd Edition," Addison-Wesley, 1999 。
杜克的面包店 -- 一个JDBC订购系统原型,第2部分
关于作者
Michael Meloan,他经常为Java Developer Connection撰稿,他的职业生涯从编写IBM大型机和DEC PDP-11汇编语言开始。他还在继续使用PL/I, APL 和 C语言编写代码。另外,他的小说曾发表在WIRED, BUZZ, Chic, L.A. Weekly和National Public Radio上。