Skip to content

6. [课程]:JDBC API 入门

关键词:关系型数据库、JDBC API、SQLException。

6.1. 支持

文件夹 [support / chap-06] 包含本章的 Eclipse 项目。

6.2. 架构

JDBC(Java数据库连接)层是一个通用的数据库访问接口。它始终向[DAO]层提供相同的接口。如果更换数据库管理系统(DBMS),只需更换JDBC驱动程序即可。[DAO]层保持不变。

6.3. 操作数据库的步骤

在上述架构中,通过控制台程序操作数据库包括以下步骤:

  1. 加载数据库的 JDBC 驱动程序;
  2. 建立与数据库的连接;
  3. 在数据库上执行 SQL 语句并处理 SQL 语句的结果;
  4. 关闭连接;

步骤 1 仅执行一次。步骤 2–4 会反复执行。请注意,连接不会保持打开状态;一旦不再需要,它们就会立即关闭。

6.3.1. 步骤 1 - 将 JDBC 驱动程序加载到内存中

代码


        // driver loading JDBC
        try {
            Class.forName(nom de la classe du pilote JDBC);
        } catch (ClassNotFoundException e1) {
             // handle the exception
}

第 3 行操作的目的是将数据库的 JDBC 驱动程序加载到内存中。此操作只需执行一次。不过,重复执行也不会导致错误。系统会在项目的类路径中搜索 JDBC 驱动程序类。因此,在 Eclipse 项目中,包含 JDBC 驱动程序类的 [jar] 文件必须已添加到项目的类路径中。

6.3.2. 步骤 2 - 建立连接

一旦 JDBC 驱动程序就位,我们就指示它打开与数据库的连接:

代码


package spring.jdbc;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
 
public class IntroJdbc01 {
 
...
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
...
        } catch (SQLException e1) {
            // we handle the exception
            ...
        } finally {
         // close connection
         if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e2) {
                // handle the exception
                ...
            }
         }
}
  • 第 3–7 行:实现 JDBC 接口的类均位于 [java.sql] 包中。 此外,一旦发生错误,它们都会抛出 [SQLException](第 19、27 行)。该异常继承自 [Exception] 类,属于所谓的“受检查异常”:必须使用 try/catch 代码块进行处理;或者,也可以选择不进行处理,并在方法签名中添加 [throws SQLException] 来表明该方法允许异常向上传播;
  • 第 17 行:[DriverManager.getConnection] 是一个静态方法,接受三个参数:
    • [url]:数据库 URL。这是一个字符串,具体取决于所使用的数据库。对于 MySQL,其格式为 [jdbc:mysql://localhost:3306/db_name];
    • [user]:连接所有者;
    • [passwd]:用户的密码;
  • 第 24–30 行:必须在 [finally] 子句中关闭连接,以确保无论是否发生异常,连接都会被关闭。

6.3.3. 步骤 3 - 执行 SQL [SELECT] 语句

建立连接后,即可执行 SQL 命令。读取命令 [SELECT] 的处理方式与更新操作 [UPDATE、INSERT、DELETE] 不同。我们将从 [SELECT] SQL 命令开始讲解:

代码


Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read-only mode
            connexion.setReadOnly(true);
            // table [PRODUITS] is read
            ps = connexion.prepareStatement("SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS");
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                System.out.println(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
             doCatchException(connexion,e1);
        } finally {
            // we treat the finally
            doFinally(rs, ps, connexion);
        }
 
    private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
....
}
  • 第 8、10 行:以只读模式(第 10 行)打开事务(第 8 行)。 事务是一系列SQL语句,要么全部成功,要么全部失败。因此,在一个包含N条SQL语句的事务中,如果第(I+1)条语句失败,则之前的I条语句将被回滚。对于读取操作,事务并非必需。但是,创建只读事务可以使某些数据库管理系统(DBMS)进行特定的优化;
  • 第 12 行:使用 [PreparedStatement]。通常 [PreparedStatement] 的参数用字符 ? 表示,但此处并非如此。 [PreparedStatement]是由DBMS预编译的语句。该预编译过程会产生开销,且仅执行一次。随后,DBMS会使用实际参数替换形式参数 ? 来执行该预编译语句。请注意,建议明确指定所需列,而非使用 * 符号来检索所有列。通过指定列名,可以基于它们在 SELECT 语句中的位置检索其值;
  • 第 13 行:执行 [PreparedStatement]。获取一个 [ResultSet] 对象;

