6. 使用 API 管理数据库 JDBC
6.1. 概述
市场上有许多数据库。 为了统一在 Windows 系统下访问数据库的方式,微软开发了一个名为 Open Connectivity 的接口。该接口将各数据库的特性封装在标准接口之下。 在 Windows 系统中,存在许多便于访问数据库的驱动程序。例如,以下是安装在 Win95 机器上的驱动程序列表:

基于这些驱动程序的应用程序无需重写代码,即可使用上述任何数据库。

为了使 Java 应用程序也能利用 ODBC 接口, Sun 创建了 JDBC 接口(Java DataBase 连接性),该接口将位于 Java 应用程序与 ODBC 接口之间:

6.2. 数据库运维的关键步骤
6.2.1. 简介
在使用JAVA接口连接数据库的JAVA应用程序中,通常包含以下步骤:
- 连接数据库
- 向数据库发送 SQL 查询
- 接收并处理这些查询的结果
- 关闭连接
步骤2和3会反复执行,而连接关闭仅在数据库操作结束时进行。这是一个相对经典的流程,如果您曾进行过交互式数据库操作,可能对此已很熟悉。我们将通过一个示例详细说明每个步骤。 我们考虑一个名为 ACCESS 的数据库,其名称为 ARTICLES,结构如下:
名称 | 类型 |
code | 4位商品代码 |
nom | 名称(字符串) |
prix | 其价格(实际) |
stock_actu | 当前库存(整数) |
stock_mini | 最低库存(整数),低于该数值时需补货 |
该数据库 ACCESS 在数据库管理器中被定义为“用户”数据源 ODBC:


其特征通过按钮 Configurer 如下所示:

该配置主要涉及将数据库 ARTICLES 与其对应的 Access 文件 articles.mdb 关联。完成此操作后,使用接口 ODBC 的应用程序即可访问数据库 ARTICLES。
6.2.2. 连接步骤
要使用数据库,Java应用程序必须首先执行连接操作。该操作通过以下类方法实现:
其中
DriverManager:包含应用程序可用驱动程序列表的 Java 类
Connection:Java类,用于在应用程序与数据库之间建立连接,通过该连接,应用程序将向数据库发送查询 SQL 并接收结果
URL:用于标识数据库的名称。该名称类似于互联网中的URL。因此它属于URL类。但此处与互联网毫无关联。 URL的格式如下:
jdbc:nom_du_pilote:nom_de_la_source;param=val1;param2=val2
在我们的示例中,我们将仅使用 ODBC 驱动程序,该驱动程序名为 odbc。URL 的第三部分由数据源名称及其可能的参数组成。 在我们的示例中,将使用系统已知的数据源。因此,先前定义的数据源 Articles 的 URL 将为
jdbc:odbc:Articles
id:用户名(login)
mdp:用户密码(password)
简而言之,该程序连接到一个数据库:
- 通过名称(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){
// 处理异常
}
要连接数据库,必须安装相应的驱动程序。在我们的示例中,需要使用能够管理所需数据库的 ODBC 驱动程序。 若需在机器上的 ODBC 驱动程序列表中提供该驱动程序,还必须具备 JAVA 类,该类将作为其接口。为此,应用程序将按以下方式请求所需的类:
Class 类与 JDBC 接口毫无关联。它是一个通用的类管理类。其静态方法 forName 允许动态加载一个类,从而利用该类的静态属性和方法。 与 Windows 系统中 MS 驱动程序进行接口交互的类名为“sun.jdbc.odbc.JdbcOdbcDriver”。因此应编写如下代码(该方法可能引发异常):
接口 JDBC 所需的类位于包 java.sql 中。因此,在程序开头应编写:
以下是一个用于连接数据库的程序:
import java.sql.*;
import java.io.*;
// 调用:pg PILOTE URL UID MDP
// 通过类 JDBC PILOTE 连接到数据库 URL
// 用户 UID 通过密码 MDP 进行身份验证
public class connexion1{
static String syntaxe="pg PILOTE URL UID MDP";
public static void main(String arg[]){
// 验证参数数量
if(arg.length<2 || arg.length>4)
erreur(syntaxe,1);
// 连接数据库
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);
}
// 关闭数据库
try{
connect.close();
System.out.println("Base " + arg[1] + " fermée");
} catch (Exception e){}
}// 主程序
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
}// 类
以下是一个运行示例:
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 对象。 如果 connexion 是表示与数据库连接的对象,则可通过以下方式获取 Statement 对象:
一旦获得 Statement 对象,即可发出 SQL 请求。具体操作方式将根据请求是查询请求还是数据库更新请求而有所不同。
6.2.3.2. 发送数据库查询请求
查询请求通常采用以下形式:
只有第一行的关键字是必填的,其余均为可选。还有其他未在此列出的关键字。
- 会与位于 from 关键字后面的所有表进行连接
- 仅保留位于 select 关键字后面的列
- 仅保留满足 where 关键字条件的行
- 根据 order by 关键字的表达式排序后的结果行即构成查询结果。
select的查询结果是一个表。 若参考前面的 ARTICLES 表,并希望获取当前库存低于最低阈值的商品名称,则应编写:select 商品名称 from 商品 where stock_actu<stock_mini。 如果按名称的字母顺序排序,则应写为: select 名称 from 商品 where stock_actu<stock_mini order by 名称
要执行此类查询,Statement类提供了executeQuery方法:
其中 requête 是待发送的 SELECT 查询的文本。
因此,如果
- connexion 是表示与数据库连接的对象
- Statement s=connexion.createStatement() 会创建 Statement 对象,该对象是发送 SQL 查询所必需的
- ResultSet rs=s.executeQuery(" select 名称 from 文章 where stock_actu<stock_mini”) 执行 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.*;
// 显示系统数据库的内容 ARTICLES
public class articles1{
static final String DB="ARTICLES"; // 待处理的数据库
public static void main(String arg[]){
Connection connect=null; // 连接数据库
Statement S=null; // 查询对象
ResultSet RS=null; // 查询结果表
try{
// 数据库连接
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
connect=DriverManager.getConnection("jdbc:odbc:"+DB,"","");
System.out.println("Connexion avec la base " + DB + " établie");
// 创建Statement对象
S=connect.createStatement();
// 执行 SELECT 查询
RS=S.executeQuery("select * from " + DB);
// 处理结果集
while(RS.next()){ // 只要还有待处理的行
// 将其显示在屏幕上
System.out.println(RS.getString("code")+","+
RS.getString("nom")+","+
RS.getString("prix")+","+
RS.getString("stock_actu")+","+
RS.getString("stock_mini"));
}// 下一行
} catch (Exception e){
erreur("Erreur " + e,2);
}
// 关闭数据库
try{
connect.close();
System.out.println("Base " + DB + " fermée");
} catch (Exception e){}
}// 主
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
}// 类
所得结果如下:
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(nom_colonne) 方法。 此时应改用 getType(列号)。然而,若要获取所有列,我们需要知道所获取的 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.*;
// 显示系统数据库的内容 ARTICLES
public class sql1{
static final String DB="ARTICLES"; // 待处理的数据库
public static void main(String arg[]){
Connection connect=null; // 连接数据库
Statement S=null; // 查询对象
ResultSet RS=null; // 查询结果表
String select; // 查询文本 SQL select
int nbColonnes; // ResultSet 的列数
// 创建键盘输入流
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{
// 连接数据库
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
connect=DriverManager.getConnection("jdbc:odbc:"+DB,"","");
System.out.println("Connexion avec la base " + DB + " établie");
// 创建一个Statement对象
S=connect.createStatement();
// 执行通过键盘输入的 SQL 查询的循环
System.out.print("Requête : ");
select=in.readLine();
while(!select.equals("fin")){
// 执行查询
RS=S.executeQuery(select);
// 列数
nbColonnes=RS.getMetaData().getColumnCount();
// 处理结果表
System.out.println("Résultats obtenus\n\n");
while(RS.next()){ // 只要还有待处理的行
// 将其显示在屏幕上
for(int i=1;i<nbColonnes;i++)
System.out.print(RS.getString(i)+",");
System.out.println(RS.getString(nbColonnes));
}// 下一行
// 下一条查询
System.out.print("Requête : ");
select=in.readLine();
}// while
} catch (Exception e){
erreur("Erreur " + e,2);
}
// 关闭数据库和输入流
try{
connect.close();
System.out.println("Base " + DB + " fermée");
in.close();
} catch (Exception e){}
}// main
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
}// 类
以下是获得的部分结果:
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.*;
// 显示系统数据库的内容ARTICLES
public class sql2{
static final String DB="ARTICLES"; // 待使用的数据库
public static void main(String arg[]){
Connection connect=null; // 连接数据库
Statement S=null; // 查询对象
ResultSet RS=null; // 查询结果表
String sqlUpdate; // 更新查询 SQL 的文本
int nbLignes; // 受更新影响的行数
// 创建键盘输入流
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{
// 连接数据库
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
connect=DriverManager.getConnection("jdbc:odbc:"+DB,"","");
System.out.println("Connexion avec la base " + DB + " établie");
// 创建语句对象
S=connect.createStatement();
// 执行键盘输入的 SQL 查询的循环
System.out.print("Requête : ");
sqlUpdate=in.readLine();
while(!sqlUpdate.equals("fin")){
// 执行查询
nbLignes=S.executeUpdate(sqlUpdate);
// 跟踪
System.out.println(nbLignes + " ligne(s) ont été mises à jour");
// 下一个查询
System.out.print("Requête : ");
sqlUpdate=in.readLine();
}// while
} catch (Exception e){
erreur("Erreur " + e,2);
}
// 关闭数据库和输入流
try{
// 释放与数据库相关的资源
RS.close();
S.close();
connect.close();
System.out.println("Base " + DB + " fermée");
// 关闭键盘流
in.close();
} catch (Exception e){}
}// 手
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
}// 类
以下是多次执行程序 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 请求:
若请求返回的是布尔值,则返回结果为 true;若返回的是 ResultSet(executeQuery),则返回 true;若返回的是数字,则返回 false (executeUpdate)。通过方法 getResultSet 可获取得到的 ResultSet,并通过方法 getUpdateCount 获取更新行的数量。因此,代码应写为:
Statement S=...;
ResultSet RS=...;
int nbLignes;
String requête=...;
// 执行查询SQL
if (S.execute(requête)){
// 获得结果集
RS=S.getResultSet();
// 处理 ResultSet
...
} else {
// 这是一条更新查询
nbLignes=S.getUpdateCount();
...
}
6.2.3.10. 第四个示例
我们借鉴了程序 sql1 和 sql2 的设计思路,创建了程序 sql3,该程序现在能够执行通过键盘输入的任何 SQL 查询。 为了使程序更具通用性,待处理数据库的特征被作为参数传递给程序。
import java.sql.*;
import java.io.*;
// 调用:pg PILOTE URL UID MDP
// 通过类 JDBC PILOTE 连接到数据库 URL
// 用户 UID 通过密码 MDP 进行身份验证
public class sql3{
static String syntaxe="pg PILOTE URL UID MDP";
public static void main(String arg[]){
// 验证参数数量
if(arg.length<2 || arg.length>4)
erreur(syntaxe,1);
// 初始化连接参数
Connection connect=null;
String uid="";
String mdp="";
if(arg.length>=3) uid=arg[2];
if(arg.length==4) mdp=arg[3];
// 其他数据
Statement S=null; // 查询发送对象
ResultSet RS=null; // 查询结果表
String sqlText; // 待执行的 SQL 查询文本
int nbLignes; // 受更新操作影响的行数
int nbColonnes; // ResultSet 的列数
// 创建键盘输入流
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{
// 连接数据库
Class.forName(arg[0]);
connect=DriverManager.getConnection(arg[1],uid,mdp);
System.out.println("Connexion avec la base " + arg[1] + " établie");
// 创建语句对象
S=connect.createStatement();
// 执行通过键盘输入的 SQL 查询的循环
System.out.print("Requête : ");
sqlText=in.readLine();
while(!sqlText.equals("fin")){
// 执行查询
try{
if(S.execute(sqlText)){
// 已获取 ResultSet - 进行处理
RS=S.getResultSet();
// 列数
nbColonnes=RS.getMetaData().getColumnCount();
// 处理结果表
System.out.println("\nRésultats obtenus\n-----------------\n");
while(RS.next()){ // 只要还有待处理的行
// 将其显示在屏幕上
for(int i=1;i<nbColonnes;i++)
System.out.print(RS.getString(i)+",");
System.out.println(RS.getString(nbColonnes));
}// ResultSet 的下一行
} else {
// 这是一条更新请求
nbLignes=S.getUpdateCount();
// 后续
System.out.println(nbLignes + " ligne(s) ont été mises à jour");
}//if
} catch (Exception e){
System.out.println("Erreur " +e);
}
// 下一个请求
System.out.print("\nNouvelle Requête : ");
sqlText=in.readLine();
}// while
} catch (Exception e){
erreur("Erreur " + e,2);
}
// 关闭数据库和输入流
try{
// 释放与数据库相关的资源
RS.close();
S.close();
connect.close();
System.out.println("Base " + arg[1] + " fermée");
// 关闭键盘流
in.close();
} catch (Exception e){}
}// main
public static void erreur(String msg, int exitCode){
System.err.println(msg);
System.exit(exitCode);
}
}// 类
建立以下查询文件:
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
程序的运行方式如下:
因此,该程序从文件 requetes 读取输入,并将输出写入文件 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. IMPOTS 带数据库
上次我们处理税款计算问题时,使用的是图形界面,数据存储在文件中。 我们在此基础上重新开发了该版本,并假设数据现在存储在 ODBC-MySQL 数据库中。MySQL 是一个公共领域的 SGBD,可在包括 Windows 和 Linux 在内的不同平台上使用。 使用此 SGBD 时,已创建名为 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 | txtConnexion | 数据库连接字符串ODBC |
2 | JScrollPane | JScrollPane1 | 文本区域容器 3 |
3 | JTextArea | txtStatus | 显示状态消息,特别是错误消息 |
在(1)中输入的登录串格式如下:DSN;登录名;密码,其中
数据源名称为 DSN,数据源为 ODBC | |
| 具有数据库读取权限的用户身份 |
| 其密码 |
数据库 dbimpots 是通过 MySQL 手动创建的。将其转换为数据源 ODBC 的方法如下:
- 启动 32 位数据源管理器 ODBC

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

- 指定驱动程序 MySQL,并执行 [Terminer]

- 驱动程序 MySQL 需要一些信息:
1 | 需为数据源 ODBC 指定的名称 DSN —— 可能为任意名称 |
2 | 运行 SGBD 的机器 MySQL - 此处为 localhost。值得注意的是,该数据库可能是远程数据库。 使用数据源 ODBC 的本地应用程序不会察觉到这一点。我们的 Java 应用程序就是如此。 |
3 | 要使用的数据库为 MySQL。MySQL 是一个 SGBD,用于管理关系型数据库,即通过关系相互关联的表集合。此处指定了所管理的数据库名称。 |
4 | 具有该数据库访问权限的用户名 |
5 | 其密码 |
定义好数据源 ODBC 后,即可测试我们的程序:
![]() | ![]() |
让我们来查看代码,与不使用数据库的图形化版本相比,该代码已进行了修改。回顾一下迄今为止使用的 impots 类的代码:
// 创建税类
public class impots{
// 计算税款所需的数据
// 来自外部来源
private double[] limites, coeffR, coeffN;
// 构造函数
public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
// 验证三个数组是否大小相同
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+")");
// 没问题
this.limites=LIMITES;
this.coeffR=COEFFR;
this.coeffN=COEFFN;
}//生成器
// 计算税额
public long calculer(boolean marié, int nbEnfants, int salaire){
// 计算份额数
double nbParts;
if (marié) nbParts=(double)nbEnfants/2+2;
else nbParts=(double)nbEnfants/2+1;
if (nbEnfants>=3) nbParts+=0.5;
// 应税收入及家庭系数计算
double revenu=0.72*salaire;
double QF=revenu/nbParts;
// 税额计算
limites[limites.length-1]=QF+1;
int i=0;
while(QF>limites[i]) i++;
// 返回结果
return (long)(revenu*coeffR[i]-nbParts*coeffN[i]);
}//计算
}//分类
该类根据构造函数中传入的三个数组,构建出三个数组 limites、coeffR 和 coeffN。我们决定为其添加一个新的构造函数,以便从数据库中构建这三个相同的数组:
public impots(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
throws SQLException,ClassNotFoundException{
// dsnIMPOTS:数据库名称 DSN
// userIMPOTS, mdpIMPOTS:数据库登录名/密码
在此示例中,我们决定不在类 impots 中实现此新构造函数,而是在其派生类 impotsJDBC 中实现:
// 导入的软件包
import java.sql.*;
import java.util.*;
public class impotsJDBC extends impots{
// 添加了一个用于构建的生成器
// 根据表
// 从数据库中导入税项
public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
throws SQLException,ClassNotFoundException{
// dsnIMPOTS:数据库名称
// userIMPOTS, mdpIMPOTS:数据库登录名/密码
// 数据表
ArrayList aLimites=new ArrayList();
ArrayList aCoeffR=new ArrayList();
ArrayList aCoeffN=new ArrayList();
// 连接数据库
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
Connection connect=DriverManager.getConnection("jdbc:odbc:"+dsnIMPOTS,userIMPOTS,mdpIMPOTS);
// 创建Statement对象
Statement S=connect.createStatement();
// SELECT 查询
String select="select limites, coeffr, coeffn from impots";
// 执行查询
ResultSet RS=S.executeQuery(select);
while(RS.next()){
// 处理当前行
aLimites.add(RS.getString("limites"));
aCoeffR.add(RS.getString("coeffr"));
aCoeffN.add(RS.getString("coeffn"));
}// 下一行
// 关闭资源
RS.close();
S.close();
connect.close();
// 将数据传输到限定数组
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
}//构造函数
}//类
该生成器读取作为参数传递的数据库中 impots 表的内容,并填充三个数组 limites、coeffR、coeffN。 可能会发生一些错误。构造函数不处理这些错误,而是将它们“上报”给调用程序:
public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
throws SQLException,ClassNotFoundException{
若仔细观察上述代码,会发现类 impotsJDBC 直接使用了其基类 limites、 coeffR、coeffN。由于这些字段被声明为私有:
因此类impotsJDBC无法直接访问这些字段。因此,我们首先对基类进行修改,写入:
属性 protected 允许从类 impots 派生的类直接访问使用该属性声明的字段。我们需要进行第二次修改。子类 impotsJDBC 的构造函数声明如下:
public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
throws SQLException,ClassNotFoundException{
我们知道,在构造子类对象之前,必须先构造父类对象。为此,子类的构造函数必须通过 super(....) 语句显式调用父类的构造函数。此处未进行此操作,因为我们无法确定应调用父类的哪个构造函数。 目前只有一个构造函数,但它并不适用。编译器会尝试在父类中查找可调用的无参构造函数。由于未找到,编译时会报错。因此,我们在 impots 类中添加了一个无参构造函数:
我们将它声明为“protected”,以便仅允许子类使用。现在,impots类的骨架如下:
public class impots{
// 计算税款所需的数据
// 来自外部来源
protected double[] limites=null;
protected double[] coeffR=null;
protected double[] coeffN=null;
// 制造商(空)
protected impots(){}
// 制造商
public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
...........
}//制造商
// 税款计算
public long calculer(boolean marié, int nbEnfants, int salaire){
.............
}//计算
}//类
我们应用程序中菜单项 Initialiser 的操作变为如下:
void mnuInitialiser_actionPerformed(ActionEvent e) {
// 获取连接字符串
Pattern séparateur=Pattern.compile("\\s*;\\s*");
String[] champs=séparateur.split(txtConnexion.getText().trim());
// 需要三个字段
if(champs.length!=3){
// 错误
txtStatus.setText("Chaîne de connexion (DSN;uid;mdp) incorrecte");
// 返回图形界面
txtConnexion.requestFocus();
return;
}//if
// 加载数据
try{
// 创建对象 impotsJDBC
objImpots=new impotsJDBC(champs[0],champs[1],champs[2]);
// 确认
txtStatus.setText("Données chargées");
// 工资可修改
txtSalaire.setEditable(true);
// 无法再进行更改
mnuInitialiser.setEnabled(false);
txtConnexion.setEditable(false);
}catch(Exception ex){
// 问题
txtStatus.setText("Erreur : " + ex.getMessage());
// 结束
return;
}//捕获
}
创建完对象 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 的唯一表,该表记录了某公司销售的商品。其结构如下:
名称 | 类型 |
code | 4位商品代码 |
nom | 名称(字符串) |
prix | 其价格(实际) |
stock_actu | 当前库存(整数) |
stock_mini | 最低库存(整数),低于该数值时需补货 |
我们计划通过以下表单来查看和更新该表:

该表单的控件如下:
编号 | 类型 | 名称 | 功能 |
1 | 文本框 | 表单 | 当前显示的记录编号 enabled 设置为 false |
2 | 文本框 | 代码 | 商品代码 |
3 | 文本框 | 名称 | 商品名称 |
4 | 文本框 | 价格 | 商品价格 |
5 | 文本框 | 当前 | 商品当前库存 |
6 | 文本框 | 最低 | 该商品的最低库存 |
7 | data | data1 | 与数据库关联的数据控件 databasename=articles.mdb 文件路径 recordsource=articles connect=access |
8 | HScrollBar | position | 用于在表格中导航 |
9 | 按钮 | OK | 用于确认更新 - 仅在更新时显示 |
10 | 按钮 | 取消 | 用于取消更新——仅在更新过程中显示 |
11 | frame | frame1 | 仅用于美观 |
12 | 文本框 | 基名 | 打开的数据库名称 enabled 设为 false |
13 | 文本框 | sourcename | 打开的表名 enabled 设置为 false |
控件 | 特殊说明 |
data1 | databasename 和 recordsource 字段已填写。databasename 必须指定您目录中的 Access 数据库 articles.mdb,而 recordsource 必须指定 articles 表。 |
code | 这是一个文本框,我们希望将其与 data1 当前记录的 code 字段建立关联。为此,需填写两个字段: datasource:输入 data1,表示该文本框与 data1 关联的表相关联 数据字段:选择“产品”表中的“代码”字段 完成上述操作后,代码文本框将始终显示 data1 当前记录的“代码”字段。反之,修改该文本框的内容将更新当前记录的“代码”字段。 其他文本框也按此方式设置 |
nom | 数据源:data1 数据字段:nom |
prix | 数据源:data1 数据字段:价格 |
actuel | 数据源:data1 数据字段:stock_actu |
minimum | 数据源:data1 数据字段:stock_mini |
菜单结构如下
编辑 | 浏览 | 退出 |
添加 | 上一页 | |
编辑 | 下一页 | |
删除 | 首页 | |
最后 |
各项选项的作用如下:
菜单 | 名称 | 功能 |
mnuajouter | 用于向商品表添加新记录 | |
mnusupprimer | 用于从商品表中删除当前显示的记录 | |
mnumodifier | 用于在物料表中修改当前显示的记录 | |
mnuprecedent | 用于切换到上一条记录 | |
mnusuivant | 跳转到下一条记录 | |
mnupremier | 跳转到第一条记录 | |
mnudernier | 跳转到最后一条记录 | |
mnuquitter | 退出应用程序 |
在事件 form_load 发生时,将打开与 data1 关联的表(data1.refresh)。 如果打开失败,将显示一条错误消息并退出程序(end)。否则,将显示表单并显示表 articles 的第一条记录。 字段中无法进行任何输入(enabled属性设为false)。仅通过“添加”和“修改”选项才可进行输入。按钮OK和“取消”被隐藏(visible=false)。
我们将构建与菜单中各项选项以及按钮 OK 和“取消”相关的过程。首先,我们将忽略以下几点:
- 菜单中某些选项的启用/禁用:例如,当光标位于表的最后一条记录时,应禁用“下一步”选项
- 水平滚动条的管理
“浏览/下一页”菜单
- 若未到达文件末尾(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)
- 显示 OK 和 取消 按钮(visible=true)
OK 按钮
- 确认记录修改(data1.recordset.Update)
- 隐藏 OK 和“取消”按钮(visible=false)
- 启用“编辑”、“浏览”、“退出”选项(enabled=true)
- 更新表单文本框
“取消”按钮
- 取消记录修改(data1.recordset.CancelUpdate)
- 隐藏按钮 OK 和 取消(visible=false)
- 启用“编辑”、“浏览”和“退出”选项(enabled=true)
- 更新表单文本框
“编辑”/“修改”菜单
- 允许修改表单上显示的记录
- 进入记录编辑模式(data1.recordset.edit)
- 允许在“名称”、“价格”等 4 个字段中输入数据(enabled=true),但不允许在“代码”字段中输入(enabled=false)
- 禁用“编辑”、“浏览”和“退出”菜单(enabled=false)
- 显示“OK”和“取消”按钮(visible=true)
“编辑”/“删除”菜单
- 允许删除(data1.recordset.delete)当前显示的表记录
- 转到下一条记录(data1.recordset.movenext)
“退出”菜单
- 卸载工作表(unload me)
事件 form_unload (cancel as integer)
- 由“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_change。 在此事件中,我们将修改表格的当前记录,使其反映可变器上的移动操作。为此,我们将使用 data1.recordset 中的 absoluteposition 字段。当向该字段赋值 i 时,表格中的第 i 号记录将成为当前记录。 记录编号从 0 开始,因此编号范围在 [0,data1.recordset.recordcount-1] 之间。在 position_change 过程 中,我们只需编写
即可使表单上显示的当前数据卡反映变频器上的实际调整。
完成上述操作后,我们将调用 Oueston 过程来更新菜单。
变频器更新
由于变频器光标的位置必须反映表格中的位置,因此每当表格中的当前记录因某个菜单操作而发生变化时,都必须更新变频器的数值。由于每个菜单都会调用 Oueston 过程,因此最好将此更新操作也放在该过程中。只需在此处编写:
我们添加了“浏览/搜索”选项,旨在允许用户通过输入商品代码来查看该商品。
当此选项被激活时,将执行以下步骤:
- 进入“添加记录”(Addnew)模式,此举仅为避免修改选项启用时当前所在的记录;
- 允许在代码字段中输入内容,并清空记录字段的内容,
- 禁用菜单,并显示按钮 OK 和 取消
- 当用户点击 OK 时,我们需要根据用户输入的代码查找对应的记录。然而,OK_click 过程已用于“浏览/添加”和“浏览/修改”选项。 为区分这些情况,我们需要管理一个全局变量,此处命名为状态,该变量有三个可能的值:“添加”、“修改”和“查找”。与按钮 OK 和“取消”相关的过程将使用该变量来判断它们是在何种上下文中被调用的。
- 如果状态为“搜索”,在与 OK 相关的过程内,
- 将取消 addnew 操作(data1.recordset.cancelupdate),因为本意并非要添加记录。需要注意的是,此时当前记录将恢复为执行“浏览/查找”操作前显示在屏幕上的那条。
- 构建与代码相关的搜索条件并启动搜索(data1.recordset.findfirst 条件),
- 若搜索失败(data1.recordset.nomatch=true),则向用户提示,随后恢复为添加模式(addnew)并退出OK过程。用户需重新输入新代码或选择“取消”选项。
- 若搜索成功,找到的记录将成为新的当前记录。恢复菜单显示,隐藏OK/取消按钮,禁用代码字段的输入,并退出该过程。
- . 如果状态为“搜索”,则在与“取消”相关的程序中,
- 将取消 addnew 操作(data1.recordset.cancelupdate)。随后系统将自动返回“浏览/搜索”操作之前的当前记录。
- 恢复菜单,隐藏按钮 OK/取消,禁用代码字段的输入,并退出该过程。
每件商品必须通过其代码进行唯一标识。请确保在“浏览/添加”选项中,如果要添加的记录的商品代码在表中已存在,则拒绝添加。
6.4.4. 练习 4
这里介绍一个基于练习2中服务器的Web应用程序。这是一个电子商务应用程序的雏形。
客户可通过以下 Web 界面订购商品:

客户可以执行以下操作:
- 从下拉列表中选择商品
- 指定所需数量
- 点击按钮 Acheter 确认购买
- 购买的商品将显示在已购商品列表中
- 他可以通过选中商品并点击按钮 Retirer 从该列表中移除商品
- 当他点击按钮 Bilan 时,将显示以下总结:

对账单可让用户查看账单详情。用户可通过按钮 Informations 访问下拉列表中选定商品的详细信息:

当用户请求查看账单明细后,可在下一页进行确认。为此,用户需提供电子邮箱地址,并通过相应按钮确认订单。

在保存订单前,应用程序会要求用户确认:

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

实际上,应用程序并未处理该订单。它只是向用户发送一封电子邮件,要求其支付购物金额:
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 应用程序功能相当的程序。

