Skip to content

3. Introduzione all'API JDBC

3.1. Configurazione dell'ambiente di lavoro

Lavoreremo con un database MySQL5.

È necessario disporre di:

Si presume che l'amministratore di MySQL5 sia root con password root. Avviare il DBMS MySQL5 e il suo client [MyManager]. Utilizzando [MyManager], creare il database [dbproduits] [1-34]:

  • in [3], il database deve essere denominato [dbproduits];
  • in [8-9], accedi come root utilizzando la password di root (che non è visibile nella schermata sopra);
  • in [14a], la password è di nuovo root (che lo screenshot non mostra);
  • in [15], il database [dbproduits] è stato creato;
  • in [20], prestare attenzione al database selezionato. Deve trattarsi del database [dbproduits];
  • in [22], la cartella è <examples>/spring-database-config/mysql/databases, dove <examples> è la cartella contenente gli esempi scaricati;
  • in [23], selezionare lo script SQL [dbproduits.sql]. Questo genererà la tabella [PRODUITS] nel database [dbproduits];
  • in [30], è stata creata la tabella [prodotti];
  • in [33], le colonne della tabella [prodotti];
  • in [34], inizialmente è vuota;

Ora, utilizzando STS, importa i seguenti progetti (segui la stessa procedura utilizzata per i progetti nella cartella <examples>/spring-core):

  • in [2], il progetto [mysql-config-jdbc] si trova nella cartella [<examples>/spring-database-config/mysql/eclipse/mysql-config-jdbc] [1];

Questo progetto configura il livello JDBC dell'architettura riportata di seguito:

Quindi importare nuovamente i seguenti tre progetti:

  • In [2], i progetti si trovano nella cartella [<examples>/spring-database-config/spring-jdbc] [1];

Questi tre progetti sono progetti Maven che utilizzano il progetto Maven [mysql-config-jdbc]. Questo progetto genera il seguente artefatto Maven (vedere pom.xml):


    <groupId>dvp.spring.database</groupId>
    <artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>

Lo stesso artefatto verrà generato dai progetti [oracle-config-jdbc, db2-config-jdbc, ...]. Per assicurarsi che i progetti [spring-generic-jdbc-*] attualmente caricati in STS stiano effettivamente utilizzando il progetto [mysql-config-jdbc]:

  • Assicurarsi che nessun altro progetto [sgbd-config-jdbc] sia caricato contemporaneamente. Ciò potrebbe causare errori difficili da comprendere;
  • Aggiornare la configurazione Maven dei progetti caricati come segue:

Per verificare la configurazione, esegui la configurazione di build [spring-jdbc-generic-01.IntroJdbc01] [1-3]:

Dovresti vedere il seguente output della console:

------------------------------ Vidage de la table [PRODUITS]
------------------------------ Remplissage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
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.00000000000001,"description":"DESC10"}
------------------------------ Mise à jour de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":110.00000000000001,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.10000000000001,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.2,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.30000000000001,"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.00000000000001,"description":"DESC10"}
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Insertion de deux produits de même clé primaire dans la table [PRODUITS]
Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire : 
- Duplicate entry '100' for key 'PRIMARY'
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Travail terminé

Negli esempi seguenti, il lettore può:

  • lavorare direttamente con i progetti caricati in precedenza;
  • oppure creare i progetti autonomamente;

3.2. Passaggi per l'utilizzo di un database

Nell'architettura sopra descritta, la gestione di un database tramite il programma da console prevede i seguenti passaggi:

  1. caricare il driver JDBC del database;
  1. aprire una connessione al database;
  2. eseguire una query SQL sul database ed elaborare i risultati della query SQL;
  3. chiudere la connessione;

Il passaggio 1 viene eseguito una sola volta. I passaggi da 2 a 4 vengono eseguiti ripetutamente. Si noti che le connessioni non vengono lasciate aperte; vengono chiuse non appena non sono più necessarie.

3.2.1. Passaggio 1 - Caricamento del driver JDBC in memoria

Il codice


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

Lo scopo dell'operazione alla riga 3 è caricare in memoria il driver JDBC del database. Questa operazione deve essere eseguita una sola volta. Tuttavia, ripeterla non causa alcun errore. La classe del driver JDBC viene cercata nel classpath del progetto. Pertanto, nel progetto Eclipse, il file [jar] contenente la classe del driver JDBC deve essere stato incluso nel classpath del progetto.

3.2.2. Passaggio 2 - Apertura di una connessione

Una volta che il driver JDBC è a posto, gli chiediamo di aprire una connessione al database:

Il codice


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
                ...
            }
         }
}
  • Righe 3–7: Le classi che implementano l'interfaccia JDBC si trovano tutte nel pacchetto [java.sql]. Inoltre, in caso di errore, generano tutte un'[SQLException] (righe 19, 27). Questa eccezione deriva dalla classe [Exception] ed è una cosiddetta eccezione controllata: è necessario utilizzare un blocco try/catch per gestirla o, in alternativa, scegliere di non gestirla e indicare che il metodo consente la propagazione dell'eccezione aggiungendo [throws SQLException] alla firma del metodo;
  • alla riga 17, [DriverManager.getConnection] è un metodo statico che richiede tre parametri:
    • [url]: l'URL del database. Si tratta di una stringa che dipende dal database utilizzato. Per MySQL, ha la forma [jdbc:mysql://localhost:3306/db_name];
    • [user]: il proprietario della connessione;
    • [passwd]: la password dell'utente;
  • righe 24–30: la connessione deve essere chiusa nella clausola [finally] in modo che venga chiusa indipendentemente dal fatto che si verifichi o meno un'eccezione.

3.2.3. Passaggio 3 - Esecuzione delle istruzioni SQL [SELECT]

Una volta stabilita la connessione, è possibile eseguire i comandi SQL. Il modo in cui vengono gestiti i comandi di lettura [SELECT] differisce da quello utilizzato per le operazioni di aggiornamento [UPDATE, INSERT, DELETE]. Inizieremo con i comandi SQL [SELECT]:

Il codice


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) {
....
}
  • Righe 8, 10: apertura di una transazione (riga 8) in modalità di sola lettura (riga 10). Una transazione è una sequenza di istruzioni SQL che o vanno tutte a buon fine o falliscono tutte. Pertanto, in una transazione contenente N istruzioni SQL, se la (I+1)-esima istruzione fallisce, le I istruzioni precedenti verranno annullate. Per un'operazione di lettura, una transazione non è necessaria. Tuttavia, la creazione di una transazione in sola lettura può consentire ad alcuni DBMS di eseguire determinate ottimizzazioni;
  • Riga 12: utilizzo di un [PreparedStatement]. Un [PreparedStatement] ha normalmente dei parametri indicati dal carattere ?. Qui, non ne ha nessuno. Un [PreparedStatement] è un'istruzione preparata dal DBMS. Questa preparazione ha un costo e viene eseguita una sola volta. L'istruzione preparata viene quindi eseguita dal DBMS con i parametri effettivi che sostituiscono i parametri segnaposto ?. Si noti che è preferibile specificare le colonne desiderate piuttosto che utilizzare la notazione * per recuperare tutte le colonne. Specificando i nomi delle colonne, i loro valori possono quindi essere recuperati in base alla loro posizione nell'istruzione SELECT;
  • Riga 13: esecuzione del [PreparedStatement]. Viene recuperato un oggetto [ResultSet];

Un oggetto [ResultSet] rappresenta una tabella, ovvero un insieme di righe e colonne. In un dato momento, abbiamo accesso a una sola riga della tabella, chiamata riga corrente. Quando il [ResultSet] viene creato inizialmente, non c'è alcuna riga corrente. Dobbiamo eseguire un'operazione [ResultSet.next()] per ottenerla. La firma del metodo next è la seguente:

    boolean next()

Questo metodo tenta di passare alla riga successiva del [ResultSet] e restituisce true in caso di esito positivo, false in caso contrario. In caso di esito positivo, la riga successiva diventa la nuova riga corrente. La riga precedente viene persa e non può essere recuperata.

La tabella [ResultSet] ha colonne denominate labelCol1, labelCol2, ... come specificato nella query [SELECT] eseguita. Con la query:

SELECT ID as myId, NOM as myNom, CATEGORIE as myCategorie, PRIX as myPrix, DESCRIPTION as myDescription FROM PRODUITS
  • la colonna [ID] andrà in una colonna nel [ResultSet] denominata [myId];
  • la colonna [NAME] andrà in una colonna del [ResultSet] denominata [myName];
  • ...

Nell'esempio sopra riportato, gli identificatori [myCol] sono denominati etichette di colonna. Senza queste etichette, i nomi delle colonne del [ResultSet] dipendono dal DBMS. Quando il [SELECT] opera su una singola tabella, le etichette di colonna saranno per impostazione predefinita i nomi delle colonne richieste dal SELECT. Il problema sorge quando il [SELECT] opera su più tabelle e tali tabelle contengono nomi di colonne identici, come nell'esempio seguente:

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

supponendo che la tabella [PRODUCTS] abbia una chiave esterna verso la tabella [CATEGORIES] rappresentata dalla relazione [PRODUCTS].CATEGORY_ID --> [CATEGORIES].ID, e che entrambe le tabelle [PRODUCTS] e [CATEGORIES] abbiano un campo [NAME]. In questo caso, i nomi assegnati nel [ResultSet] alle colonne [PRODUITS.NOM] e [CATEGORIES.NOM] dipendono dal DBMS. Per garantire la portabilità tra i DBMS, è quindi necessario utilizzare qui le etichette delle colonne, e scriveremmo:


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

Per accedere ai vari campi della riga corrente nel [ResultSet], sono disponibili i seguenti metodi:

Type getType("labelColi") 

per recuperare la colonna denominata "labelColi" dalla riga corrente, ovvero la colonna nell'istruzione [SELECT] con quell'etichetta. Type si riferisce al tipo di dati del campo "labelColi". È possibile utilizzare i seguenti metodi [getType]: getInt, getLong, getString, getDouble, getFloat, getDate, ... Invece di utilizzare il nome della colonna, è possibile utilizzare la sua posizione nella query [SELECT] eseguita:

Type getType(i) 