[ResultSet] 对象代表一张表,即一组行和列。在任何给定时刻,我们只能访问表中的一行,称为当前行。当 [ResultSet] 最初创建时,尚无当前行。我们必须执行 [ResultSet.next()] 操作才能获取它。next 方法的签名如下:

    boolean next()

该方法尝试跳转到 [ResultSet] 的下一行,若成功则返回 true,否则返回 false。若成功,下一行将成为新的当前行。上一行将被丢弃且无法恢复。

[ResultSet] 表包含名为 labelCol1、labelCol2、... 的列,这些列名由已执行的 [SELECT] 查询指定。查询语句如下:

SELECT ID as myId, NOM as myNom, CATEGORIE as myCategorie, PRIX as myPrix, DESCRIPTION as myDescription FROM PRODUITS
  • [ID] 列将被映射到 [ResultSet] 中名为 [myId] 的列;
  • [NAME] 列将存入 [ResultSet] 中名为 [myName] 的列;
  • ...

在上例中,标识符 [myCol] 被称为列标签。如果没有这些标签,[ResultSet] 列的名称将取决于数据库管理系统(DBMS)。当 [SELECT] 操作针对单个表时,列标签默认即为 SELECT 语句所请求的列名。问题出现在 [SELECT] 操作涉及多个表且这些表包含相同列名时,如下例所示:

SELECT PRODUITS.NOM, CATEGORIES.NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID

假设 [PRODUCTS] 表与 [CATEGORIES] 表之间存在外键关系,表示为 [PRODUCTS].CATEGORY_ID --> [CATEGORIES].ID,且 [PRODUCTS] 和 [CATEGORIES] 表均包含 [NAME] 字段。 在这种情况下,[ResultSet] 中为 [PRODUCTS].NAME 和 [CATEGORIES].NAME 列指定的名称取决于数据库管理系统(DBMS)。为了实现跨 DBMS 的可移植性,此处必须使用列标签,因此我们将写为:


SELECT PRODUITS.NOM as p_NOM, CATEGORIES.NOM as c_NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID

要访问 [ResultSet] 中当前行的各个字段,可使用以下方法:

Type getType("labelColi") 

用于从当前行中检索名为“labelColi”的列,即 [SELECT] 语句中带有该标签的列。 Type 指“labelColi”字段的数据类型。可使用以下 [getType] 方法:getInt、getLong、getString、getDouble、getFloat、getDate 等。除了使用列名,您还可以使用该列在已执行的 [SELECT] 查询中的位置:

Type getType(i) 

其中 i 是目标列的索引(i>=1)。

  • 第 15–17 行:从数据库中读取值;
  • 第 19 行:事务被提交(也称为提交)。这将终止事务并释放 DBMS 为其分配的资源;
  • 第 25 行:在 [finally] 代码块中释放资源。这会调用后续的 [doFinally] 方法:

private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // closure ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {
 
            }
        }
        // closure [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {
 
            }
        }
        if (connexion != null) {
            try {
                // close connection
                connexion.close();
            } catch (SQLException e3) {
                 // handle the exception
            }
        }
    }
  • 第 3–9 行:关闭 [ResultSet];
  • 第 11–17 行:关闭 [PreparedStatement];
  • 第 18–27 行:关闭连接;

第 3–17 行的关闭语句似乎是多余的,因为连接已在第 18–25 行关闭。事实上,在某些情况下它们并非多余,建议保留这些语句 [http://stackoverflow.com/questions/4507440/must-jdbc-resultsets-and-statements-be-closed-separately-although-the-connection]。

  • 第 22 行:该异常由以下 [doCatchException] 方法处理:

    private static void doCatchException(Connection connexion, Throwable th) {
        // cancel transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // handle the exception
        }
}
  • 第 4–6 行:事务被回滚。这将终止事务,数据库管理系统 (DBMS) 即可释放分配给该事务的资源;

6.3.4. 步骤 3 - 执行 SQL 语句 [INSERT, UPDATE, DELETE]

SQL 语句 [INSERT、UPDATE、DELETE] 属于更新操作:它们会修改数据库,但不会返回任何行。唯一返回的信息是受更新操作影响的行数。

