6. 使用 JDBC API 进行数据库管理
6.1. 概述
市场上有许多数据库。为了在 MS Windows 环境下标准化数据库访问,微软开发了一个名为 ODBC(开放数据库连接)的接口。该层通过标准接口隐藏了各数据库的具体特性。MS Windows 环境下有许多可用的 ODBC 驱动程序,它们有助于数据库访问。例如,以下是安装在 Win95 机器上的 ODBC 驱动程序列表:

依赖这些驱动程序的应用程序无需修改即可使用上述任何数据库。

为了使 Java 应用程序也能利用 ODBC 接口,Sun 创建了 JDBC(Java 数据库连接)接口,该接口充当 Java 应用程序与 ODBC 接口之间的中介:

6.2. 数据库操作的关键步骤
6.2.1. 简介
在使用 JDBC 接口访问数据库的 Java 应用程序中,通常涉及以下步骤:
- 连接数据库
- 向数据库发送 SQL 查询
- 接收并处理这些查询的结果
- 关闭连接
步骤 2 和 3 会反复执行,只有在数据库操作结束时才会关闭连接。这是一个相对标准的模式,如果您曾交互式地使用过数据库,可能对此已经很熟悉。我们将通过一个示例详细说明每个步骤。我们将考虑一个名为 ARTICLES 的 Access 数据库,其结构如下:
name | 类型 |
type | 4位项目代码 |
名称 | 其名称(字符串) |
price | 其价格(实际) |
当前库存 | 当前库存(整数) |
min_stock | 最低库存(整数),低于该数值时必须补货 |
此 ACCESS 数据库在 ODBC 数据库管理器中被定义为“用户”数据源:


其属性可通过“配置”按钮按以下方式进行设置:

此配置主要涉及将 ARTICLES 数据库与对应于该数据库的 Access 文件 articles.mdb 建立关联。完成此操作后,应用程序即可通过 ODBC 接口访问 ARTICLES 数据库。
6.2.2. 连接步骤
要使用数据库,Java 应用程序必须首先建立连接。这可通过以下类方法实现:
其中
DriverManager:一个包含应用程序可用驱动程序列表的 Java 类
Connection:一个 Java 类,用于在应用程序与数据库之间建立连接,应用程序将通过该连接向数据库发送 SQL 查询并接收结果
URL:用于标识数据库的名称。该名称类似于互联网 URL。正因如此,它属于 URL 类的一部分。但此处与互联网毫无关联。URL 采用以下形式:
**jdbc:驱动程序名称:源名称;param=val1;param2=val2**
在我们的示例中,我们将仅使用 ODBC 驱动程序,该驱动程序名为 **odbc**。URL 的第三部分由源名称和任何参数组成。在我们的示例中,这些将是系统已知的 ODBC 数据源。因此,先前定义的 *Articles* 数据源的 URL 将为
**jdbc:odbc:Articles**
id:用户 ID(登录名)
mdp:用户密码
总而言之,程序通过以下方式连接数据库:
- 通过名称(URL)进行标识
- 使用用户的凭据(ID、密码)
如果这三个参数正确,且存在能够建立 Java 应用程序与指定数据库之间连接的驱动程序,则 Java 应用程序与数据库之间将建立连接。该连接在程序中由 DriverManager 类返回的 Connection 对象表示。由于该连接可能因各种原因失败,因此可能会抛出异常。因此,我们将编写:
Connection connexion=null;
URL base=...;
String id=...;
String mdp=...;
try{
connexion=DriverManager.getConnection(base,id,mdp);
} catch (Exception e){
// traiter l’exception
}
要连接数据库,必须具备相应的驱动程序。在我们的示例中,这将是一个能够处理目标数据库的 ODBC 驱动程序。虽然该驱动程序必须出现在机器上的 ODBC 驱动程序列表中,但您还必须拥有与之对接的 Java 类。为此,应用程序将按以下方式请求所需的类:
Class 类与 JDBC 接口毫无关联。它是一个通用的类管理类。其静态方法 forName 允许动态加载类,从而使其静态属性和方法可用。与 MS Windows ODBC 驱动程序进行交互的类名为“sun.jdbc.odbc.JdbcOdbcDriver”。因此,我们将编写如下代码(该方法可能会抛出异常):
try{
Class.forName(« sun.jdbc.odbc.JdbcOdbcDriver ») ;
} catch (Exception e){
// handle exception (non-existent class)
}
JDBC 接口所需的类位于 java.sql 包中。因此,在程序开头,我们写:
以下是一个允许你连接数据库的程序:
import java.sql.*;
import java.io.*;
// call: pg PILOTE URL UID MDP
// connects to the URL database using class JDBC PILOTE
// user UID is identified by password MDP
public class connexion1{
static String syntaxe="pg PILOTE URL UID MDP";
public static void main(String arg[]){
// check number of arguments
if(arg.length<2 || arg.length>4)
erreur(syntaxe,1);
// connection to base
Connection connect=null;
String uid="";
String mdp="";
if(arg.length>=3) uid=arg[2];
if(arg.length==4) mdp=arg[3];
try{
Class.forName(arg[0]);
connect=DriverManager.getConnection(arg[1],uid,mdp);
System.out.println("Connexion avec la base " + arg[1] + " établie");
} catch (Exception e){
erreur("Erreur " + e,2);
}
// closing the base
try{
connect.close();
System.out.println("Base " + arg[1] + " fermée");
} catch (Exception e){}
}// hand
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
}// class
以下是一个执行示例:
E:\data\java\jdbc\0>java connexion1 sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles
Connexion avec la base jdbc:odbc:articles établie
Base jdbc:odbc:articles fermée
6.2.3. 向数据库发送查询
JDBC 接口允许向连接到 Java 应用程序的数据库发送 SQL 查询,并处理这些查询的结果。SQL(结构化查询语言)是关系型数据库的一种标准化查询语言。查询主要有以下几种类型:
- 数据库查询语句(SELECT)
- 数据库更新查询(INSERT、DELETE、UPDATE)
- 用于创建/删除表的查询(CREATE、DELETE)
这里我们假设读者已经熟悉 SQL 语言的基础知识。
6.2.3.1. Statement 类
要向数据库发送任何 SQL 查询,Java 应用程序必须拥有一个 Statement 类型的对象。该对象将存储查询文本等内容。该对象必然与当前连接相关联。因此,这是已建立连接的一种方法,允许您创建用于发出 SQL 查询所需的 Statement 对象。如果 connection 是代表与数据库连接的对象,则可通过以下方式获取 Statement 对象:
获取 Statement 对象后,即可执行 SQL 查询。具体执行方式取决于查询是用于检索数据还是更新数据库。
6.2.3.2. 执行从数据库检索数据的查询
查询通常采用以下形式:
仅第一行中的关键字是必需的;其余均为可选。还有其他未在此处显示的关键字。
- 将对 `FROM` 关键字后列出的所有表执行连接
- 仅保留 `select` 关键字后面的列
- 仅保留满足 `where` 关键字条件的行
- 根据 `ORDER BY` 关键字中的表达式对所得行进行排序,即构成查询结果。
SELECT 的结果是一个表。如果我们考虑前面的 ARTICLES 表,并希望获取当前库存低于最低阈值的商品名称,我们会写:SELECT name FROM articles WHERE current\_stock < min\_stock*。如果希望按名称字母顺序排序,我们会写: select name from articles where current_stock < min_stock order by name*
要执行此类查询,Statement 类提供了 executeQuery 方法:
其中 query 是要执行的 SELECT 查询的文本。
因此,如果
- connection 是表示与数据库连接的对象
- Statement s = connection.createStatement() 将创建执行 SQL 查询所需的 Statement 对象
- ResultSet rs = s.executeQuery("select name from articles where current_stock < minimum_stock") 执行一个 SELECT 查询,并将查询的结果行赋值给一个 ResultSet 类型的对象。
6.2.3.3. ResultSet 类:SELECT 查询的结果
一个 ResultSet 对象代表一张表,即一组行和列。在任何给定时刻,只能访问表中的一行,称为当前行。在初始创建 ResultSet 时,如果 ResultSet 不为空,则当前行为第 1 行。要移动到下一行,ResultSet 类提供了 next 方法:
该方法尝试跳转到 ResultSet 中的下一行,若成功则返回 true,否则返回 false。若成功,下一行将成为新的当前行。上一行将被丢弃且无法检索。ResultSet 表包含列 col1、col2、... 等。要访问当前行的各个字段,可以使用以下方法:
来获取当前行中的“coli”字段。Type 指代 coli 字段的类型。getString 方法常用于所有字段,它允许您将字段内容作为字符串获取。随后可根据需要进行转换。若您不知道列名,可以使用以下方法
,其中 i 是目标列的索引(i>=1)。
6.2.3.4. 第一个示例
以下是一个程序,用于显示之前创建的 ARTICLES 数据库的内容:
import java.sql.*;
import java.io.*;
// displays the contents of a ARTICLES system database
public class articles1{
static final String DB="ARTICLES"; // database to exploit
public static void main(String arg[]){
Connection connect=null; // connection to base
Statement S=null; // purpose of queries
ResultSet RS=null; // query result table
try{
// base connection
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
connect=DriverManager.getConnection("jdbc:odbc:"+DB,"","");
System.out.println("Connexion avec la base " + DB + " établie");
// creation of a Statement object
S=connect.createStatement();
// execute a select query
RS=S.executeQuery("select * from " + DB);
// using the results table
while(RS.next()){ // as long as there's a line to operate
// it is displayed on screen
System.out.println(RS.getString("code")+","+
RS.getString("nom")+","+
RS.getString("prix")+","+
RS.getString("stock_actu")+","+
RS.getString("stock_mini"));
}// next line
} catch (Exception e){
erreur("Erreur " + e,2);
}
// closing the base
try{
connect.close();
System.out.println("Base " + DB + " fermée");
} catch (Exception e){}
}// hand
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
}// class
结果如下:
Connexion avec la base ARTICLES établie
a300,vélo,1202,30,2
d600,arc,5000,10,2
d800,canoé,1502,12,6
x123,fusil,3000,10,2
s345,skis nautiques,1800,3,2
f450,essai3,3,3,3
f807,cachalot,200000,0,0
z400,léopard,500000,1,1
g457,panthère,800000,1,1
Base ARTICLES fermée
6.2.3.5. ResultSetMetadata 类
在上一个示例中,我们已知 ResultSet 列的名称。如果不知道列名,则无法使用 getType(column_name) 方法。此时,我们需要使用 getType(column_number)。然而,要获取所有列,我们需要知道 ResultSet 包含多少列。ResultSet 类并不提供此信息,而是由 ResultSetMetaData 类提供。 更广泛地说,该类提供了关于表结构的信息,即其列的性质。
要获取 ResultSet 的结构信息,首先需实例化一个 ResultSetMetaData 对象。若 RS 是一个 ResultSet,则可通过以下方式获取其关联的 ResultSetMetaData:
ResultSetMetaData 类中有两个有用的方法:
- int getColumnCount(),该方法返回 ResultSet 中的列数
- String getColumnLabel(int i),返回 ResultSet 中第 i 个列的名称(i >= 1)
6.2.3.6. 第二个示例
前一个程序显示了 ARTICLES 数据库的内容。在此,我们将编写一个程序,用于执行用户在键盘上输入的任何针对 ARTICLES 数据库的 SQL SELECT 查询。
import java.sql.*;
import java.io.*;
// displays the contents of a ARTICLES system database
public class sql1{
static final String DB="ARTICLES"; // database to exploit
public static void main(String arg[]){
Connection connect=null; // connection to base
Statement S=null; // purpose of queries
ResultSet RS=null; // query result table
String select; // query text SQL select
int nbColonnes; // no. of columns in ResultSet
// creation of a keyboard input stream
BufferedReader in=null;
try{
in=new BufferedReader(new InputStreamReader(System.in));
} catch(Exception e){
erreur("erreur lors de l'ouverture du flux clavier ("+e+")",3);
}
try{
// base connection
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
connect=DriverManager.getConnection("jdbc:odbc:"+DB,"","");
System.out.println("Connexion avec la base " + DB + " établie");
// creation of a Statement object
S=connect.createStatement();
// execution loop for SQL requests typed on keyboard
System.out.print("Requête : ");
select=in.readLine();
while(!select.equals("fin")){
// query execution
RS=S.executeQuery(select);
// number of columns
nbColonnes=RS.getMetaData().getColumnCount();
// using the results table
System.out.println("Résultats obtenus\n\n");
while(RS.next()){ // as long as there's a line to operate
// it is displayed on screen
for(int i=1;i<nbColonnes;i++)
System.out.print(RS.getString(i)+",");
System.out.println(RS.getString(nbColonnes));
}// next line
// following request
System.out.print("Requête : ");
select=in.readLine();
}// while
} catch (Exception e){
erreur("Erreur " + e,2);
}
// closing the base and input stream
try{
connect.close();
System.out.println("Base " + DB + " fermée");
in.close();
} catch (Exception e){}
}// hand
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
}// class
以下是部分研究结果:
Connexion avec la base ARTICLES établie
Requête : select * from articles order by prix desc
Résultats obtenus
g457,panthère,800000,1,1
z400,léopard,500000,1,1
f807,cachalot,200000,0,0
d600,arc,5000,10,2
x123,fusil,3000,10,2
s345,skis nautiques,1800,3,2
d800,canoé,1502,12,6
a300,vélo,1202,30,2
f450,essai3,3,3,3
Requête : select nom, prix from articles where prix >10000 order by prix desc
Résultats obtenus
panthère,800000
léopard,500000
cachalot,200000
6.2.3.7. 执行数据库更新查询
Statement 对象用于存储 SQL 查询。该对象用于执行 SQL 更新查询(INSERT、UPDATE、DELETE)的方法不再是前面讨论的 executeQuery 方法,而是 executeUpdate 方法:
两者的区别在于返回结果:executeQuery 返回结果集 (ResultSet),而 executeUpdate 返回受更新操作影响的行数。
6.2.3.8. 第三个示例
我们将重新审视之前的程序并稍作修改:现在,在键盘上输入的查询已成为针对 ARTICLES 数据库的更新查询。
import java.sql.*;
import java.io.*;
// displays the contents of a ARTICLES system database
public class sql2{
static final String DB="ARTICLES"; // database to exploit
public static void main(String arg[]){
Connection connect=null; // connection to base
Statement S=null; // purpose of queries
ResultSet RS=null; // query result table
String sqlUpdate; // text of SQL update request
int nbLignes; // no. of lines affected by an update
// creation of a keyboard input stream
BufferedReader in=null;
try{
in=new BufferedReader(new InputStreamReader(System.in));
} catch(Exception e){
erreur("erreur lors de l'ouverture du flux clavier ("+e+")",3);
}
try{
// base connection
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
connect=DriverManager.getConnection("jdbc:odbc:"+DB,"","");
System.out.println("Connexion avec la base " + DB + " établie");
// creation of a Statement object
S=connect.createStatement();
// execution loop for SQL requests typed on keyboard
System.out.print("Requête : ");
sqlUpdate=in.readLine();
while(!sqlUpdate.equals("fin")){
// query execution
nbLignes=S.executeUpdate(sqlUpdate);
// follow-up
System.out.println(nbLignes + " ligne(s) ont été mises à jour");
// following request
System.out.print("Requête : ");
sqlUpdate=in.readLine();
}// while
} catch (Exception e){
erreur("Erreur " + e,2);
}
// closing the base and input stream
try{
// free up resources linked to the base
RS.close();
S.close();
connect.close();
System.out.println("Base " + DB + " fermée");
// keyboard flow closure
in.close();
} catch (Exception e){}
}// hand
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
}// class
以下是 sql1 和 sql2 程序多次运行的结果:
ARTICLES 数据库中的行列表:
E:\data\java\jdbc\0>java sql1
Connexion avec la base ARTICLES établie
Requête : select nom,stock_actu from articles
Résultats obtenus
vélo,30
arc,10
canoé,12
fusil,10
skis nautiques,3
essai3,3
cachalot,0
léopard,1
panthère,1
我们修改某些行:
E:\data\java\jdbc\0>java sql2
Connexion avec la base ARTICLES établie
Requête : update articles set stock_actu=stock_actu+1 where stock_actu>10
2 ligne(s) ont été mises à jour
验证:
E:\data\java\jdbc\0>java sql1
Connexion avec la base ARTICLES établie
Requête : select nom,stock_actu from articles
Résultats obtenus
vélo,31
arc,10
canoé,13
fusil,10
skis nautiques,3
essai3,3
cachalot,0
léopard,1
panthère,1
添加一行:
E:\data\java\jdbc\0>java sql2
Connexion avec la base ARTICLES établie
Requête : insert into articles (code,nom,prix,stock_actu,stock_mini) values ('x400','nouveau',200,20,10)
1 ligne(s) ont été mises à jour
验证:
E:\data\java\jdbc\0>java sql1
Connexion avec la base ARTICLES établie
Requ_te : select nom,stock_actu from articles
Résultats obtenus
vélo,31
arc,10
canoé,13
fusil,10
skis nautiques,3
essai3,3
cachalot,0
léopard,1
panthère,1
nouveau,20
删除一行:
E:\data\java\jdbc\0>java sql2
Connexion avec la base ARTICLES établie
Requête : delete from articles where code='x400'
1 ligne(s) ont été mises à jour
Requête : fin
验证:
E:\data\java\jdbc\0>java sql1
Connexion avec la base ARTICLES établie
Requête : select nom,stock_actu from articles
Résultats obtenus
vélo,31
arc,10
cano_,13
fusil,10
skis nautiques,3
essai3,3
cachalot,0
léopard,1
panthère,1
6.2.3.9. 执行任何 SQL 查询
用于执行 SQL 查询的 Statement 对象具有一个 execute 方法,该方法能够执行任何类型的 SQL 查询:
如果查询返回了一个 ResultSet*(executeQuery*),则返回值为布尔值 true;如果返回了一个数字(executeUpdate*),则返回值为 false。可以通过 getResultSet 方法获取生成的 ResultSet,并通过 getUpdateCount* 方法获取更新行的数量。因此,我们应编写如下代码:
Statement S=...;
ResultSet RS=...;
int nbLignes;
String requête=...;
// exécution d’une requête SQL
if (S.execute(requête)){
// on a un resultset
RS=S.getResultSet();
// exploitation du ResultSet
...
} else {
// c’était une requête de mise à jour
nbLignes=S.getUpdateCount();
...
}
6.2.3.10. 第四个示例
我们借鉴程序 sql1 和 sql2 的概念,将其应用到程序 sql3 中,该程序现在可以执行键盘上输入的任何 SQL 查询。为了使程序更具通用性,将要使用的数据库的特性作为参数传递给程序。
import java.sql.*;
import java.io.*;
// call: pg PILOTE URL UID MDP
// connects to the URL database using class JDBC PILOTE
// user UID is identified by password MDP
public class sql3{
static String syntaxe="pg PILOTE URL UID MDP";
public static void main(String arg[]){
// check number of arguments
if(arg.length<2 || arg.length>4)
erreur(syntaxe,1);
// init connection parameters
Connection connect=null;
String uid="";
String mdp="";
if(arg.length>=3) uid=arg[2];
if(arg.length==4) mdp=arg[3];
// other data
Statement S=null; // purpose of queries
ResultSet RS=null; // result table of a query request
String sqlText; // text of query SQL to be executed
int nbLignes; // no. of lines affected by an update
int nbColonnes; // nb of columns in a ResultSet
// creation of a keyboard input stream
BufferedReader in=null;
try{
in=new BufferedReader(new InputStreamReader(System.in));
} catch(Exception e){
erreur("erreur lors de l'ouverture du flux clavier ("+e+")",3);
}
try{
// base connection
Class.forName(arg[0]);
connect=DriverManager.getConnection(arg[1],uid,mdp);
System.out.println("Connexion avec la base " + arg[1] + " établie");
// creation of a Statement object
S=connect.createStatement();
// execution loop for SQL requests typed on keyboard
System.out.print("Requête : ");
sqlText=in.readLine();
while(!sqlText.equals("fin")){
// query execution
try{
if(S.execute(sqlText)){
// we have obtained a ResultSet - we exploit it
RS=S.getResultSet();
// number of columns
nbColonnes=RS.getMetaData().getColumnCount();
// using the results table
System.out.println("\nRésultats obtenus\n-----------------\n");
while(RS.next()){ // as long as there's a line to operate
// it is displayed on screen
for(int i=1;i<nbColonnes;i++)
System.out.print(RS.getString(i)+",");
System.out.println(RS.getString(nbColonnes));
}// next line of ResultSet
} else {
// it was an update request
nbLignes=S.getUpdateCount();
// follow-up
System.out.println(nbLignes + " ligne(s) ont été mises à jour");
}//if
} catch (Exception e){
System.out.println("Erreur " +e);
}
// following request
System.out.print("\nNouvelle Requête : ");
sqlText=in.readLine();
}// while
} catch (Exception e){
erreur("Erreur " + e,2);
}
// closing the base and input stream
try{
// free up resources linked to the base
RS.close();
S.close();
connect.close();
System.out.println("Base " + arg[1] + " fermée");
// keyboard flow closure
in.close();
} catch (Exception e){}
}// hand
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
}// class
我们创建以下查询文件:
select * from articles
update articles set stock_mini=stock_mini+5 where stock_mini<5
select nom,stock_mini from articles
insert into articles (code,nom,prix,stock_actu,stock_mini) values ('x400','nouveau',100,20,10)
select * from articles
delete from articles where code='x400'
select * from articles
fin
程序运行如下:
该程序从 queries 文件读取输入,并将输出写入 results 文件。所得结果如下:
Connexion avec la base jdbc:odbc:articles établie
Requête : (requete 1 du fichier des requetes : select * from articles)
Résultats obtenus
-----------------
a300,vélo,1202,31,3
d600,arc,5000,10,3
d800,canoé,1502,13,7
x123,fusil,3000,10,3
s345,skis nautiques,1800,3,3
f450,essai3,3,3,4
f807,cachalot,200000,0,1
z400,léopard,500000,1,2
g457,panthère,800000,1,2
Nouvelle Requête : (requete 2 du fichier des requetes : update articles set stock_mini=stock_mini+5 where stock_mini<5)
8 ligne(s) ont été mises à jour
Nouvelle Requête : (requete 3 du fichier des requetes : select nom,stock_mini from articles)
Résultats obtenus
-----------------
vélo,8
arc,8
canoé,7
fusil,8
skis nautiques,8
essai3,9
cachalot,6
léopard,7
panthère,7
Nouvelle Requête : (requete 4 du fichier des requetes : insert into articles (code,nom,prix,stock_actu,stock_mini) values ('x400','nouveau',100,20,10))
1 ligne(s) ont été mises à jour
Nouvelle Requête : (requete 5 du fichier des requetes : select * from articles)
Résultats obtenus
-----------------
a300,vélo,1202,31,8
d600,arc,5000,10,8
d800,canoé,1502,13,7
x123,fusil,3000,10,8
s345,skis nautiques,1800,3,8
f450,essai3,3,3,9
f807,cachalot,200000,0,6
z400,léopard,500000,1,7
g457,panthère,800000,1,7
x400,nouveau,100,20,10
Nouvelle Requête : (requete 6 du fichier des requêtes : delete from articles where code='x400')
1 ligne(s) ont été mises à jour
Nouvelle Requête : (requete 7 du fichier des requêtes : select * from articles)
Résultats obtenus
-----------------
a300,vélo,1202,31,8
d600,arc,5000,10,8
d800,canoé,1502,13,7
x123,fusil,3000,10,8
s345,skis nautiques,1800,3,8
f450,essai3,3,3,9
f807,cachalot,200000,0,6
z400,léopard,500000,1,7
g457,panthère,800000,1,7
6.3. 使用数据库进行税款计算
上次我们探讨税款计算问题时,使用了图形界面,且数据存储在文件中。我们将重新审视该版本,但此次假设数据存储在 ODBC-MySQL 数据库中。MySQL 是一款开源数据库管理系统(DBMS),可在包括 Windows 和 Linux 在内的多种平台上运行。使用该 DBMS 已创建了一个名为 dbimpots 的数据库,其中包含一个名为 impots 的表。 数据库访问通过用户名/密码进行控制,本例中为 admimpots/mdpimpots。下图展示了如何在 MySQL 中使用 dbimpots 数据库:
C:\Program Files\EasyPHP\mysql\bin>mysql -u admimpots -p
Enter password: *********
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 18 to server version: 3.23.49-max-nt
Type 'help;' or '\h' for help. Type '\c' to clear the buffer.
mysql> use dbimpots;
Database changed
mysql> show tables;
+--------------------+
| Tables_in_dbimpots |
+--------------------+
| impots |
+--------------------+
1 row in set (0.00 sec)
mysql> describe impots;
+---------+--------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+--------+------+-----+---------+-------+
| limites | double | YES | | NULL | |
| coeffR | double | YES | | NULL | |
| coeffN | double | YES | | NULL | |
+---------+--------+------+-----+---------+-------+
3 rows in set (0.02 sec)
mysql> select * from impots;
+---------+--------+---------+
| limites | coeffR | coeffN |
+---------+--------+---------+
| 12620 | 0 | 0 |
| 13190 | 0.05 | 631 |
| 15640 | 0.1 | 1290.5 |
| 24740 | 0.15 | 2072.5 |
| 31810 | 0.2 | 3309.5 |
| 39970 | 0.25 | 4900 |
| 48360 | 0.3 | 6898 |
| 55790 | 0.35 | 9316.5 |
| 92970 | 0.4 | 12106 |
| 127860 | 0.45 | 16754 |
| 151250 | 0.5 | 23147.5 |
| 172040 | 0.55 | 30710 |
| 195000 | 0.6 | 39312 |
| 0 | 0.65 | 49062 |
+---------+--------+---------+
14 rows in set (0.00 sec)
mysql>quit
该应用程序的图形界面如下:

图形界面已进行了一些更改:
否。 | 类型 | 名称 | 角色 |
1 | JTextField | txtConnection | ODBC 数据库连接字符串 |
2 | JScrollPane | JScrollPane1 | Textarea 的容器3 |
3 | JTextArea | txtStatus | 显示状态消息,包括错误消息 |
在 (1) 中输入的连接字符串采用以下格式:DSN;登录名;密码,其中
表示 ODBC 数据源的 DSN 名称 | |
具有数据库读取权限的用户身份 | |
其密码 |
dbimpots 数据库是使用 MySQL 手动创建的。将其转换为 ODBC 数据源的步骤如下:
- 启动 32 位 ODBC 数据源管理器

- 使用 [添加] 按钮添加新的 ODBC 数据源

- 选择 MySQL 驱动程序,然后单击 [完成]

- MySQL 驱动程序需要您提供以下信息:
1 | 要为 ODBC 数据源指定的 DSN 名称——名称可以是任意内容 |
2 | 运行 MySQL 数据库管理系统(DBMS)的机器——此处为 localhost。值得注意的是,该数据库也可能是远程数据库。使用 ODBC 数据源的本地应用程序不会察觉这一点。特别是对于我们的 Java 应用程序而言,情况正是如此。 |
3 | 要使用的 MySQL 数据库。MySQL 是一种管理关系型数据库的 DBMS,关系型数据库是由关系相互关联的表集合。在此,我们指定要管理的数据库名称。 |
4 | 具有此数据库访问权限的用户名 |
5 | 该用户的密码 |
定义好 ODBC 数据源后,我们可以测试我们的程序:
![]() | ![]() |
让我们看看与不使用数据库的图形化版本相比,代码有哪些修改。以下是迄今为止使用的 *imports* 类的代码:
// creation of an impots class
public class impots{
// data required for tax calculation
// come from an external source
private double[] limites, coeffR, coeffN;
// manufacturer
public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
// check that the 3 arrays have the same size
boolean OK=LIMITES.length==COEFFR.length && LIMITES.length==COEFFN.length;
if (! OK) throw new Exception ("Les 3 tableaux fournis n'ont pas la même taille("+
LIMITES.length+","+COEFFR.length+","+COEFFN.length+")");
// it's good
this.limites=LIMITES;
this.coeffR=COEFFR;
this.coeffN=COEFFN;
}//manufacturer
// tAX CALCULATION
public long calculer(boolean marié, int nbEnfants, int salaire){
// calculating the number of shares
double nbParts;
if (marié) nbParts=(double)nbEnfants/2+2;
else nbParts=(double)nbEnfants/2+1;
if (nbEnfants>=3) nbParts+=0.5;
// calculation of taxable income & family quota
double revenu=0.72*salaire;
double QF=revenu/nbParts;
// tAX CALCULATION
limites[limites.length-1]=QF+1;
int i=0;
while(QF>limites[i]) i++;
// return result
return (long)(revenu*coeffR[i]-nbParts*coeffN[i]);
}//calculate
}//class
该类通过构造函数接收的三个参数数组,构建出 limits、coeffR 和 coeffN 这三个数组。我们决定添加一个新的构造函数,以便能够从数据库中构建这三个数组:
public impots(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
throws SQLException,ClassNotFoundException{
// dsnIMPOTS: DSN database name
// userIMPOTS, mdpIMPOTS: database login/password
在此示例中,我们决定不在 *impots* 类中实现这个新构造函数,而是在其派生类 *impotsJDBC* 中实现:
// imported packages
import java.sql.*;
import java.util.*;
public class impotsJDBC extends impots{
// addition of a constructor for building
// limit tables, coeffr, coeffn from table
// database taxes
public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
throws SQLException,ClassNotFoundException{
// dsnIMPOTS: DSN database name
// userIMPOTS, mdpIMPOTS: database login/password
// data tables
ArrayList aLimites=new ArrayList();
ArrayList aCoeffR=new ArrayList();
ArrayList aCoeffN=new ArrayList();
// connection to base
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
Connection connect=DriverManager.getConnection("jdbc:odbc:"+dsnIMPOTS,userIMPOTS,mdpIMPOTS);
// creation of a Statement object
Statement S=connect.createStatement();
// select request
String select="select limites, coeffr, coeffn from impots";
// query execution
ResultSet RS=S.executeQuery(select);
while(RS.next()){
// running line operation
aLimites.add(RS.getString("limites"));
aCoeffR.add(RS.getString("coeffr"));
aCoeffN.add(RS.getString("coeffn"));
}// next line
// closing resources
RS.close();
S.close();
connect.close();
// data transfer to bounded arrays
int n=aLimites.size();
limites=new double[n];
coeffR=new double[n];
coeffN=new double[n];
for(int i=0;i<n;i++){
limites[i]=Double.parseDouble((String)aLimites.get(i));
coeffR[i]=Double.parseDouble((String)aCoeffR.get(i));
coeffN[i]=Double.parseDouble((String)aCoeffN.get(i));
}//for
}//manufacturer
}//class
构造函数从作为参数传递给它的数据库中读取 impots 表的内容,并填充三个数组:limites、coeffR 和 coeffN。可能会发生多种错误。构造函数不会处理这些错误,而是将它们“上报”给调用程序:
public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
throws SQLException,ClassNotFoundException{
如果仔细观察前面的代码,我们会发现 impotsJDBC 类直接使用了其基类 impots 的 limits、coeffR 和 coeffN 字段。由于这些字段被声明为 private:
因此,*importsJDBC* 类无法直接访问这些字段。为此,我们对基类进行初步修改,写入以下代码:
protected 属性允许从 impots 类派生的类直接访问带有此属性的字段。我们需要进行第二次修改。子类 impotsJDBC 的构造函数声明如下:
public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
throws SQLException,ClassNotFoundException{
我们知道,在构造子类的对象之前,必须先构造父类的对象。为此,子类的构造函数必须使用 super(....) 语句显式调用父类的构造函数。这里未这样做,因为我们无法确定应调用父类的哪个构造函数。 目前只有一个构造函数,但它并不适用。编译器随后会在父类中搜索可调用的无参构造函数。由于未找到,因此会引发编译错误。因此,我们在 impots 类中添加了一个无参构造函数:
我们将它声明为“protected”,以便仅允许子类使用。现在,*impots*类的骨架如下所示:
public class impots{
// data required for tax calculation
// come from an external source
protected double[] limites=null;
protected double[] coeffR=null;
protected double[] coeffN=null;
// empty builder
protected impots(){}
// manufacturer
public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
...........
}//manufacturer
// tAX CALCULATION
public long calculer(boolean marié, int nbEnfants, int salaire){
.............
}//calculate
}//class
我们应用程序中“初始化”菜单的操作如下:
void mnuInitialiser_actionPerformed(ActionEvent e) {
// retrieve the connection string
Pattern séparateur=Pattern.compile("\\s*;\\s*");
String[] champs=séparateur.split(txtConnexion.getText().trim());
// three fields are required
if(champs.length!=3){
// error
txtStatus.setText("Chaîne de connexion (DSN;uid;mdp) incorrecte");
// back to visual interface
txtConnexion.requestFocus();
return;
}//if
// load data
try{
// creation of impotsJDBC object
objImpots=new impotsJDBC(champs[0],champs[1],champs[2]);
// confirmation
txtStatus.setText("Données chargées");
// salary can be modified
txtSalaire.setEditable(true);
// no more chgt possible
mnuInitialiser.setEnabled(false);
txtConnexion.setEditable(false);
}catch(Exception ex){
// problem
txtStatus.setText("Erreur : " + ex.getMessage());
// end
return;
}//catch
}
一旦创建了 objImpots 对象,该应用程序就与之前编写的图形化应用程序完全相同。欢迎读者参考该应用程序。
6.4. 练习
6.4.1. 练习 1
为前面的 sql3 程序提供一个图形界面。
6.4.2. 练习 2
Java 小程序只能通过加载它的服务器访问数据库。由于小程序无法访问其运行机器的磁盘,因此数据库不能位于使用该小程序的客户端机器上。因此,我们面临以下情况:

运行 Applet 的机器、服务器以及托管数据库的机器可能是三台不同的机器。在此,我们假设数据库位于服务器上。
问题 1
用 Java 编写以下服务器应用程序:
- 该服务器应用程序在作为参数传递给它的端口上运行
- 当客户端连接时,服务器应用程序发送消息
- 随后,客户端将以以下格式发送连接数据库所需的参数以及要执行的查询:
参数之间用斜杠分隔。前四个参数是本章所述的 sql3 程序的参数。
- 随后,服务器应用程序会与指定的数据库建立连接(该数据库必须位于与服务器相同的机器上),并在其上执行 SQL 查询。结果将以以下格式返回给客户端:
...
如果是 Select 查询的结果,或者
以返回受更新查询影响的行数。如果发生数据库连接错误或查询执行错误,应用程序将返回
- 查询执行完成后,服务器应用程序将关闭连接。
问题 2
编写一个 Java 小程序来查询上述服务器。你可以参考练习 1 中的图形界面。由于服务器端口可能不同,因此需通过小程序的界面输入。发送行所需的所有参数也需通过此方式输入:
客户端必须发送给服务器的。
6.4.3. 练习 3
下文描述了一个原本打算用 Visual Basic 解决的问题。请将其改编为可在 Java 小程序中处理的形式。该小程序将依赖于练习 2 中的服务器。图形界面可根据新的执行环境进行修改。
我们建议创建一个应用程序,用于演示对 ACCESS 数据库中某张表可能进行的各种更新操作。该 ACCESS 数据库名为 articles.mdb。其中包含一张名为 articles 的表,列出了某公司销售的商品。其结构如下:
name | 类型 |
类型 | 4位商品代码 |
名称 | 其名称(字符串) |
price | 其价格(实际) |
当前库存 | 当前库存(整数) |
min_stock | 最低库存(整数),低于该数值时必须补货 |
我们建议使用以下表单查看和更新此表格:

此表单上的控件如下:
编号 | 类型 | 名称 | 功能 |
1 | 文本框 | 记录 | 显示的记录编号 启用状态为 false |
2 | 文本框 | 代码 | 项目代码 |
3 | 文本框 | 名称 | 项目名称 |
4 | 文本框 | 价格 | 商品价格 |
5 | 文本框 | 当前 | 该商品的当前库存 |
6 | 文本框 | minimum | 该商品的最低库存 |
7 | 数据 | data1 | 与数据库相关的数据控件 databasename=articles.mdb 文件的路径 recordsource=articles connect=access |
8 | HScrollBar | 位置 | 允许您在表格中导航 |
9 | 按钮 | 确定 | 用于确认更新——仅在更新过程中显示 |
10 | 按钮 | 取消 | 允许您取消更新 - 仅在更新过程中显示 |
11 | 帧 | frame1 | 出于美观考虑 |
12 | 文本框 | 基名 | 打开的数据库名称 enabled 设置为 false |
13 | 文本框 | sourcename | 打开的表的名称 enabled 设置为 false |
控件 | 特殊功能 |
data1 | databasename 和 recordsource 字段已填写完毕。databasename 必须指向您目录中的 Access 数据库 articles.mdb,而 recordsource 则指向 articles 表。 |
code | 这是一个文本框,我们希望将其与 data1 中当前记录的 code 字段建立关联。为此,我们需要填写两个字段: 数据源:输入 data1,表示该文本框与 data1 关联的表相关联 数据字段:从 articles 表中选择 code 字段 完成这些步骤后,code文本框将始终显示data1中当前记录的code字段内容。反之,修改此文本框的内容将更新当前记录的code字段。 对其他文本框也进行同样的设置 |
name | 数据源:data1 数据字段:name |
price | 数据源:data1 数据字段:price |
当前 | 数据源:data1 数据字段:current_stock |
minimum | 数据源:data1 数据字段:price |
菜单结构如下
编辑 | 浏览 | 退出 |
添加 | 返回 | |
编辑 | 下一页 | |
删除 | 首页 | |
最后 |
各项选项的作用如下:
菜单 | 名称 | 功能 |
mnuadd | 向 items 表添加新记录 | |
delete | 以从 items 表中删除当前显示的记录 | |
编辑 | 用于编辑项目表中当前显示的记录 | |
mnuprecedent | 转到上一条记录 | |
向下-下一条 | 跳至下一条录音 | |
上一首 | 上一首 | |
mnunlast | 跳至最后一个录音 | |
mnuquit | 退出应用程序 |
在 form_load 事件期间,将打开与 data1 关联的数据表(data1.refresh)。如果数据表无法打开,将显示错误消息并终止程序(end)。否则,将显示表单,并显示 articles 表中的第一条记录。字段不允许输入(enabled 属性设置为 false)。仅可通过“添加”和“编辑”选项进行输入。 “确定”和“取消”按钮被隐藏(visible=false)。
我们建议先构建与各项菜单选项以及“确定”和“取消”按钮相关的过程。目前,我们将忽略以下几点:
- 启用/禁用某些菜单选项:例如,如果光标位于表中的最后一条记录上,则“下一条”选项必须被禁用
- 管理水平滚动条
“浏览/下一条”菜单
- 如果尚未到达文件末尾(data1.RecordSet.EOF),则跳转到下一条记录(data1.RecordSet.MoveNext)。更新记录文本框(data1.RecordSet.AbsolutePosition / data1.RecordSet.RecordCount)。
“浏览/上一条”菜单
- 若未位于文件开头 (data1.RecordSet.BOF),则跳转到上一条记录 (data1.RecordSet.MovePrevious)。更新记录文本框。
浏览/首页菜单
- 如果文件不为空(data1.RecordSet.RecordCount = 0),则跳转到第一条记录(data1.RecordSet.MoveFirst)。更新记录文本框。
“浏览/末条”菜单
- 如果文件不为空(data1.RecordSet.RecordCount = 0),则移至最后一条记录(data1.RecordSet.MoveLast)。更新记录文本框。
编辑/添加菜单
- 允许您向表中添加一条记录
- 进入“添加记录”模式(data1.recordset.addnew)
- 启用 5 个字段的数据输入:代码、名称、价格等(enabled=true)
- 禁用“编辑”、“浏览”和“退出”菜单(enabled=false)
- 显示“确定”和“取消”按钮(visible=true)
“确定”按钮
- 保存记录更新(data1.recordset.Update)
- 隐藏“确定”和“取消”按钮(visible=false)
- 启用“编辑”、“浏览”和“退出”选项(enabled=true)
- 更新表单文本框
“取消”按钮
- 取消记录更新(data1.recordset.CancelUpdate)
- 隐藏“确定”和“取消”按钮(visible=false)
- 启用“编辑”、“浏览”和“退出”选项(enabled=true)
- 更新表单文本框
“编辑/修改”菜单
- 允许您编辑表单上显示的记录
- 进入记录编辑模式 (data1.recordset.edit)
- 启用 4 个字段(名称、价格等)的输入(enabled=true),但不启用代码字段的输入(enabled=false)
- 禁用“编辑”、“浏览”和“退出”菜单(enabled=false)
- 显示“确定”和“取消”按钮(visible=true)
编辑/删除菜单
- 允许您从表中删除(data1.recordset.delete)当前显示的记录
- 移至下一条记录(data1.recordset.movenext)
退出菜单
- 卸载表单(unload me)
form_unload 事件(cancel 作为整数)
- 由“unload me”操作、使用 Alt-F4 关闭表单或双击系统托盘图标触发,因此不一定是通过“退出”选项触发。
- 显示“您真的要退出应用程序吗?”的问题,并带有两个“是/否”按钮(msgbox,style=vbyes+vbno)
- 如果答案为“否”(=vbno),则将 cancel 设置为 -1 并退出 form_unload 过程。cancel 设置为 -1 表示拒绝关闭窗口。
- 如果回答是“是”(=vbyes),则关闭数据库(data1.recordset.close, data1.database.close)。
在此,我们将重点关注菜单的启用/禁用。在每次更改当前记录的操作之后,我们将调用一个名为 Oueston 的过程。该过程将检查以下条件:
. 如果文件为空,我们将
- 禁用“浏览”、“编辑/删除”菜单,
- 并启用其余菜单
。 如果当前记录是第一条记录,则
- 禁用“浏览/上一条”
- 将允许其余部分
。 如果当前记录是最后一条,
- 禁用“浏览/下一条”
- 将允许其余操作
水平滑块有三个重要字段:
- min:其最小值
- max:其最大值
- value:其当前值
初始化滑块
在加载时(form_load),滑块将按以下方式初始化:
- min=0
- max=data1.recordset.recordcount-1
- value=1
请注意,在打开数据库(data1.refresh)后,data1.recordset.recordcount 所表示的表中记录数是不正确的。您必须先移动到表的末尾(MoveLast),然后返回表的开头(MoveFirst),该数值才会正确。
对驱动器的直接操作
滑块光标的位置代表表中的位置。
当用户移动滑块(此处命名为 position)时,将触发 position_change 事件。在此事件中,我们将更改表中的当前记录,使其反映滑块上的移动。为此,我们将使用 data1.recordset 的 absoluteposition 字段。当我们将值 i 赋给该字段时,表中的第 i 条记录将成为当前记录。 记录编号从 0 开始,因此编号范围为 [0,data1.recordset.recordcount-1]。在 position_change 过程 中,我们只需编写
这样,表单上显示的当前记录就能反映摇杆的操作。
完成此操作后,我们将调用 Oueston 过程来更新菜单。
更新 DCS
由于驱动程序光标的位置必须反映表中的位置,因此每当表中的当前记录发生变化(由某个菜单触发)时,都必须更新驱动程序的值。由于每次调用都会调用 Oueston 过程,因此最好将此更新也放在该过程内。只需在此处编写:
我们添加了“浏览/搜索”选项,允许用户通过输入代码来查看商品。
启用此选项时,将执行以下步骤:
- 系统将切换至“新增”模式(Addnew),此举仅为避免修改启用该选项时已打开的当前记录,
- 我们启用代码字段的输入功能,并清空记录字段的内容,
- 禁用所有菜单,并显示“确定”和“取消”按钮
- 当用户点击“确定”时,我们必须搜索与用户输入代码对应的记录。然而,“OK_click”过程已被用于“浏览/添加”和“浏览/修改”选项。为区分这些情况,我们需要管理一个全局变量(此处称为“state”),该变量将有三个可能的值:“add”、“modify”和“search”。 与“确定”和“取消”按钮关联的程序将使用该变量来确定其被调用的上下文。
- 如果 state 为“search”,在与 OK 按钮关联的存储过程中,我们将
- 将取消 addnew 操作(data1.recordset.cancelupdate),因为我们本意并非添加记录。请注意,此时当前记录将恢复为“浏览/搜索”操作执行前显示在屏幕上的那条记录。
- 根据代码构建搜索条件并启动搜索(data1.recordset.findfirst criteria),
- 如果搜索失败(data1.recordset.nomatch=true),则通知用户,随后返回添加模式(addnew)并退出 OK 过程。用户需要重新输入新代码或选择“取消”选项。
- 如果搜索成功,找到的记录将成为新的活动记录。我们将恢复菜单,隐藏“确定/取消”按钮,禁用代码字段的输入,并退出该过程。
- . 如果状态为“搜索”,在与“取消”关联的程序中,我们
- 将取消 addnew 操作(data1.recordset.cancelupdate)。系统随后将自动返回至“浏览/搜索”操作之前的活动记录。
- 恢复菜单,隐藏“确定/取消”按钮,禁用代码字段的输入,并退出该过程。
项目必须通过其代码进行唯一标识。请确保在“浏览/添加”选项中,如果待添加记录的项目代码在表中已存在,则拒绝添加。
6.4.4. 练习 4
在此,我们将展示一个基于练习 2 中服务器构建的 Web 应用程序。这是一个基本的电子商务应用程序。
客户通过以下 Web 界面订购商品:

用户可以执行以下操作:
- 从下拉列表中选择商品
- 指定所需数量
- 点击“购买”按钮确认购买
- 其购买的商品将显示在已购商品列表中
- 用户可通过选中商品并点击“移除”按钮从该列表中删除商品
- 当用户点击“摘要”按钮时,将看到以下摘要:

该摘要允许用户查看其发票的详细信息。用户可以通过点击“信息”按钮,查看下拉列表中选定项目的详细信息:

用户请求发票摘要后,可在下一页进行确认。为此,用户必须输入电子邮件地址,并通过相应的按钮确认订单。

在提交订单前,应用会要求用户确认:

订单确认后,应用将进行处理并显示确认页面:

实际上,该应用并未处理订单。它只是向用户发送一封电子邮件,要求用户支付所购商品的款项:
Cher client,
Vous trouverez ci-dessous le détail de votre commande au magasin SuperPrix. Elle vous sera livrée après réception de votre chèque établi à l'ordre de SuperPrix et à envoyer à l'adresse suivante :
SuperPrix
ISTIA
62 av Notre-Dame du Lac
49000 Angers
France
Nous vous remercions vivement de votre commande
----------------------------------------
Votre commande
----------------------------------------
article, quantité, prix unitaire, total
========================================
vélo, 2, 1202.00 F, 2404.00 F
skis nautiques, 3, 1800.00 F, 5400.00 F
Total à payer : 7804 F
问题:使用 Java 小程序创建与该 Web 应用程序功能相当的程序。