dove i è l'indice della colonna desiderata (i>=1).

  • righe 15–17: recupero dei valori letti dal database;
  • riga 19: la transazione viene convalidata (operazione nota anche come commit). Ciò la conclude e libera le risorse che il DBMS aveva allocato per essa;
  • riga 25: le risorse vengono rilasciate nel blocco [finally]. Questo chiama il seguente metodo [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
            }
        }
    }
  • righe 3-9: chiudere il [ResultSet];
  • righe 11–17: chiudere il [PreparedStatement];
  • righe 18–27: chiusura della connessione;

Le chiusure nelle righe 3–17 sembrano ridondanti poiché la connessione viene chiusa nelle righe 18–25. In realtà, in alcuni casi non sono ridondanti ed è consigliabile lasciarle [http://stackoverflow.com/questions/4507440/must-jdbc-resultsets-and-statements-be-closed-separately-although-the-connection].

  • Riga 22: L'eccezione viene gestita dal seguente metodo [doCatchException]:

    private static void doCatchException(Connection connexion, Throwable th) {
        // cancel transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // handle the exception
        }
}
  • righe 4–6: la transazione viene annullata. Ciò la termina e il DBMS può liberare le risorse ad essa assegnate;

3.2.4. Fase 3 - Emissione di istruzioni SQL [INSERT, UPDATE, DELETE]

Le istruzioni SQL [INSERT, UPDATE, DELETE] sono operazioni di aggiornamento: modificano il database ma non restituiscono alcuna riga. L'unica informazione restituita è il numero di righe interessate dall'operazione di aggiornamento.

Il codice


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);
        }
    }
  • riga 9: la connessione viene utilizzata per la lettura e la scrittura;
  • riga 11: un [PreparedStatement] con 1 parametro (rappresentato da ?). I parametri possono essere più di uno. Sono numerati a partire da 1;
  • riga 13: il suo valore viene assegnato al singolo parametro. Il primo parametro di [setType] è la posizione del parametro nel [PreparedStatement] (1, 2, ...) e il secondo è il valore ad esso assegnato. È possibile utilizzare i metodi [setInt, setLong, setFloat, setDouble, setString, setDate, ...];
  • riga 15: viene utilizzato il metodo [executeUpdate], non [executeQuery], che è riservato alle istruzioni SELECT. Il metodo restituisce il numero di righe interessate dall'operazione. Può essere 0.
  • riga 17: la transazione viene confermata;

3.2.5. Fase 4 - Chiusura della connessione

In un ambiente multiutente, una connessione deve essere chiusa il più rapidamente possibile perché un DBMS accetta un numero limitato di connessioni aperte. Negli esempi precedenti, è stata chiusa nella clausola [finally] delle operazioni SQL in modo che venisse chiusa indipendentemente dal verificarsi o meno di un'eccezione.

3.3. Configurazione del livello JDBC per il sistema di gestione di database MySQL 5

Esamineremo il progetto [mysql-config-jdbc], che configura il livello JDBC come segue:

3.3.1. Il progetto Eclipse

 

3.3.2. Configurazione Maven

Il file [pom.xml] del progetto è il seguente:


<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>dvp.spring.database</groupId>
    <artifactId>generic-config-jdbc</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>configuration generic jdbc</name>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>
 
    <dependencies>
        <!-- dépendances variables ********************************************** -->
        <!-- driver JDBC from SGBD -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- dépendances constantes ********************************************** -->
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- library jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- Google Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
        </dependency>
        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!-- logs -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>

Questa configurazione Maven include una serie di archivi richiesti dal progetto [mysql-config-jdbc] o dai progetti che ne dipenderanno:

  • righe 4–6: l'artefatto Maven generato dal progetto. Come accennato in precedenza, tutti i progetti [*-config-jdbc] generano questo stesso artefatto. Pertanto, due progetti [*-config-jdbc] non devono essere caricati contemporaneamente;
  • righe 9–13: il progetto Maven padre di questo. Definisce le versioni di un gran numero di archivi utilizzati dall'ecosistema Spring. Ciò evita di doverle specificare nei progetti che ne derivano;
  • righe 18–21: l'archivio del driver JDBC per il DBMS MySQL5. Questo è l'unico archivio richiesto dal progetto [spring-jdbc-01];
  • righe 24–27: l'artefatto [tomcat-jdbc] fornisce un archivio richiesto dai progetti JDBC [spring-jdbc-02 a 04];
  • righe 29–36: forniscono le librerie necessarie per la gestione di JSON. Utilizzate in quasi tutti i progetti in questo documento;
  • righe 38–42: Google Guava è una libreria di gestione delle collezioni. Utilizzata in quasi tutti i progetti in questo documento;
  • righe 43–52: librerie per la scrittura di test che integrano Spring e JUnit. Utilizzate in quasi tutti i progetti del documento;
  • righe 54–57: librerie di logging. Utilizzate in quasi tutti i progetti in questo documento;
  • righe 67–71: il plugin utilizzato per installare l'artefatto del progetto [mysql-config-jdbc] nel repository Maven locale;

3.3.3. La classe di configurazione [ConfigJdbc]

  

La classe [ConfigJdbc] è la seguente:


package generic.jdbc.config;
 
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
 
public class ConfigJdbc {
 
    // paramètres de connexion
    public final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
    public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
    public final static String USER_DBPRODUITS = "root";
    public final static String PASSWD_DBPRODUITS = "root";
...
    // ordres SQL [jdbc-01, jdbc-02]
    public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
    public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";
    //public final static String V1_DELETE_PRODUITS = String.format("DELETE FROM %s", TAB_PRODUITS);
    public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    public final static String V1_UPDATE_PRODUITS = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
    public final static String V1_INSERT_PRODUITS_2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";
 
    // ordres SQL [jdbc-03]
    public final static String V2_INSERT_PRODUITS = "INSERT INTO PRODUITS(NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?)";
    public final static String V2_DELETE_ALLPRODUITS = "DELETE FROM PRODUITS";
    public final static String V2_DELETE_PRODUITS = "DELETE FROM PRODUITS WHERE ID=?";
    public final static String V2_SELECT_ALLPRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    public final static String V2_SELECT_PRODUIT_BYID = "SELECT NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE ID=?";
    public final static String V2_SELECT_PRODUIT_BYNAME = "SELECT ID, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE NOM=?";
    public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";
 
...
 
}

La classe [ConfigJdbc] viene utilizzata per configurare il livello JDBC per i quattro progetti [spring-jdbc-01 a 04]. La maggior parte della configurazione riguarda il progetto [spring-jdbc-04]. Tratteremo questa sezione quando esamineremo quel progetto. Sopra è riportata solo la configurazione per i progetti [spring-jdbc-01 a 03].

  • righe 14–17: parametri di connessione per il database MySQL5 [dbproduits];
  • righe 20–25: le istruzioni SQL utilizzate nei progetti [spring-jdbc-01 e 02];
  • righe 28–34: le istruzioni SQL utilizzate nel progetto [spring-jdbc-03];

Queste istruzioni SQL utilizzano la tabella [PRODUCTS] nel database MySQL5 [dbproducts], che ha la seguente struttura:

 
  • [ID]: chiave primaria in modalità AUTO_INCREMENT (se non viene specificata alcuna chiave primaria, il DBMS la genera);
  • [NAME]: nome di un prodotto — univoco;
  • [CATEGORY]: numero di categoria;
  • [PRICE]: il suo prezzo;
  • [DESCRIPTION]: una descrizione del prodotto;

3.3.4. La classe [Product]

  

La classe [Product] rappresenta una riga della tabella [PRODUCTS]:


package generic.jdbc.entities.dbproduits;
 
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
...
}

Più avanti, dovremo confrontare due prodotti per determinare se sono uguali o meno. Diremo che due prodotti sono uguali se tutti i loro campi sono uguali. Per farlo, sovrascriveremo il metodo [equals] della classe [Object], da cui deriva la classe [Product]:


    // méthode d'égalité
    @Override
    public boolean equals(Object o) {
        // cas simples
        if (o == null || o.getClass() != this.getClass()) {
            return false;
        }
        Produit p = (Produit) o;
        return this == o
                || (this.id == p.id && this.nom.equals(p.getNom()) && this.categorie == p.categorie
                        && Math.abs(this.prix - p.prix) < 1e-6 && this.description.equals(p.description));
}
  • riga 3: il metodo [equals] riceve un oggetto o che deve confrontare con l'oggetto this;
  • righe 5–7: i casi semplici in cui possiamo immediatamente stabilire che i due oggetti non sono uguali. [Object].getClass() restituisce un'istanza di tipo [Class], un tipo che rappresenta la classe effettiva dell'oggetto;
  • riga 8: l'oggetto o viene convertito in un prodotto p;
  • riga 9: se i due riferimenti o e p a un prodotto sono uguali, allora si riferiscono fisicamente allo stesso prodotto;
  • riga 9: se o e p sono due riferimenti diversi a due prodotti con gli stessi campi, diremo che sono uguali. Poiché il prezzo è di tipo [double] e non esiste una rappresentazione esatta dei numeri reali nell'informatica, considereremo due prezzi identici se la loro differenza è inferiore a 10⁻⁶;

Inoltre, ridefiniremo il metodo [hashCode] della classe [Object]:


    // hashcode
    @Override
    public int hashCode() {
        return id + 2 * nom.hashCode() + 3 * categorie + 4 * description.hashCode();
}

I valori hashCode di due prodotti devono essere uguali se il metodo [equals] ha dichiarato che questi due prodotti sono uguali. Questo valore hashCode viene utilizzato per ordinare gli oggetti in collezioni come i dizionari. Nell'esempio sopra riportato, se due prodotti sono identici, avranno effettivamente lo stesso hashCode.

3.3.5. L'[UncheckedException]

  

Si consideri la seguente architettura:

  • il livello [JDBC] genera eccezioni [SQLException]. Questa eccezione deve propagarsi attraverso i livelli fino a raggiungere il livello più alto, in questo caso il livello di test;

Il livello [DAO] potrebbe semplicemente lasciare che l'[SQLException] si propaghi fino al livello di test. Ma poiché questa eccezione è non controllata (deriva direttamente da [Exception]), ciò implicherebbe che l'interfaccia [IDao] del livello [DAO] sarebbe la seguente:


public interface IDao {
 
    // ajouter des produits
    public List<Produit> addProduits(List<Produit> produits) throws SQLException;
 
