Skip to content

6. 使用 JDBC API 进行数据库管理

6.1. 概述

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

Image

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

Image

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

Image

6.2. 数据库操作的关键步骤

6.2.1. 简介

在使用 JDBC 接口访问数据库的 Java 应用程序中,通常涉及以下步骤:

  1. 连接数据库
  2. 向数据库发送 SQL 查询
  3. 接收并处理这些查询的结果
  4. 关闭连接

步骤 2 和 3 会反复执行,只有在数据库操作结束时才会关闭连接。这是一个相对标准的模式,如果您曾交互式地使用过数据库,可能对此已经很熟悉。我们将通过一个示例详细说明每个步骤。我们将考虑一个名为 ARTICLES 的 Access 数据库,其结构如下:

name
类型
type
4位项目代码
名称
其名称(字符串)
price
其价格(实际)
当前库存
当前库存(整数)
min_stock
最低库存(整数),低于该数值时必须补货

此 ACCESS 数据库在 ODBC 数据库管理器中被定义为“用户”数据源:

Image

Image

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

Image

此配置主要涉及将 ARTICLES 数据库与对应于该数据库的 Access 文件 articles.mdb 建立关联。完成此操作后,应用程序即可通过 ODBC 接口访问 ARTICLES 数据库。

6.2.2. 连接步骤

要使用数据库,Java 应用程序必须首先建立连接。这可通过以下类方法实现:

Connection DriverManager.getConnection(String URL, String id, String mdp)

其中

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.forName(String nomClasse)

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.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 requete=connexion.CreateStatement();

获取 Statement 对象后,即可执行 SQL 查询。具体执行方式取决于查询是用于检索数据还是更新数据库。

6.2.3.2. 执行从数据库检索数据的查询

查询通常采用以下形式:

    select col1, col2,... from table1, table2,...
    where condition
    order by expression
    ...

仅第一行中的关键字是必需的;其余均为可选。还有其他未在此处显示的关键字。

  1. 将对 `FROM` 关键字后列出的所有表执行连接
  2. 仅保留 `select` 关键字后面的列
  3. 仅保留满足 `where` 关键字条件的行
  4. 根据 `ORDER BY` 关键字中的表达式对所得行进行排序,即构成查询结果。

SELECT 的结果是一个表。如果我们考虑前面的 ARTICLES 表,并希望获取当前库存低于最低阈值的商品名称,我们会写:SELECT name FROM articles WHERE current\_stock &lt; min\_stock*。如果希望按名称字母顺序排序,我们会写: select name from articles where current_stock < min_stock order by name*

要执行此类查询,Statement 类提供了 executeQuery 方法:

    ResultSet executeQuery(String requête)

其中 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 方法:

    boolean next()

该方法尝试跳转到 ResultSet 中的下一行,若成功则返回 true,否则返回 false。若成功,下一行将成为新的当前行。上一行将被丢弃且无法检索。ResultSet 表包含列 col1、col2、... 等。要访问当前行的各个字段,可以使用以下方法:

Type getType("coli") 

来获取当前行中的“coli”字段。Type 指代 coli 字段的类型。getString 方法常用于所有字段,它允许您将字段内容作为字符串获取。随后可根据需要进行转换。若您不知道列名,可以使用以下方法

Type getType(i) 

,其中 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

    RS.getMetaData()

ResultSetMetaData 类中有两个有用的方法:

  1. int getColumnCount(),该方法返回 ResultSet 中的列数
  2. 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 方法:

    int executeUpdate(String requête)

两者的区别在于返回结果: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 查询:

    boolean execute(String requête)

如果查询返回了一个 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. 第四个示例

我们借鉴程序 sql1sql2 的概念,将其应用到程序 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

程序运行如下:

E:\data\java\jdbc\0>java sql3 sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles <requetes >results

该程序从 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 é 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 é 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 é 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

该应用程序的图形界面如下:

Image

图形界面已进行了一些更改:

否。
类型
名称
角色
1
JTextField
txtConnection
ODBC 数据库连接字符串
2
JScrollPane
JScrollPane1
Textarea 的容器3
3
JTextArea
txtStatus
显示状态消息,包括错误消息

在 (1) 中输入的连接字符串采用以下格式:DSN;登录名;密码,其中

DSN
表示 ODBC 数据源的 DSN 名称
登录名
具有数据库读取权限的用户身份
密码
其密码

dbimpots 数据库是使用 MySQL 手动创建的。将其转换为 ODBC 数据源的步骤如下:

  • 启动 32 位 ODBC 数据源管理器

Image

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

Image

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

Image

  • 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