代码


Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // ouverture connexion
            connexion = DriverManager.getConnection(url, user, passwd);
            // début transaction
            connexion.setAutoCommit(false);
            // en mode lecture / écriture
            connexion.setReadOnly(false);
            // on met à jour la table
            ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
            // catégorie 1
            ps.setInt(1, 10);
            // exécution
            int nbLignes=ps.executeUpdate();
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // on traite l'exception
            doCatchException(connexion, e1);
        } finally {
            // on traite le finally
            doFinally(null, ps, connexion);
        }
    }
  • 第 9 行:该连接用于读写操作;
  • 第 11 行:一个包含 1 个参数(用 ? 表示)的 [PreparedStatement]。参数可以有多个,编号从 1 开始;
  • 第 13 行:将其值赋给该参数。[setType] 的第一个参数是该参数在 [PreparedStatement] 中的位置(1、2、...),第二个参数是赋给它的值。您可以使用 [setInt、setLong、setFloat、setDouble、setString、setDate、...] 等方法;
  • 第 15 行:使用 [executeUpdate] 方法,而非 [executeQuery](后者专用于 SELECT 语句)。该方法返回受操作影响的行数,可能为 0。
  • 第 17 行:提交事务;

6.3.5. 步骤 4 - 关闭连接

在多用户环境中,必须尽快关闭连接,因为数据库管理系统(DBMS)接受的打开连接数量是有限的。在之前的示例中,连接是在 SQL 操作的 [finally] 子句中关闭的,这样无论是否发生异常,连接都会被关闭。

6.4. 示例项目

6.4.1. 支持

[support / chap5] 文件夹包含本章的 Eclipse 项目 [1, 2]。[database] 文件夹包含用于创建本章示例 MySQL 数据库的 SQL 脚本 [1, 3]。

6.4.2. 所使用的数据库

以下示例使用以下 MySQL 数据库:

 
  • [ID]:处于 AUTO_INCREMENT 模式的主键(如果未指定主键,DBMS 会自动生成);
  • [NAME]: 产品名称——唯一;
  • [CATEGORY]: 类别编号;
  • [PRICE]:其价格;
  • [DESCRIPTION]: 产品描述;

我们将使用 [WampServer] 工具按以下步骤创建该表 [1-9]:

6.4.3. Eclipse 项目

  

该项目是一个由以下 [pom.xml] 文件定义的 Maven 项目:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.jdbc</groupId>
    <artifactId>intro-jdbc-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.34</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.5.1</version>
        </dependency>
    </dependencies>
</project>
  • 第 8–12 行:MySQL5 数据库管理系统(DBMS)的 JDBC 驱动程序;
  • 第 13–17 行:一个能够处理 JSON(JavaScript 对象表示法)的库(参见第 22.6 节)。我们将使用它以 JSON 格式显示数据库中的产品;

6.4.4. Product 类

[Product] 类如下所示:


package istia.st.jdbc;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class Produit {
 
    // fields
    private int id;
    private String nom;
    private int categorie;
    private double prix;
    private String description;
 
    // manufacturers
    public Produit() {
 
    }
 
    public Produit(int id, String nom, int categorie, double prix, String description) {
        this.id = id;
        this.nom = nom;
        this.categorie = categorie;
        this.prix = prix;
        this.description = description;
    }
 
    // getters and setters
    ...
 
    // to String
    public String toString() {
        try {
            return new ObjectMapper().writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
}
  • 第 34 行:我们使用 JSON 库来显示产品的 JSON 字符串。这将生成类似于以下内容的输出:
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}

上述 [toString] 方法的优势在于,即使向类中添加或移除了字段,其 [toString] 方法仍然有效。此外,如果字段本身是对象(列表、数组、字典、用户对象),JSON 库可以进一步将其转换为 JSON 字符串;

6.4.5. [Static] 类

[Static] 类将主类中经常使用的方法集中在一起:


package istia.st.jdbc;
 
import java.util.ArrayList;
import java.util.List;
 
public class Static {
 
    public static List<String> getErreursFromThrowable(Throwable th) {
        // retrieve the list of exception error msgs
        List<String> erreurs = new ArrayList<String>();
        while (th != null) {
            // throwable error message
            erreurs.add(th.getMessage());
            // we move on to the cause of throwable
            th = th.getCause();
        }
        // result
        return erreurs;
    }
 
    public static void show(String title, List<String> messages){
        // title
        System.out.println(String.format("%s : ",title));
        // messages
        for(String message : messages){
            System.out.println(String.format("- %s",message));
        }
    }
}
  • 第 8–19 行:返回一个包含错误的列表,这些错误封装在一个类型为 [Throwable] 的对象中,该类型是 [Exception] 类的超类;
  • 第 21–28 行:在屏幕上显示一条消息列表;

这段代码本可以放在主类中,因为这里只有它在使用。不过,我们正在考虑更广泛的情况,即其他类可能也需要这段代码。

6.4.6. 主类的骨架


package istia.st.jdbc;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
 
public class IntroJdbc01 {
 
    // constants
    final static String url = "jdbc:mysql://localhost:3306/dbIntroJdbc";
    final static String user = "root";
    final static String passwd = "";
    final static String insert = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
    final static String delete = "DELETE FROM PRODUITS";
    final static String select = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    final static String update = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
    final static String insert2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";
 
    public static void main(String[] args) {
        // loading the JDBC driver from MySQL
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // empty table [PRODUITS]
        delete();
        // fill it
        insert();
        // we read it
        select();
        // update
        update();
        // display
        select();
        // insertion of two identical elements
        // insertion must fail and neither element is inserted because of the transaction
        insert2();
        // we check
        select();
        // finish
        System.out.println("Travail terminé");
    }
 
    // product list
    private static void select() {
...
    }
 
    // product deletion
    public static void delete() {
...
    }
 
    // add products
    public static void insert() {
...
    }
 
    // add 2 products
    public static void insert2() {
...
    }
 
    // product updates
    public static void update() {
..
    }
 
    private static void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // closure ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {
 
            }
        }
        // closure [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {
 
            }
        }
        // close connection
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // display error msg
                Static.show("Les erreurs suivantes se sont produites lors de la fermeture de la connexion",
                        Static.getErreursFromThrowable(e3));
            }
        }
    }
 
    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // display error msg
        Static.show(title, Static.getErreursFromThrowable(th));
        // cancel transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // display error msg
            Static.show(title, Static.getErreursFromThrowable(e2));
        }
    }
}

6.4.7. 删除 products 表中的内容

[delete] 方法用于删除表中的内容:


// product deletion
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // empty table [PRODUITS]
            ps = connexion.prepareStatement(delete);
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, null, connexion);
        }
    }

此示例使用了事务。事务允许您将一组 SQL 语句进行打包,这些语句要么全部成功,要么全部回滚。需要注意以下四种操作:

  • 开始事务:[connection.setAutoCommit(false)];
  • 成功结束事务:[connection.commit()]。此时,事务期间在数据库上执行的所有操作均被提交;
  • 事务失败结束:[connection.rollback()]。此时,事务期间在数据库上执行的所有操作都会被回滚;
  • 恢复到 [auto-commit] 模式(即 JDBC API 的默认模式):[connection.setAutoCommit(true)]。在此模式下,每个 SQL 语句都属于一个事务。因此,如果您执行两次插入操作,而第二次失败:
    • 在 [AutoCommit=true] 模式下,第一个插入操作将保留(它已被第一次自动提交所提交);
    • 在 [AutoCommit=false] 模式下,第一个插入操作会被回滚;

在我们的示例中,每当发生异常时,我们都会在 [doCatchException] 方法中回滚事务:


    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // display error msg
        Static.show(title, Static.getErreursFromThrowable(th));
        // cancel transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // display error msg
            Static.show("Erreur lors de l'annulation de la transaction", Static.getErreursFromThrowable(e2));
        }
}

6.4.8. 创建 products 表的内容

[insert] 方法用于创建表中的数据:


// add products
    public static void insert() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // fill the table
            ps = connexion.prepareStatement(insert);
            for (int i = 0; i < 10; i++) {
                // preparation
                int n = i + 1;
                ps.setInt(1, n);
                ps.setString(2, String.format("NOM%s", n));
                ps.setInt(3, n / 5 + 1);
                ps.setDouble(4, 100 * (1 + (double) i / 100));
                ps.setString(5, String.format("DESC%s", n));
                // execution
                ps.executeUpdate();
            }
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, null, connexion);
        }
    }

6.4.9. 显示 products 表的内容

[select] 方法用于显示表的内容:


    // product list
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // table [PRODUITS] is read
            ps = connexion.prepareStatement(select);
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                System.out.println(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, null, connexion);
        }
}

6.4.10. 更新表内容

[update] 方法用于更新特定产品:


// product updates
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // table is updated
            ps = connexion.prepareStatement(update);
            // category 1
            ps.setInt(1, 1);
            // execution
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, null, connexion);
        }
    }