    // liste de tous les produits
    public List<Produit> getAllProduits() throws SQLException;
 
    // un produit particulier
    public Produit getProduitById(int id) throws SQLException;
 
    public Produit getProduitByName(String name) throws SQLException;
 
    // mise à jour de plusieurs produits
    public int updateProduits(List<Produit> produits) throws SQLException;
 
    // suppression de tous les produits
    public int deleteAllProduits() throws SQLException;
 
    // suppression de plusieurs produits
    public int deleteProduits(int[] ids) throws SQLException;
}

E questo è molto fastidioso perché ci impedisce di implementare l'interfaccia [IDao] con una classe che genererebbe un'eccezione diversa. Per aggirare questo problema, il livello [DAO] genererà una [DaoException] non gestita (derivata da [RuntimeException]), il che ci permette di omettere la clausola [throws] nelle firme dei metodi dell'interfaccia. Di conseguenza, l'interfaccia può essere implementata da qualsiasi classe che generi anche un'eccezione non controllata, che potrebbe essere diversa da [DaoException]. La nostra architettura ora si presenta così:

Per facilitare la creazione di eccezioni non controllate per i diversi livelli di un'applicazione, creiamo una classe padre [UncheckedException] per esse:

  

package generic.jdbc.infrastructure;
 
import java.util.ArrayList;
import java.util.List;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
// generic exception class
// the exception is uncontrolled
 
public class UncheckedException extends RuntimeException {
 
    // serial ID generated
    private static final long serialVersionUID = -2924871763340170310L;
 
    // properties
    private int code;
    private String trace;
    private List<ShortException> exceptions;
 
    // manufacturers
    public UncheckedException() {
        super();
    }
 
    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // local
        this.code = code;
        this.exceptions = getErreursForException(e);
        // trace
        String fileName = String.format("%s.java", simpleClassName);
        StackTraceElement[] traces = e.getStackTrace();
        boolean trouve = false;
        for (int i = 0; !trouve && i < traces.length; i++) {
            StackTraceElement trace = traces[i];
            if (fileName.equals(trace.getFileName())) {
                this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
                trouve = true;
            }
        }
    }
 
    @Override
    public String getMessage() {
        return this.toString();
    }
 
    @Override
    public void printStackTrace() {
        System.out.println(this);
    }
 
    // list of exception error messages
    private List<ShortException> getErreursForException(Throwable th) {
        // retrieve the elements of the exception stack
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // retrieve the current exception
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // following exception
            cause = cause.getCause();
        }
        return exceptions;
    }
 
    @Override
    public String toString() {
        ObjectMapper jsonMapper = new ObjectMapper();
        try {
            return String.format("[code=%s, trace=%s, exceptions=%s", code, trace, jsonMapper.writeValueAsString(exceptions));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
 
    // getters and setters
...
}
  • riga 12: la classe estende [RuntimeException] ed è quindi un tipo di eccezione non controllata. Verrà utilizzata per incapsulare un'eccezione controllata (SQLException) in un tipo di eccezione non controllata (UncheckedException);
  • per distinguere tra le eccezioni [UncheckedException], possiamo assegnare loro un codice che verrà memorizzato nel campo privato alla riga 18. Il codice Java che intercetta una [UncheckedException] avrà accesso a questo codice di errore tramite il metodo [getCode] (righe 80 e seguenti);
  • riga 20: memorizza i messaggi di errore dallo stack dell'eccezione incapsulata;
  • righe 23–43: i diversi modi per costruire un oggetto di tipo [UncheckedException];
  • righe 56–67: un metodo privato che consente di costruire l'elenco degli errori della riga 20 a partire da un oggetto [Throwable] o da un tipo derivato, in particolare il tipo [Exception];
  • righe 69–78: il metodo [toString] restituisce una stringa che rappresenta l'eccezione. Per visualizzare l'elenco degli errori della riga 20, utilizza una libreria JSON. Questa libreria è inclusa nelle dipendenze Maven del progetto:

        <!-- library jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
</dependency>
  • righe 45–48: ridefiniscono il metodo [getMessage] della classe padre [RuntimeException]. Qui restituisce la firma [toString] della classe;
  • righe 50–53: ridefiniscono il metodo [printStackTrace] della classe padre [RuntimeException]. Verrà visualizzata la firma [toString] della classe;

La classe [UncheckedException] memorizza, nel campo alla riga 20, un elenco di eccezioni descritte dal seguente tipo [ShortException]:


package pam.dao.exceptions;
 
public class ShortException {
 
    // properties
    private String className;
    private String errorMessage;
 
    // manufacturers
    public ShortException() {
 
    }
 
    public ShortException(String className, String errorMessage) {
        this.className = className;
        this.errorMessage = errorMessage;
    }
 
    // getters and setters
...
}
  • riga 6: il nome della classe dell'eccezione che si è verificata;
  • riga 7: il messaggio di errore associato;

Esaminiamo il seguente costruttore della classe [UncheckedException]:


    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // local
        this.code = code;
        this.exceptions = getErreursForException(e);
        // trace
        String fileName = String.format("%s.java", simpleClassName);
        StackTraceElement[] traces = e.getStackTrace();
        boolean trouve = false;
        for (int i = 0; !trouve && i < traces.length; i++) {
            StackTraceElement trace = traces[i];
            if (fileName.equals(trace.getFileName())) {
                this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
                trouve = true;
            }
        }
}
  • riga 1, i parametri sono i seguenti:
    • [code]: un codice di errore;
    • [e]: l'eccezione che viene incapsulata. [Throwable] è la classe padre della classe [Exception] e deriva direttamente dalla classe [Object]. È la classe padre di tutte le classi C con cui è possibile scrivere [throw c;] dove c è un'istanza di C;
    • [simpleClassName]: il nome semplice della classe del codice utente in cui è stata rilevata l'eccezione e;
  • riga 4: viene registrato il codice di errore;
  • riga 5: viene costruito l'elenco di [ShortException] a partire da [Throwable e] passato come parametro;
  • righe 7–16: vengono esaminate le cosiddette tracce di eccezione. Un'eccezione iniziale si verifica in un punto specifico del codice e poi si propaga a ritroso verso il metodo che ha chiamato quello in cui si è verificata l'eccezione, e così via fino a quando un blocco try/catch non la intercetta. Durante questa propagazione, l'eccezione iniziale lascia delle tracce memorizzate nell'array [e.stackTrace] dell'eccezione e. Queste vengono recuperate qui alla riga 8 dal [Throwable e] passato come parametro. Ogni elemento di tipo [StackTraceElement] è un oggetto con i seguenti campi:
    • [fileName]: il nome del file Java in cui si è verificata l'eccezione;
    • [lineNumber]: il numero di riga in questo file in cui si è verificata l'eccezione;
    • [methodName]: il nome del metodo in questo file in cui si è verificata l'eccezione;
  • Le righe 10-16 cercano nell'array delle tracce l'eccezione passata come parametro, cercando la prima occorrenza della condizione [trace.fileName == simpleClassName.java], dove [simpleClassName] è il terzo parametro del costruttore. L'idea è quella di registrare dove si è verificata l'eccezione nel codice utente. Il codice utente incapsulerà un'eccezione come segue:
1
2
3
4
5
6
7
try{
// code qui peut lancer une exception contrôlée
...
}catch(UnTypeDexception e){
// on encapsule l'exception contrôlée e dans une exception non contrôlée
    throw new UncheckedException(189,e,getClass().getSimpleClassName())
}
  • riga 13: creiamo una stringa di tipo [nomeFile, nomeMetodo, numeroRiga] che descrive la posizione nel codice utente in cui è stata intercettata l'eccezione e;

Ora, esaminiamo il codice che registra l'elenco delle eccezioni dallo stack delle eccezioni dell'eccezione [Throwable th] incapsulata dal costruttore precedente:


    // liste des messages d'erreur d'une exception
    private List<ShortException> getErreursForException(Throwable th) {
        // on récupère les éléments de la pile de l'exception
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // on récupère l'exception courante
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // exception suivante
            cause = cause.getCause();
        }
        return exceptions;
}

Man mano che si propaga al metodo che l'ha intercettata utilizzando un blocco try/catch, l'eccezione iniziale e potrebbe essere stata incapsulata all'interno di un'altra eccezione. È quindi quest'ultima eccezione che si propaga al metodo che alla fine la intercetterà. Anche questa, quindi, può essere incapsulata. In definitiva, quando un metodo decide di intercettare un'eccezione th e gestirla, troverà l'eccezione iniziale e sepolta in fondo a una pila di eccezioni. Pertanto, nell'esempio sopra riportato, il parametro [Throwable th] è solo la punta dell'iceberg delle eccezioni. Il suo attributo [th.cause] rivela l'eccezione che esso stesso incapsula. E così via. Quando un'eccezione e soddisfa [e.getCause()==null], significa che e è l'eccezione iniziale.

  • Riga 8: Per ogni eccezione nello stack di eccezioni di [Throwable th], vengono memorizzate due informazioni:
    • [getClass().getName()]: il nome completo dell'eccezione;
    • [getMessage()]: il messaggio di errore associato;

3.4. Esempio-01

3.4.1. Architettura del progetto

In questo esempio, un programma da console utilizza l'interfaccia del livello [JDBC].

3.4.2. Il progetto Eclipse

Creiamo un progetto Spring/Maven [spring-jdbc-01] seguendo la procedura descritta nella Sezione 2.5.2.1.

  