该类通过构造函数接收的三个参数数组,构建出 limitscoeffRcoeffN 这三个数组。我们决定添加一个新的构造函数,以便能够从数据库中构建这三个数组:

  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 表的内容,并填充三个数组:limitescoeffRcoeffN。可能会发生多种错误。构造函数不会处理这些错误,而是将它们“上报”给调用程序:

  public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
      throws SQLException,ClassNotFoundException{

如果仔细观察前面的代码,我们会发现 impotsJDBC 类直接使用了其基类 impotslimitscoeffRcoeffN 字段。由于这些字段被声明为 private:

    private double[] limites, coeffR, coeffN;

因此,*importsJDBC* 类无法直接访问这些字段。为此,我们对基类进行初步修改,写入以下代码:

  protected double[] limites=null;
  protected double[] coeffR=null;
  protected double[] coeffN=null;

protected 属性允许从 impots 类派生的类直接访问带有此属性的字段。我们需要进行第二次修改。子类 impotsJDBC 的构造函数声明如下:

  public impotsJDBC(String dsnIMPOTS, String userIMPOTS, String mdpIMPOTS)
      throws SQLException,ClassNotFoundException{

我们知道,在构造子类的对象之前,必须先构造父类的对象。为此,子类的构造函数必须使用 super(....) 语句显式调用父类的构造函数。这里未这样做,因为我们无法确定应调用父类的哪个构造函数。 目前只有一个构造函数,但它并不适用。编译器随后会在父类中搜索可调用的无参构造函数。由于未找到,因此会引发编译错误。因此,我们在 impots 类中添加了一个无参构造函数:

  // constructeur vide
  protected 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 小程序只能通过加载它的服务器访问数据库。由于小程序无法访问其运行机器的磁盘,因此数据库不能位于使用该小程序的客户端机器上。因此,我们面临以下情况:

Image

运行 Applet 的机器、服务器以及托管数据库的机器可能是三台不同的机器。在此,我们假设数据库位于服务器上。

问题 1

用 Java 编写以下服务器应用程序:

  • 该服务器应用程序在作为参数传递给它的端口上运行
  • 当客户端连接时,服务器应用程序发送消息
200 - Bienvenue - Envoyez votre requête
  • 随后,客户端将以以下格式发送连接数据库所需的参数以及要执行的查询:
Pilote Java/URL base/UID/MDP/requête SQL

参数之间用斜杠分隔。前四个参数是本章所述的 sql3 程序的参数。

  • 随后,服务器应用程序会与指定的数据库建立连接(该数据库必须位于与服务器相同的机器上),并在其上执行 SQL 查询。结果将以以下格式返回给客户端:
100 - ligne1
100 - ligne2

...

如果是 Select 查询的结果,或者

101 - nbLignes

以返回受更新查询影响的行数。如果发生数据库连接错误或查询执行错误,应用程序将返回

500 - Message d’erreur
  • 查询执行完成后,服务器应用程序将关闭连接。

问题 2

编写一个 Java 小程序来查询上述服务器。你可以参考练习 1 中的图形界面。由于服务器端口可能不同,因此需通过小程序的界面输入。发送行所需的所有参数也需通过此方式输入:

Pilote Java/URL base/UID/MDP/requête SQL

客户端必须发送给服务器的。

6.4.3. 练习 3

下文描述了一个原本打算用 Visual Basic 解决的问题。请将其改编为可在 Java 小程序中处理的形式。该小程序将依赖于练习 2 中的服务器。图形界面可根据新的执行环境进行修改。

我们建议创建一个应用程序,用于演示对 ACCESS 数据库中某张表可能进行的各种更新操作。该 ACCESS 数据库名为 articles.mdb。其中包含一张名为 articles 的表,列出了某公司销售的商品。其结构如下:

name
类型
类型
4位商品代码
名称
其名称(字符串)
price
其价格(实际)
当前库存
当前库存(整数)
min_stock
最低库存(整数),低于该数值时必须补货

我们建议使用以下表单查看和更新此表格:

Image

此表单上的控件如下:

编号
类型
名称
功能
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

在 VB 中创建工作表

控件
特殊功能
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)。


第 1 部分

我们建议先构建与各项菜单选项以及“确定”和“取消”按钮相关的过程。目前,我们将忽略以下几点:

  • 启用/禁用某些菜单选项:例如,如果光标位于表中的最后一条记录上,则“下一条”选项必须被禁用
  • 管理水平滚动条

“浏览/下一条”菜单

  • 如果尚未到达文件末尾(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)。

第 2 部分 - 菜单管理

在此,我们将重点关注菜单的启用/禁用。在每次更改当前记录的操作之后,我们将调用一个名为 Oueston 的过程。该过程将检查以下条件:

. 如果文件为空,我们将

  • 禁用“浏览”、“编辑/删除”菜单,
  • 并启用其余菜单

。 如果当前记录是第一条记录,则

  • 禁用“浏览/上一条”
  • 将允许其余部分

。 如果当前记录是最后一条,

  • 禁用“浏览/下一条”
  • 将允许其余操作

第 3 部分 - 管理水平滑块

水平滑块有三个重要字段:

  • 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 过程 中,我们只需编写

    data1.recordset.absoluteposition=position.value

这样,表单上显示的当前记录就能反映摇杆的操作。

完成此操作后,我们将调用 Oueston 过程来更新菜单。

更新 DCS

由于驱动程序光标的位置必须反映表中的位置,因此每当表中的当前记录发生变化(由某个菜单触发)时,都必须更新驱动程序的值。由于每次调用都会调用 Oueston 过程,因此最好将此更新也放在该过程内。只需在此处编写:

    position.value=data1.recordset.absoluteposition

第 4 部分 - 搜索选项

我们添加了“浏览/搜索”选项,允许用户通过输入代码来查看商品。

启用此选项时,将执行以下步骤:

  • 系统将切换至“新增”模式(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)。系统随后将自动返回至“浏览/搜索”操作之前的活动记录。
    • 恢复菜单,隐藏“确定/取消”按钮,禁用代码字段的输入,并退出该过程。

第 5 部分 - 代码管理

项目必须通过其代码进行唯一标识。请确保在“浏览/添加”选项中,如果待添加记录的项目代码在表中已存在,则拒绝添加。

6.4.4. 练习 4

在此,我们将展示一个基于练习 2 中服务器构建的 Web 应用程序。这是一个基本的电子商务应用程序。

客户通过以下 Web 界面订购商品:

Image

用户可以执行以下操作:

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

Image

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

Image

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

Image

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

Image

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

Image

实际上,该应用并未处理订单。它只是向用户发送一封电子邮件,要求用户支付所购商品的款项:

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 应用程序功能相当的程序。