6.4.11. 事务的作用

[insert2] 方法试图将两个主键相同的产品插入到表中,这是不被允许的。由于我们处于事务中,因此第一次插入操作将被回滚。


// add 2 pro ducts with the same primary keys
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // add 1 line
            ps = connexion.prepareStatement(insert2);
            // execution
            ps.executeUpdate();
            // we add the same line a 2nd time, with the same primary key
            // the insertion must fail and neither of the two elements must be inserted because of the transaction
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, null, connexion);
        }
    }

6.4.12. 结果

执行 [main] 方法的结果如下:

Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":110.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
Les erreurs suivantes se sont produites lors de l'ajout : 
- Duplicate entry '100' for key 'PRIMARY'
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":110.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
Travail terminé

6.5. 使用 [DataSource] 数据源

我们将使用 [javax.sql.DataSource] 数据源重新审视之前的应用程序:

Image

我们将使用由 [org.apache.tomcat.jdbc.pool.DataSource] 类实现的数据源。该类使用连接池,即一组已打开的连接:

  • 当连接池被实例化时,会向数据库打开一定数量的连接。该数量可通过配置进行调整;
  • 当 Java 代码打开连接时,该连接由连接池提供;
  • 当 Java 代码关闭连接时,该连接将返回给连接池;

最终,连接仅需建立一次,从而提升了数据库访问性能。该数据源将在一个 Spring 配置类中进行定义

6.5.1. Eclipse 项目

  

该项目是一个由以下 [pom.xml] 文件定义的 Maven 项目:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.jdbc</groupId>
    <artifactId>intro-jdbc-02</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.34</version>
        </dependency>
        <!-- library jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.5.1</version>
        </dependency>
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.1.3.RELEASE</version>
        </dependency>
        <!-- Tomcat Jdbc -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
            <version>8.0.20</version>
        </dependency>
    </dependencies>
</project>
  • 第 21–25 行:对 Spring 的依赖;
  • 第 27–31 行:对提供数据源的库的依赖;

Spring 配置类 [AppConfig] 如下所示:


package istia.st.jdbc;
 
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class AppConfig {
 
    // constantes
    final static String URL = "jdbc:mysql://localhost:3306/dbIntroJdbc";
    final static String USER = "root";
    final static String PASSWD = "";
    final static String INSERT = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
    final static String DELETE = "DELETE FROM PRODUITS";
    final static String SELECT = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    final static String UPDATE = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
    final static String INSERT2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";
    final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
 
    @Bean
    public DataSource dataSource() {
        // source de données TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration accès JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // une connexion ouverte initialement
        dataSource.setInitialSize(1);
        // résultat
        return dataSource;
    }
}
  • 第 11–19 行:之前在 [IntroJdbc01] 中定义的常量已移至 [AppConfig];
  • 第 31–34 行:定义数据源的 Spring Bean;
  • 第 24 行:创建数据源,该数据源尚未配置;
  • 第 26–29 行:允许数据源连接到数据库的信息;
  • 第 31 行:创建一个包含 1 个连接的连接池。此处无需更多连接,因为永远不会出现多个并发连接;

6.5.2. 主类

主类 [IntroJdbc02] 如下所示:


package istia.st.jdbc;
 
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
 
import javax.sql.DataSource;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
public class IntroJdbc02 {
    // data source
    private static DataSource dataSource;
 
    public static void main(String[] args) {
        // spring context retrieval
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        // data source recovery
        dataSource = ctx.getBean(DataSource.class);
        // empty table [PRODUITS]
        delete();
...
        // finish
        ctx.close();
        System.out.println("Travail terminé");
    }
 
    // product list
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // opening connection
            connexion = dataSource.getConnection();
            // start of transaction
            connexion.setAutoCommit(false);
...
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, null, connexion);
        }
    }
...
}
  • 第 14 行:数据源。请注意,其类型为 [javax.sql.DataSource],这是一个接口;
  • 第 18 行:Spring 对象的实例化;
  • 第 20 行:获取数据源的引用。请注意,实际使用的类从未被提及。因此,此处没有任何迹象表明正在使用 [TomcatJdbc] 的实现;
  • 第 36 行:获取已打开的连接;
  • 其余代码与 [IntroJdbc01] 类完全相同;

6.6. 结论

有关数据库管理的更多信息,请参阅文档 [使用 Spring 生态系统操作关系型数据库]。