Il progetto è un progetto Maven definito dal seguente file [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<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>dvp.spring.database</groupId>
    <artifactId>spring-jdbc-generic-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>spring-jdbc-generic-01</name>
    <description>Demo project for API JDBC</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <dependencies>
        <!-- configuration JDBC of SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>
  • Righe 28–32: Il progetto utilizza l'artefatto [generic-config-jdbc] del progetto [mysql-config-jdbc] che abbiamo appena esaminato. Il progetto [spring-jdbc-01] ha quindi accesso a tutti gli elementi del progetto [mysql-config-jdbc];

Possiamo verificare quest'ultimo punto in due modi, esaminando le dipendenze Maven del progetto:

  • In [2], vediamo che il progetto [mysql-config-jdbc] è elencato nelle dipendenze Maven del progetto. Poiché queste dipendenze si trovano nel Classpath del progetto, ciò significa che anche il progetto [mysql-config-jdbc] si trova in questo Classpath e, di conseguenza, le sue classi e interfacce sono visibili nel progetto [spring-jdbc-01];

Il progetto Maven [mysql-config-jdbc] non deve necessariamente essere presente nella scheda [Package Explorer] per essere utilizzabile da altri progetti Maven. Deve semplicemente essere presente nel repository Maven locale. A differenza di un IDE come NetBeans, in Eclipse ciò non avviene automaticamente. Deve essere forzato:

Abbiamo esaminato le condizioni che consentono questa generazione nella Sezione 2.3.5. Una volta completata, è possibile rimuovere il progetto [mysql-config-jdbc] dalla scheda [Package Explorer]:

  • Non selezionare [3], poiché ciò comporta l'eliminazione fisica del progetto dal disco, rendendolo irrecuperabile;

Questa operazione ricalcola le dipendenze Maven dei progetti che dipendono dal progetto rimosso dal [Package Explorer]. Ciò modifica il ramo [Maven Dependencies] di questi progetti. Ad esempio, per il progetto [spring-jdbc-01], il ramo [Maven Dependencies] diventa il seguente:

In questo caso, la dipendenza non è più su un progetto ma sul suo artefatto Maven, in questo caso l'artefatto [generic-config-jdbc] [1]. Possiamo vedere che abbiamo effettivamente accesso a tutte le classi e le interfacce di questo artefatto. Come accennato, questo artefatto verrà generato da tutti i progetti [*-config-jdbc]. Per evitare errori:

  • manteniamo sempre un unico progetto [*-config-jdbc] nella scheda [Package Explorer];
  • aggiorniamo la configurazione Maven di tutti i progetti nella scheda [Package Explorer] (Alt-F5) in modo che includano il progetto [*-config-jdbc] nelle loro dipendenze Maven;

3.4.3. Lo scheletro della classe principale

  

Lo scheletro della classe principale [IntroJdbc01] è il seguente:


package spring.jdbc;
 
import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class IntroJdbc01 {
 
    // constants
    final static ObjectMapper jsonMapper = new ObjectMapper();
 
    public static void main(String[] args) {
        // loading the JDBC driver from SGBD
        try {
            Class.forName(ConfigJdbc.DRIVER_CLASSNAME);
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // empty table [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // fill it
        System.out.println(String.format("------------------------------ %s", "Remplissage de la table [PRODUITS]"));
        insert();
        // we read it
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // update
        System.out.println(String.format("------------------------------ %s", "Mise à jour de la table [PRODUITS]"));
        update();
        // display
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // empty table [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // we display it
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // INSERTion of two identical elements
        // the INSERTion must fail and neither element is inserted because of the transaction
        System.out.println(String.format("------------------------------ %s",
                "Insertion de deux produits de même clé primaire dans la table [PRODUITS]"));
        insert2();
        // we check
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // finish
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }
 
    // product list
    private static void select() {
    ...
    }
 
    // display jSON of an object
    private static void affiche(Object object) {
...
    }
 
    // product deletion
    public static void delete() {
...
    }
 
    // add products
    public static void insert() {
...
    }

    // add 2 products with the same primary keys
    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) {
 
            }
        }
        if (connexion != null) {
            try {
                // close connection
                connexion.close();
            } catch (SQLException e3) {
                // display error msg
                show("Les erreurs suivantes se sont produites lors de la fermeture de la connexion",
                        getErreursFromThrowable(e3));
            }
        }
    }
 
    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // display error msg
        show(title, getErreursFromThrowable(th));
        // cancel transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // display error msg
            show("Erreur lors de l'annulation de la transaction", getErreursFromThrowable(e2));
        }
    }
 
    private 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;
    }
 
    private 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));
        }
    }
}
  • righe 23–29: caricamento del driver JDBC per il DBMS. Alla riga 25 viene utilizzata la costante [ConfigJdbc.DRIVER_CLASSNAME] definita nel progetto [mysql-config-jdbc];
  • righe 136–147: il metodo [getErrorsFromThrowable] restituisce l'elenco dei messaggi di errore incapsulati in un oggetto di tipo [Throwable], che è la classe padre della classe [Exception]. Un'eccezione può contenere un'altra eccezione, che può essere recuperata utilizzando il metodo [Throwable].getCause(). Ciò ci consente di iterare attraverso tutte le eccezioni incapsulate nell'oggetto [Throwable];
  • righe 149–156: il metodo [show(String title, List<String> messages)] visualizza i messaggi preceduti dal testo [title];
  • righe 122–134: il metodo [doCatchException(String title, Connection connection, Throwable th)] gestisce le eccezioni incontrate dai metodi della classe. L'eccezione gestita è rappresentata dal parametro [Throwable th]. Lo scopo del metodo è:
    • annullare la transazione corrente dell'oggetto [Connection connection] (righe 127–129);
    • scrivere i messaggi di errore incapsulati nell'eccezione [Throwable th] (righe 124, 132);
  • righe 93–120: il metodo [doFinally(ResultSet rs, PreparedStatement ps, Connection connection)] gestisce il blocco [finally] dei metodi di accesso al DBMS. Il suo scopo è liberare le risorse allocate dalla connessione;

3.4.4. Eliminazione del contenuto della tabella products

Il metodo [delete] elimina il contenuto della tabella:


    // product deletion
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // empty table [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V1_DELETE_PRODUITS);
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
        } 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, ps, connexion);
        }
}

La riga 7 utilizza le seguenti costanti della classe [ConfigJdbc]:


public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
public final static String USER_DBPRODUITS = "root";
public final static String PASSWD_DBPRODUITS = "";

Riga 13, l'istruzione SQL preparata è la seguente:


public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";

Il metodo [delete] utilizza le transazioni. Una transazione consente di raggruppare istruzioni SQL che devono andare a buon fine tutte o essere tutte annullate. Ci sono quattro operazioni da tenere presenti:

  • inizio di una transazione: [connection.setAutoCommit(false)];
  • fine di una transazione riuscita: [connection.commit()]. In questo caso, tutte le operazioni eseguite sul database durante la transazione vengono confermate;
  • fine di una transazione con errore: [connection.rollback()]. In questo caso, tutte le operazioni eseguite sul database durante la transazione vengono annullate;

Nei nostri esempi, ogni volta che si verifica un'eccezione, annulliamo la transazione nel metodo [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));
        }
}

3.4.5. Creazione del contenuto della tabella dei prodotti

Il metodo [insert] crea il contenuto della tabella:


public static void insert() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // fill the table
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_WITH_ID);
            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();
        } 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, ps, connexion);
        }
    }

Riga 12, l'istruzione SQL preparata è la seguente:


public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";

3.4.6. Visualizzazione del contenuto della tabella dei prodotti

Il metodo [select] visualizza il contenuto della tabella:


// product list
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read-only mode
            connexion.setReadOnly(true);
            // table [PRODUITS] is read
            ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                affiche(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("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(rs, ps, connexion);
        }
    }

Riga 14, l'istruzione SQL preparata è la seguente:


public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";

Il metodo [display] (riga 18) è il seguente:


    // display jSON of an object
    private static void affiche(Object object) {
        try {
            System.out.println(jsonMapper.writeValueAsString(object));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
}

Visualizza la rappresentazione JSON dell'oggetto passato come parametro (vedere JSON nella Sezione 23.12).

3.4.7. Aggiornamento del contenuto della tabella

Il metodo [update] aggiorna determinati prodotti:


    // product updates
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // table is updated
            ps = connexion.prepareStatement(ConfigJdbc.V1_UPDATE_PRODUITS);
            // category 1
            ps.setInt(1, 1);
            // execution
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
        } 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, ps, connexion);
        }
}

Riga 13, l'istruzione SQL preparata è la seguente:


public final static String V1_UPDATE_PRODUITS = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";

3.4.8. Ruolo della transazione

Il metodo [insert2] inserisce nella tabella due prodotti con la stessa chiave primaria, cosa che non è possibile. Poiché ci troviamo in una transazione, il primo inserimento verrà annullato.


    // add 2 products with the same primary keys
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // add 1 line
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_2);
            // execution
            ps.executeUpdate();
            // we add the same line a 2nd time, with the same primary key
            // the INSERTion must fail and neither element must be inserted because of the transaction
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire",
                    connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, ps, connexion);
        }
}

Riga 13, l'istruzione SQL preparata è la seguente:


public final static String V1_INSERT_PRODUITS_2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";

3.4.9. Risultati

Eseguiamo la configurazione di esecuzione denominata [spring-jdbc-generic-01.IntroJdbc01]:

 

Si ottiene il seguente output della console:


------------------------------ Vidage de la table [PRODUITS]
------------------------------ Remplissage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
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"}
------------------------------ Mise à jour de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
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"}
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Insertion de deux produits de même clé primaire dans la table [PRODUITS]
Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire : 
- Duplicate entry '100' for key 'PRIMARY'
------------------------------ Affichage de la table [PRODUITS]
Liste des produits : 
------------------------------ Travail terminé
  • riga 30: prima di inserire i due prodotti con la stessa chiave primaria, la tabella è vuota;
  • riga 35: dopo aver inserito i due prodotti con la stessa chiave primaria, la tabella è vuota. Questo dimostra il ruolo della transazione:
    • il primo inserimento va a buon fine. Non c'è motivo per cui debba fallire;
    • il secondo inserimento fallisce (riga 32). Di conseguenza, poiché questi due inserimenti rientrano nella stessa transazione, tutte le istruzioni SQL in quella transazione vengono annullate, compreso il primo inserimento.

3.4.10. Conclusione

Ciò che colpisce nei frammenti di codice precedenti è la notevole quantità di spazio dedicata alla gestione dell'[SQLException]. Poiché qualsiasi operazione JDBC può potenzialmente generare questa eccezione, nel codice sono presenti numerosi blocchi try/catch.

3.5. Esempio-02

Rivedremo l'applicazione precedente utilizzando un'origine dati [javax.sql.DataSource]:

Image

Useremo una fonte dati implementata dalla classe [org.apache.tomcat.jdbc.pool.DataSource]. Questa classe utilizza un pool di connessioni, ovvero un insieme di connessioni aperte:

  • Quando il pool viene istanziato, viene aperto un certo numero di connessioni al database. Questo numero è configurabile;
  • quando il codice Java apre una connessione, questa viene fornita dal pool;
  • quando il codice Java chiude una connessione, questa viene restituita al pool;

In definitiva, le connessioni vengono aperte una sola volta, il che migliora le prestazioni di accesso al database. L'origine dati sarà definita in una classe di configurazione Spring

3.5.1. Architettura del progetto

In questo esempio, un programma console utilizza l'interfaccia del livello [JDBC].

3.5.2. Il progetto Eclipse

Il nuovo progetto Eclipse può essere ottenuto copiando quello precedente [1-6]:

Passiamo quindi dal progetto [6] al progetto [7]:

3.5.3. Configurazione Maven

Il progetto [7] è un progetto Maven definito dal seguente file [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<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>dvp.spring.database</groupId>
    <artifactId>spring-jdbc-generic-02</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>spring-jdbc-generic-02</name>
    <description>Demo project for API JDBC</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <dependencies>
        <!-- configuration JDBC of SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • righe 28–33: la dipendenza Maven dal progetto [mysql-config-jdbc];

È il progetto [mysql-config-jdbc] che include nelle sue dipendenze Maven la libreria che fornisce un'implementazione di una fonte dati [javax.sql.DataSource] (vedi sezione 3.3.2):


        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
</dependency>

3.5.4. Configurazione Spring

  

La classe di configurazione Spring [AppConfig] è la seguente:


package spring.jdbc;
 
import generic.jdbc.config.ConfigJdbc;
 
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
 
@Configuration
@Import({ generic.jdbc.config.ConfigJdbc.class })
public class AppConfig {
    // data source
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration access JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // initially open connections
        dataSource.setInitialSize(5);
        // result
        return dataSource;
    }
 
}
  • riga 10: [AppConfig] è una classe di configurazione Spring;
  • riga 11: importazione della classe di configurazione [generic.jdbc.config.ConfigJdbc.class] definita nel progetto [mysql-config-jdbc]. Ciò significa che tutti i bean definiti da questo file di configurazione sono disponibili;
  • righe 14–27: il bean Spring che definisce l'origine dati;
  • riga 17: creazione della fonte dati, che non è ancora configurata;
  • righe 19–22: le informazioni che consentono alla fonte dati di connettersi al database;
  • riga 24: crea un pool di 5 connessioni. Qui ne serve solo una. Non ci sono mai più connessioni simultanee;

3.5.5. La classe principale

La classe principale [IntroJdbc02] è la seguente:


package spring.jdbc;
 
import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;
 
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
 
import javax.sql.DataSource;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class IntroJdbc02 {
 
    // mapper jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();
    // data source
    private static DataSource dataSource;
 
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = null;
        try {
            // spring context retrieval
            ctx = new AnnotationConfigApplicationContext(AppConfig.class);
            // data source recovery
            dataSource = ctx.getBean(DataSource.class);
            // empty table [PRODUITS]
            System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
            delete();
...
        // finish
        System.out.println(String.format("------------------------------ %s", "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);
            // in read-only mode
            connexion.setReadOnly(true);
            // table [PRODUITS] is read
            ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                affiche(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("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(rs, ps, connexion);
        }
    }
...
  • riga 25: l'origine dati. Si noti che è di tipo [javax.sql.DataSource] (riga 13), che è un'interfaccia;
  • riga 31: istanziazione degli oggetti Spring;
  • riga 32: ottenimento di un riferimento alla fonte dati. Si noti che la classe effettivamente utilizzata non viene mai menzionata. Pertanto, in questo caso, nulla suggerisce che venga utilizzata un'implementazione [TomcatJdbc];
  • riga 49: ottenimento di una connessione aperta. È così che i vari metodi in [IntroJdbc02] ottengono una connessione al database. Il resto del codice è identico a quello della classe [IntroJdbc01];

3.5.6. I test

Eseguiamo la configurazione di esecuzione denominata [spring-jdbc-generic-02.IntroJdbc02]:

 

Otteniamo gli stessi risultati di prima (sezione 3.4.9).

3.6. Esempio-03

3.6.1. Architettura del progetto

In questo esempio, i metodi di accesso ai dati sono isolati in un livello [dao]. Verranno testati utilizzando un test JUnit.

3.6.2. Il progetto Eclipse

Il progetto Eclipse [spring-jdbc-03] è un progetto Spring/Maven costruito come il precedente e poi integrato come segue:

 

I vari pacchetti hanno i seguenti ruoli:

  • [spring.jdbc.config]: configurazione del progetto Spring;
  • [spring.jdbc.dao]: implementazione del livello [DAO];
  • [spring.jdbc.infrastructure]: implementa l'eccezione non gestita [DaoException];

3.6.3. Configurazione Maven

Il progetto Maven è configurato dal seguente file [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<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>dvp.spring.database</groupId>
    <artifactId>spring-jdbc-generic-03</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
 
    <name>spring-jdbc-generic-03</name>
    <description>Demo project for API JDBC</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <dependencies>
        <!-- configuration JDBC of SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>

È identico a quello del progetto [spring-jdbc-02]. In particolare, utilizza la dipendenza Maven del progetto [mysql-config-jdbc] (righe 28–32).

3.6.4. Interfaccia del livello [DAO]

  

Il livello [DAO] fornisce la seguente interfaccia [IDao]:


package spring.jdbc.dao;
 
import java.util.List;
 
import spring.jdbc.entities.Produit;
 
public interface IDao {
 
    // add products
    public List<Produit> addProduits(List<Produit> produits);
 
    // list of all products
    public List<Produit> getAllProduits();

    // a special product
    public Produit getProduitById(int id);
 
    public Produit getProduitByName(String name);
 
    // several product updates
    public int updateProduits(List<Produit> produits);
 
    // removal of all products
    public int deleteAllProduits();
 
    // removal of several products
    public int deleteProduits(int[] ids);
}

3.6.5. La classe [DaoException]

La classe [DaoException] estende semplicemente la classe [UncheckedException] presentata nella sezione 3.3.5:

  

package spring.jdbc.infrastructure;
 
public class DaoException extends UncheckedException {
 
    private static final long serialVersionUID = 1L;
 
    // manufacturers
    public DaoException() {
        super();
    }
 
    public DaoException(int code, Throwable e, String className) {
        super(code, e, className);
    }
 
}

3.6.6. Configurazione del progetto Spring

  

La classe [AppConfig] che configura il progetto Spring è identica al file di configurazione Spring nell'esempio [spring-jdbc-02], ad eccezione della riga 11:


package spring.jdbc.config;
 
import generic.jdbc.config.ConfigJdbc;
 
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
 
@Configuration
@ComponentScan(basePackages = { "spring.jdbc.dao" })
public class AppConfig {
    // data source
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration access JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // initially open connections
        dataSource.setInitialSize(5);
        // result
        return dataSource;
    }
}
  • riga 11: il pacchetto [spring.jdbc.dao] verrà analizzato per individuare altri componenti Spring oltre a quelli definiti in questo file di configurazione;

3.6.7. Implementazione del livello [DAO]

  

Si ricordi (Sezione 3.6.4) che il livello [DAO] implementa la seguente interfaccia [IDao]:


package spring.jdbc.dao;
 
import generic.jdbc.entities.dbproduits.Produit;
 
import java.util.List;
 
public interface IDao {
 
    // add products
    public List<Produit> addProduits(List<Produit> produits);
 
    // list of all products
    public List<Produit> getAllProduits();
 
    // a special product
    public Produit getProduitById(int id);
 
    public Produit getProduitByName(String name);
 
    // several product updates
    public int updateProduits(List<Produit> produits);
 
    // removal of all products
    public int deleteAllProduits();
 
    // removal of several products
    public int deleteProduits(int[] ids);
}

Le classi [Dao1] e [Dao2] implementano entrambe questa interfaccia. La classe [Dao2] è una variante della classe [Dao1] che introduce una nuova funzionalità sintattica. Ci concentreremo sulla classe [Dao1]. Il suo scheletro è il seguente:


package spring.jdbc.dao;
 
import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;
 
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
 
import javax.sql.DataSource;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
 
import spring.jdbc.infrastructure.DaoException;
 
@Component("dao1")
public class Dao1 implements IDao {
 
    // class name
    private String simpleClassName = getClass().getSimpleName();
    // data source
    @Autowired
    protected DataSource dataSource;
 
    // manufacturer
    public Dao1() {
        System.out.println("building Dao1...");
    }
 
    // ------------------------------- interface
    @Override
    public List<Produit> getAllProduits() {
...
    }
 
    @Override
    public Produit getProduitById(int id) {
...
    }
 
    @Override
    public Produit getProduitByName(String name) {
...
    }
 
    @Override
    public List<Produit> addProduits(List<Produit> produits) {
....
    }
 
    @Override
    public int updateProduits(List<Produit> produits) {
...
    }
 
    @Override
    public int deleteAllProduits() {
...
    }
 
    @Override
    public int deleteProduits(int[] ids) {
...
    }
 
    // ---------------------------------------- local methods
    // management finally
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        ...
    }
 
    // catch management
    protected DaoException doCatchException(Connection connexion, Throwable th, int code, DaoException daoException) {
...
}
  • riga 20: la classe [Dao] è un componente Spring denominato [dao1]. Questo nome è facoltativo. Quando non è presente, il nome utilizzato è il nome della classe con la prima lettera minuscola;
  • riga 24: il nome della classe. Evitiamo di hard-codare [Dao] per consentire di rinominare la classe senza dover ridefinire questo campo, che rimane così valido;
  • righe 26–27: iniezione della fonte dati [tomcat-jdbc] definita nella classe di configurazione [AppConfig];
  • righe 36–68: implementazione dell'interfaccia [IDao];
  • righe 78–80: gestione centralizzata dei blocchi catch per i vari metodi;
  • righe 72–75: gestione centralizzata dei blocchi `finally` per i vari metodi;

I blocchi catch dei vari metodi vengono gestiti come segue:


    // gestion catch
    protected DaoException doCatchException(Connection connexion, Throwable th, int code) {
        // annulation transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            e2.printStackTrace();
        }
        // daoException
        return new DaoException(code, th, simpleClassName);
}
  • Riga 2: Il metodo è dichiarato [protected], il che consente alle classi figlie di utilizzarlo senza che sia pubblico. Accetta i seguenti parametri:
    • [Connection connection]: la connessione al DBMS — può essere null;
    • [Throwable th]: l'eccezione che si è verificata e che verrà avvolta in un tipo [DaoException];
    • [int code]: un codice di errore da utilizzare se il metodo crea una nuova [DaoException];
  • righe 4–7: il ruolo principale di questo metodo è quello di eseguire il rollback della transazione associata alla connessione passata come parametro 1;
  • righe 8–10: se il rollback della transazione fallisce, la traccia dell'eccezione viene scritta sulla console. Non c'è molto altro che possiamo fare, dato che lanceremo un'eccezione alla riga 12;

I blocchi *finally* dei vari metodi vengono gestiti come segue:


// management finally
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        // 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) {
                // record the error if possible
                if (daoException == null) {
                    daoException = new DaoException(code, e3, simpleClassName);
                }
            }
        }
        // result
        return daoException;
    }
  • riga 2: anche questo metodo è dichiarato [protected]. Accetta i seguenti parametri:
    • [ResultSet rs]: il [ResultSet] se è stata eseguita un'operazione [SELECT] — può essere nullo;
    • [PreparedStatement ps]: il [PreparedStatement] che è stato eseguito — può essere nullo;
    • [Connection connection]: la connessione al DBMS — può essere null;
    • [int code]: un codice di errore da utilizzare se il metodo genera una nuova [DaoException];
    • [DaoException daoException]: la [DaoException] se se ne è verificata una prima del blocco finally — può essere null;
  • righe 21–30: lo scopo principale di questo metodo è chiudere la connessione (riga 23);
  • righe 24–29: se si verifica un'eccezione durante questa chiusura, controlliamo lo stato del parametro [DaoException daoException] che ci è stato passato: se [daoException == null], creiamo una nuova [DaoException] con il codice passato come parametro;
  • riga 32: la vecchia o la nuova [DaoException] viene restituita come risultato;

Non presenteremo tutti i metodi della classe [Dao], ma solo alcuni. Sono tutti simili.

3.6.7.1. Il metodo [getProductById]

Il metodo [getProductById] restituisce il prodotto la cui chiave primaria è uguale al parametro [id], oppure null in caso contrario;


@Override
    public Produit getProduitById(int id) {
        // connection resources
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        // initially no exceptions
        DaoException daoException = null;
        // the product you are looking for
        Produit produit = null;
        try {
            // opening connection
            connexion = dataSource.getConnection();
            // start of transaction
            connexion.setAutoCommit(false);
            // in read-only mode
            connexion.setReadOnly(true);
            // table [PRODUITS] is read
            ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_PRODUIT_BYID);
            ps.setInt(1, id);
            rs = ps.executeQuery();
            if (rs.next()) {
                produit = new Produit(id, rs.getString(1), rs.getInt(2), rs.getDouble(3), rs.getString(4));
            }
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            daoException = doCatchException(connexion, e1, 112);
        } finally {
            // we treat the finally
            daoException = doFinally(rs, ps, connexion, 113, daoException);
        }
        // exception?
        if (daoException != null) {
            throw daoException;
        }
        // result
        return produit;
    }
  • riga 10: il prodotto da restituire è impostato su null;
  • riga 19: l'istruzione SQL [ConfigJdbc.V2_SELECT_PRODUCT_BYID] è la seguente:

public final static String V2_SELECT_PRODUIT_BYID = "SELECT NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE ID=?";

  • righe 22–24: se il [ResultSet] contiene una riga, questa viene utilizzata per creare il prodotto da restituire; in caso contrario, il prodotto da restituire rimane nullo;
  • riga 41: il prodotto viene restituito;
  • riga 8: la [DaoException] del metodo viene inizializzata a null;
  • riga 31: il metodo [doCatchException] crea una [DaoException];
  • riga 34: il parametro [daoException] del metodo [doFinally] è null oppure l'eccezione creata dal metodo [doCatchException]. Il metodo [doFinally]:
    • lascia questo parametro invariato se chiude con successo la connessione;
    • lascia questo parametro invariato se non riesce a chiudere la connessione e si è già verificata in precedenza una [DaoException];
    • crea una nuova [DaoException] se non riesce a chiudere la connessione e non si è verificata alcuna [DaoException] in precedenza;
  • righe 37–39: se la [daoException] locale non è null, viene generata; altrimenti, viene restituito il risultato richiesto (riga 41);

3.6.7.2. Il metodo [deleteProducts]

Il metodo [deleteProduits] elimina i prodotti le cui chiavi primarie gli vengono passate come parametri. Restituisce il numero di prodotti eliminati.


@Override
    public int deleteProduits(int[] ids) {
        // connection resources
        PreparedStatement ps = null;
        Connection connexion = null;
        // initially no exceptions
        DaoException daoException = null;
        // number of products updated
        int nbProduits = 0;
        try {
            // opening connection
            connexion = dataSource.getConnection();
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // we do away with products
            ps = connexion.prepareStatement(ConfigJdbc.V2_DELETE_PRODUITS);
            for (int id : ids) {
                // settings
                ps.setInt(1, id);
                // execution
                nbProduits += ps.executeUpdate();
            }
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            daoException = doCatchException(connexion, e1, 171);
        } finally {
            // we treat the finally
            daoException = doFinally(null, ps, connexion, 172, daoException);
        }
        // exception?
        if (daoException != null) {
            throw daoException;
        }
        // result
        return nbProduits;
    }
  • Riga 18, l'istruzione SQL [ConfigJdbc.V2_DELETE_PRODUITS] è la seguente:

public final static String V2_DELETE_PRODUITS = "DELETE FROM PRODUITS WHERE ID=?";

  • righe 18–24: il codice per l'eliminazione dei prodotti. Si può notare che l'istruzione SQL viene preparata una volta (riga 18) ed eseguita n volte (righe 19–24). Questo è il vantaggio dell'oggetto [PreparedStatement];
  • riga 23: il metodo [PreparedStatement].executeUpdate() restituisce il numero di righe interessate dall'operazione di aggiornamento;
  • riga 41: restituisce il numero di prodotti aggiornati;

3.6.7.3. Il metodo [updateProducts]

Il metodo [updateProduits] aggiorna nel database i prodotti che gli vengono passati come parametri. Restituisce il numero di prodotti aggiornati.


@Override
    public int updateProduits(List<Produit> produits) {
        // connection resources
        PreparedStatement ps = null;
        Connection connexion = null;
        // initially no exceptions
        DaoException daoException = null;
        // number of products updated
        int nbProduits = 0;
        try {
            // opening connection
            connexion = dataSource.getConnection();
            // start of transaction
            connexion.setAutoCommit(false);
            // in read/write mode
            connexion.setReadOnly(false);
            // table [PRODUITS] is updated
            ps = connexion.prepareStatement(ConfigJdbc.V2_UPDATE_PRODUITS);
            for (Produit produit : produits) {
                // settings
                ps.setString(1, produit.getNom());
                ps.setDouble(2, produit.getPrix());
                ps.setInt(3, produit.getCategorie());
                ps.setString(4, produit.getDescription());
                ps.setInt(5, produit.getId());
                // execution
                nbProduits += ps.executeUpdate();
            }
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            daoException = doCatchException(connexion, e1, 131);
        } finally {
            // we treat the finally
            daoException = doFinally(null, ps, connexion, 132, daoException);
        }
        // exception?
        if (daoException != null) {
            throw daoException;
        }
        // result
        return nbProduits;
    }
  • riga 18: l'istruzione SQL [ConfigJdbc.V2_UPDATE_PRODUITS] è la seguente:

public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";
  • righe 19–28: il codice di aggiornamento del prodotto;

3.6.7.4. Il metodo [addProducts]

Il metodo [addProducts] aggiunge al database i prodotti che gli vengono passati come parametri. Restituisce questi stessi prodotti con le loro chiavi primarie (prima di essere aggiunti al database, i prodotti non hanno una chiave primaria).


@Override
    public List<Produit> addProduits(List<Produit> produits) {
        // connection resources
        PreparedStatement ps = null;
        Connection connexion = null;
        // initially no exceptions
        DaoException daoException = null;
        try {
            // opening connection
            connexion = dataSource.getConnection();
            // in read/write mode
            connexion.setReadOnly(false);
            // start of transaction
            connexion.setAutoCommit(false);
            // add elements to table [PRODUITS]
            String generatedColumns[] = { ConfigJdbc.TAB_PRODUITS_ID };
            ps = connexion.prepareStatement(ConfigJdbc.V2_INSERT_PRODUITS, generatedColumns);
            for (Produit produit : produits) {
                // settings
                ps.setString(1, produit.getNom());
                ps.setLong(2, produit.getCategorie());
                ps.setDouble(3, produit.getPrix());
                ps.setString(4, produit.getDescription());
                // order execution
                ps.executeUpdate();
                // generated primary key
                ResultSet generatedKeys = ps.getGeneratedKeys();
                if (generatedKeys.next()) {
                    produit.setId(generatedKeys.getInt(1));
                } else {
                    throw new RuntimeException(String.format("Le produit de nom [%s] n'a pas récupéré de clé primaire",
                            produit.getNom()));
                }
            }
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException | RuntimeException e1) {
            // we handle the exception
            daoException = doCatchException(connexion, e1, 151);
        } finally {
            // we treat the finally
            daoException = doFinally(null, ps, connexion, 152, daoException);
        }
        // exception?
        if (daoException != null) {
            throw daoException;
        }
        // result
        return produits;
}
  • Riga 16, l'istruzione SQL [ConfigJdbc.V2_INSERT_PRODUITS] è la seguente:

public final static String V2_INSERT_PRODUITS = "INSERT INTO PRODUITS(NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?)";

In quanto sopra, il comando di inserimento del prodotto non include la chiave primaria [ID]. Poiché la chiave primaria nel database MySQL ha l'attributo [AUTOINCREMENT], il DBMS genererà una chiave primaria per ogni inserimento. Il problema sorge nel recuperare questa chiave. Questo è un punto importante perché le operazioni sui prodotti vengono eseguite tramite le loro chiavi primarie. Abbiamo quindi bisogno di conoscere queste chiavi;

  • righe 17–33: il ciclo di inserimento dei prodotti;
  • riga 16: una forma specifica del metodo [prepareStatement]. Il secondo parametro [generatedColumns] è un array di nomi di colonne i cui valori vogliamo recuperare dopo l'inserimento. Alla riga 16, abbiamo specificato che volevamo recuperare il valore della colonna [id]. Si noti che, sebbene i nomi delle colonne di una tabella non facciano distinzione tra maiuscole e minuscole, il DBMS PostgreSQL richiede che questo nome sia in minuscolo. Questo è tipicamente il tipo di problema che si incontra quando si trasferisce il codice da un DBMS a un altro;
  • riga 24: inserimento di una riga nel database;
  • Riga 26: recupera l'elenco dei valori dalle colonne specificate alla riga 16 in un [ResultSet]. Qui, per un singolo inserimento, il [ResultSet] conterrà una riga, e quella riga avrà una singola colonna contenente la chiave primaria;
  • Riga 28: recupera la chiave primaria generata dal DBMS;
  • Righe 29–32: se la chiave primaria generata non viene ottenuta, viene generata una [RuntimeException], che verrà avvolta in una [DaoException] (righe 38–40);

3.6.8. La classe [Dao2]

  

La classe [Dao2] è una variante della classe [Dao1] che utilizza una sintassi denominata try-with-resource(resource):

1
2
3
4
try(resource){
...
}
...
  • [resource] è una risorsa che implementa l'interfaccia [java.lang.AutoCloseable]. Tutte le risorse liberate tramite il metodo [close] rientrano in questa categoria. Questa sintassi garantisce che, alla riga 4, la risorsa [resource] venga chiusa. Ciò evita di dover scrivere una clausola [finally] per eseguire questa operazione di chiusura;

Prendiamo come esempio il metodo [getAllProducts] della classe [Dao2]:


    @Override
    public List<Produit> getAllProduits() {
        // possible exception
        DaoException daoException = null;
        // product list
        List<Produit> produits = new ArrayList<Produit>();
        try (Connection connexion = dataSource.getConnection()) {
            // start of transaction
            connexion.setAutoCommit(false);
            // in read-only mode
            connexion.setReadOnly(true);
            // table [PRODUITS] is read
            try (PreparedStatement ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_ALLPRODUITS)) {
                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) {
                        produits.add(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
                    }
                }
                // end transaction
                connexion.commit();
                // return to default mode
                connexion.setAutoCommit(true);
            } catch (SQLException e1) {
                // cancel the transaction
                daoException = doRollback(connexion, e1, 111);
            }
        } catch (SQLException e2) {
            // we handle the exception
            if (daoException == null) {
                daoException = new DaoException(112, e2, simpleClassName);
            }
        }
        // exception?
        if (daoException != null) {
            throw daoException;
        }
        // result
        return produits;
}
  • Riga 7: blocco try con la risorsa [Connection]. La riga 27 garantisce che venga chiusa;
  • riga 13: blocco try con la risorsa [PreparedStatement]. La riga 23 garantisce che sia chiusa;
  • riga 14: blocco try con la risorsa [ResultSet]. La riga 19 garantisce che sia chiusa;
  • Riga 25: La transazione viene annullata come segue:

    private DaoException doRollback(Connection connexion, Throwable e1, int code) {
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // génération de l'exception
        return new DaoException(code, e1, simpleClassName);
}

Alla fine, abbiamo un codice più facile da leggere.

3.6.9. Implementazione del livello di test

3.6.9.1. Le classi di test

  
  • il test [JUnitTestDao1] è un test JUnit per la classe [Dao1];
  • Il test [JUnitTestDao2] è un test JUnit per la classe [Dao2];
  • [AbstractJUnitTestDao] è la classe padre delle due classi di test precedenti;
  • [MainTestDao1] è una classe di test da console per la classe [Dao1];
  • [MainTestDao2] è una classe di test da console per la classe [Dao2];
  • [AbstractMainTestDao] è la classe padre delle due classi precedenti. Riutilizza il codice delle classi console [IntroJdbc01, IntroJdbc02] già presentate, quindi non esamineremo queste classi console;

La classe [JUnitTestDao1] è la seguente:


package spring.jdbc.tests;
 
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
 
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestDao1 extends AbstractJUnitTestDao {
 
    // layer [DAO]
    @Autowired
    @Qualifier("dao1")
    private IDao dao;
 
    @Override
    IDao getDao() {
        return dao;
    }
 
}
  • Le annotazioni alle righe 12–13 sono state discusse nella Sezione 2.5.5. Consentono a un test JUnit di accedere facilmente al contesto Spring e ai suoi bean. Questo contesto è configurato dalla classe [AppConfig] (riga 12) discussa nella Sezione 2.4.3;
  • riga 14: la classe estende la classe [AbstractJUnitTestDao], di cui parleremo tra poco. I metodi di test JUnit si trovano all'interno di questa classe;
  • righe 17–19: il bean denominato [dao1] (riga 18) viene iniettato (riga 17). Pertanto, qui viene iniettata un'istanza della classe [Dao1];
  • righe 21–24: il metodo [getDao] sovrascrive il metodo con lo stesso nome nella classe padre;

In definitiva, lo scopo di questa classe è fornire alla classe padre un riferimento al livello [DAO] che deve essere testato, in questo caso un'istanza di [Dao1]. Analogamente, la classe [JUnitTestDao2] fornisce alla classe padre [AbstractJUnitTestDao] un'istanza della classe [Dao2].

La classe [AbstractJUnitTestDao] è una classe di test JUnit:


package spring.jdbc.tests;
 
import generic.jdbc.entities.dbproduits.Produit;
 
import java.util.ArrayList;
import java.util.List;
 
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.BeansException;
 
import spring.jdbc.dao.IDao;
import spring.jdbc.infrastructure.DaoException;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public abstract class AbstractJUnitTestDao {
 
    // layer [DAO]
    abstract IDao getDao();
 
    // mapper jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();
 
    @Before
    public void clean() {
        // the base is cleaned before each test
        log("Vidage de la base de données", 1);
        getDao().deleteAllProduits();
    }
 
    @Test
    public void getProduits() throws JsonProcessingException {
    ...
    }
 
    @Test
    public void getProduitBy() {
    ...
    }
 
    @Test
    public void doInsertsInTransaction() {
...
    }
 
    @Test
    public void updateProduits() {
    ...
    }
 
    @Test
    public void deleteProduits() {
    ....
    }
 
    @Test
    public void perf1() {
        ...
    }
 
    @Test
    public void perf2() {
    ...
    }
 
    @Test
    public void perf3() {
    ....
    }

    // -------------- private methods
...
}
  • riga 19: la classe [AbstractJUnitTestDao] è astratta;
  • riga 22: il metodo astratto [getDao], che fornisce un riferimento al livello [DAO] da testare. Questo metodo viene implementato dalle classi figlie;
  • riga 25: un mappatore JSON che ci permette di visualizzare il valore JSON dei prodotti sulla console;
  • righe 27–32: prima di ogni test (riga 27), la tabella [PRODUCTS] viene svuotata;

3.6.9.2. Il metodo privato [fill]

Il metodo privato [fill] viene utilizzato per aggiungere prodotti alla tabella [PRODUCTS].


private List<Produit> fill(int nbProduits) {
        log("Remplissage de la base de données", 1);
        // on crée une liste de produits
        List<Produit> produits = new ArrayList<Produit>();
        for (int i = 0; i < nbProduits; i++) {
            int n = i + 1;
            // int id, String nom, int categorie, double prix, String description
            produits.add(new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format(
                    "DESC%s", n)));
        }
        // on la persiste en base - on récupère des produits avec leur clé primaire
        produits = getDao().addProduits(produits);
        // on crée un dictionnaire des produits pour pouvoir les retrouver + facilement
        // la clé du dictionnaire est la clé primaire du produit en base
        for (Produit produit : produits) {
            mapProduits.put(produit.getId(), produit);
        }
        // on rend les produits
        return produits;
    }
  • riga 1: il metodo [fill] inserisce [nbProducts] nella tabella [PRODUCTS], che si presume vuota;
  • righe 3–10: creazione di un elenco di prodotti nella forma:

new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format("DESC%s", n)));

che utilizza il costruttore Product(int id, String name, int category, double price, String description). Il valore del primo parametro [id] (chiave primaria della tabella [PRODUCTS]) è irrilevante poiché il metodo [addProducts] alla riga 10 non lo inserisce nel database e lascia che sia il DBMS a generare il suo valore;

  • riga 12: l'elenco dei prodotti viene salvato nel database. A ogni prodotto di questo elenco viene assegnata una nuova chiave primaria [id]. Il metodo [addProduits] restituisce il suo parametro [produits]. Avremmo quindi potuto omettere il recupero del risultato;
  • righe 15–17: i prodotti vengono inseriti in un dizionario:

    // dictionnaire des produits
    private Map<Integer, Produit> mapProduits = new HashMap<Integer, Produit>();

La chiave del dizionario è la chiave primaria del prodotto, mentre il valore associato è il prodotto stesso;

  • riga 19: restituiamo l'elenco dei prodotti;

3.6.9.3. Il test [getProducts]

È il seguente:


    @Test
    public void getProduits() throws JsonProcessingException {
        // remplissage
        fill(10);
        // liste des produits
        log("Liste des produits", 2);
        List<Produit> produits = getDao().getAllProduits();
        affiche(produits);
        // on vérifie que la liste récupérée et celle persistée sont les mêmes
        for (Produit produit : produits) {
            Produit found = mapProduits.get(produit.getId());
            Assert.assertEquals(found, produit);
            mapProduits.remove(found.getId());
        }
        // tous les produits initiaux doivent avoir disparu du dictionnaire
        Assert.assertEquals(0, mapProduits.size());
}
}
  • riga 4: vengono aggiunti 10 prodotti al database;
  • riga 7: una volta fatto ciò, richiediamo di visualizzare tutti i prodotti nel database;
  • riga 8: li visualizziamo. L'obiettivo è verificare che i prodotti siano stati salvati correttamente e che abbiano una chiave primaria;
  • righe 10–13: verifichiamo che i prodotti recuperati siano identici a quelli che abbiamo salvato e che siano presenti nel dizionario [mapProduits];
  • riga 11: recuperiamo dal dizionario il prodotto con la stessa chiave primaria di quello restituito dal database. Questo dimostra che ai prodotti salvati è stata effettivamente assegnata una chiave primaria;
  • riga 12: ci assicuriamo che i due prodotti siano identici. Ricordiamo che la classe [Product] ha definito un metodo [equals] (vedi sezione 3.3.4);
  • riga 13: l'elemento trovato viene rimosso dal dizionario;
  • riga 16: verifichiamo che il dizionario dei prodotti iniziali sia effettivamente vuoto, il che significa che questi prodotti iniziali erano tutti presenti nell'elenco dei prodotti recuperati dal database;

Il metodo [display] alla riga 8 è il seguente metodo privato:


    // product list display
    private <T> void affiche(List<T> elements) throws JsonProcessingException {
        for (T element : elements) {
            System.out.println(jsonMapper.writeValueAsString(element));
        }
}
  • riga 2: Il metodo [display] è un metodo generico. È parametrizzato da un tipo T, indicato sintatticamente come <T>. Se fosse parametrizzato da due tipi T1 e T2, scriveremmo <T1,T2>. La sintassi di un metodo m parametrizzato da un tipo T è la seguente:
portée <T> type_résultat m(... , T value1, ...){
...
    T value2=...
}

Nel codice del metodo m troveremo dati di tipo T. Il metodo m di un'istanza c della classe C può quindi essere chiamato come segue:

type_résultat r=c.<T1>m(..., T1 value1, ..) ;

dove T1 è il tipo effettivo che sostituirà il tipo formale T del metodo m. Nella maggior parte dei casi, il compilatore è in grado di dedurre il tipo T1 dagli argomenti del metodo m. Pertanto, l'istruzione precedente verrà spesso semplificata in:

type_résultat r=c.m(..., T1 value1, ..) ;

Torniamo al metodo [display]. Esso visualizza un elenco di elementi di tipo T. Ciò è possibile perché il mappatore JSON utilizzato alla riga 4 è in grado di rendere la rappresentazione JSON di qualsiasi tipo di oggetto. In questo specifico esempio, l'unico tipo T utilizzato sarà il tipo [Product].

Il metodo [display] avrebbe potuto essere scritto anche come segue:


    // product list display
    private void affiche(Object o) throws JsonProcessingException {
            System.out.println(jsonMapper.writeValueAsString(o));
        }

Poiché il parametro effettivo è un elenco di prodotti, la riga 3 avrebbe stampato la rappresentazione JSON di tale elenco. Ciò non equivale a stampare la rappresentazione di ciascuno dei suoi elementi uno per uno.

L'output prodotto dal test [getProducts] è il seguente:

-- Liste des produits
{"id":150189,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":150190,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":150191,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":150192,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":150193,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":150194,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":150195,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":150196,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":150197,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":150198,"nom":"NOM10","categorie":3,"prix":109.00000000000001,"description":"DESC10"}

3.6.9.4. Il test [getProductBy]

È il seguente:


    @Test
    public void getProduitBy() {
        // remplissage
        fill(10);
        log("getProduitBy", 1);
        Produit produit = getDao().getProduitByName("NOM3");
        Produit produit2 = getDao().getProduitById(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
}
  • riga 6: il metodo [getProductByName] dell'interfaccia [IDao] viene utilizzato per recuperare il prodotto denominato [NAME3];
  • riga 7: il metodo [getProductById] dell'interfaccia [IDao] viene quindi utilizzato per recuperare lo stesso prodotto, questa volta identificato dalla sua chiave primaria;
  • righe 8–10: verifichiamo che [product2] e [product] abbiano le stesse caratteristiche;

3.6.9.5. Il test [doInsertsInTransaction]

È il seguente:


    @Test
    public void doInsertsInTransaction() {
        log("Ajout de deux produits de même nom", 1);
        // on fait l'insertion
        List<Produit> inserts = new ArrayList<Produit>();
        inserts.add(new Produit(0, "x", 1, 1.0, ""));
        inserts.add(new Produit(0, "x", 1, 1.0, ""));
        boolean erreur = false;
        try {
            getDao().addProduits(inserts);
        } catch (DaoException daoException) {
            erreur = true;
        }
        // vérifications
        Assert.assertTrue(erreur);
        List<Produit> produits = getDao().getAllProduits();
        Assert.assertEquals(0, produits.size());
}
  • righe 5-7: creiamo un elenco di due prodotti con lo stesso nome [x];
  • riga 10: questi due prodotti vengono inseriti nella tabella [PRODUCTS], che è vuota (il metodo [clean] annotato con [@Before]). Il primo inserimento avrà esito positivo, ma non il secondo, poiché la tabella [PRODUCTS] ha un vincolo di unicità sui nomi dei prodotti. Deve quindi verificarsi un'eccezione. Questo viene verificato alla riga 15;
  • poiché tutti i metodi dell'interfaccia [IDao] vengono eseguiti all'interno di una transazione, il fatto che il secondo inserimento fallisca causerà il rollback dell'intera transazione, compreso il primo inserimento. In definitiva, non dovrebbe essere effettuato alcun inserimento nella tabella [PRODUCTS];
  • Righe 16–17: Verifichiamo ciò recuperando l'elenco dei prodotti nella tabella [PRODUCTS] e controllando che tale elenco sia vuoto;

3.6.9.6. Il test [updateProducts]

È il seguente:


    @Test
    public void updateProduits() {
        // remplissage
        fill(10);
        log("Mise à jour du prix des produits de catégorie 1", 1);
        // on récupère les produits
        List<Produit> produits = getDao().getAllProduits();
        // on met à jour ceux de catégorie 1
        List<Produit> updated = new ArrayList<Produit>();
        int nbUpdated = 0;
        for (Produit produit : produits) {
            if (produit.getCategorie() == 1) {
                // int id, String nom, int categorie, double prix, String description
                updated
                        .add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
                nbUpdated++;
            }
        }
        int nbProduits = getDao().updateProduits(updated);
        // vérifications
        // Assert.assertEquals(nbUpdated, nbProduits); -- does not work with DB2
        for (Produit produit : updated) {
            Produit produit2 = getDao().getProduitById(produit.getId());
            Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
        }
}
  • Riga 4: inseriamo 10 prodotti nel database;
  • riga 7: li recuperiamo;
  • righe 9–18: aumentiamo i prezzi dei prodotti della categoria n. 1 del 10%;
  • riga 19: queste modifiche vengono salvate nel database;
  • righe 22–25: percorriamo l'elenco dei prodotti utilizzati per l'aggiornamento in memoria. Per ciascuno di essi, cerchiamo il prodotto con la stessa chiave primaria nel database e verifichiamo che l'aggiornamento del prezzo sia andato a buon fine;
  • riga 19: recuperiamo il numero di prodotti aggiornati dall'operazione [updateProducts];
  • riga 21: verifichiamo che questo numero sia effettivamente quello previsto. Questo test viene superato da tutti i DBMS tranne DB2. Lo abbiamo quindi commentato;

3.6.9.7. Il test [deleteProducts]

Questo test è il seguente:


    @Test
    public void deleteProduits() {
        // filling
        fill(10);
        log("deleteProduits", 1);
        // product list
        List<Produit> produits = getDao().getAllProduits();
        // discontinuation of two products
        Produit produit0 = produits.get(0);
        Produit produit5 = produits.get(5);
        int nbDeleted = getDao().deleteProduits(new int[] { produit0.getId(), produit5.getId() });
        // checks
        // Assert.assertEquals(2, nbDeleted); -- does not pass with DB2
        Assert.assertNull(getDao().getProduitById(produit0.getId()));
        Assert.assertNull(getDao().getProduitById(produit5.getId()));
        Assert.assertEquals(produits.size() - 2, getDao().getAllProduits().size());
}
  • riga 4: inseriamo 10 prodotti nel database;
  • righe 7–11: recuperiamo tutti i prodotti dal database e rimuoviamo i prodotti alle posizioni 0 e 5;
  • righe 14–16: verifichiamo che i due prodotti non siano più presenti nel database e che il database ora contenga due prodotti in meno;
  • Il test alla riga 13 fallisce con il DBMS DB2. Supera il test con gli altri DBMS;

3.6.9.8. Test delle prestazioni

Abbiamo incluso nei test tre metodi il cui unico scopo è valutare le prestazioni del DBMS:


    @Test
    public void perf1() {
        // remplissage
        fill(10000);
    }

    @Test
    public void perf2() {
        // remplissage
        fill(10000);
        // modification
        List<Produit> produits = getDao().getAllProduits();
        // on met à jour ceux de catégorie 1
        List<Produit> updated = new ArrayList<Produit>();
        for (Produit produit : produits) {
            // int id, String nom, int categorie, double prix, String description
            updated.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
        }
        getDao().updateProduits(updated);
    }
 
    @Test
    public void perf3() {
        // remplissage
        fill(10000);
        // suppression
        List<Produit> produits = getDao().getAllProduits();
        // clés primaires
        int[] keys = new int[produits.size()];
        for (int i = 0; i < keys.length; i++) {
            keys[i] = produits.get(i).getId();
        }
        getDao().deleteProduits(keys);
}
  • righe 1–5: inserimento di 10.000 prodotti;
  • righe 8–20: inserimento di 10.000 prodotti e successiva modifica utilizzando le loro chiavi primarie;
  • righe 23-34: inserimento di 10.000 prodotti, quindi eliminazione degli stessi utilizzando le loro chiavi primarie;

Per eseguire i test [JUnitTestDao1] e [JUnitTestDao2], è possibile utilizzare le seguenti configurazioni di test:

I risultati del test [JUnitTestDao1] sono i seguenti:

In [1] i risultati di [JUnitTestDao1] e in [2] quelli di [JUnitTestDao2]. Non vi sono differenze significative tra loro. In [1]:

  • il test ha esito positivo;
  • l'inserimento di 10.000 prodotti richiede 3,15 secondi;
  • l'inserimento di 10.000 prodotti seguito dalla loro modifica richiede 4,80 secondi;
  • l'inserimento di 10.000 prodotti seguito dalla loro cancellazione richiede 4,40 secondi;
  • quindi l'operazione più dispendiosa è l'inserimento;