Skip to content

3. Introduzione a API JDBC

3.1. Configurazione dell’ambiente di lavoro

Lavoreremo con un database MySQL5.

È necessario disporre di:

  • installato un JDK (Java Development Kit) (paragrafo 23.1);
  • il gestore di dipendenze Maven (paragrafo 23.2);
  • installato la Spring Tool Suite (IDE) (STS) (paragrafo 23.3);
  • installato SGBD MySQL5 (paragrafo 23.4) e il relativo client EMS MyManager (paragrafo 23.5);
  • scaricato i codici del documento [http://tahe.developpez.com/java/spring-database];

Si suppone che l'amministratore di MySQL5 sia root con la password root. Avviare SGBD, MySQL5 e il relativo client [MyManager]. Utilizzando [MyManager], creiamo il database [dbproduits] [1-34]:

  • in [3], la base deve chiamarsi [dbproduits];
  • in [8-9], come root con la password root (cosa che non risulta dalla schermata sopra);
  • in [14a], la password è di nuovo root (cosa che non si vede nella schermata);
  • in [15], è stato creato il database [dbproduits];
  • in [20], prestare attenzione al database selezionato. Deve essere il database [dbproduits];
  • in [22], la cartella è <esempi>/spring-database-config/mysql/databases dove <esempi> è la cartella degli esempi scaricati;
  • in [23], selezionare lo script SQL [dbproduits.sql]. Questo genererà la tabella [PRODUITS] nel database [dbproduits];
  • in [30], è stata creata la tabella [produits];
  • in [33], le colonne della tabella [produits];
  • in [34], inizialmente vuota;

Ora, con STS, importate i seguenti progetti (seguite la procedura utilizzata per i progetti nella cartella <esempi>/spring-core):

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

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

Quindi importare nuovamente i tre progetti seguenti:

  • in [2]; i progetti si troveranno nella cartella [<exemples>/spring-database-config/spring-jdbc] [1];

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


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

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

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

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

Dovreste ottenere i seguenti risultati in 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 che seguono, il lettore potrà:

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

3.2. Le fasi di gestione di un database

Nell'architettura sopra riportata, l'utilizzo di un database da parte del programma da console prevede le seguenti fasi:

  1. caricamento del driver JDBC del database;
  1. apertura di una connessione con il database;
  2. invio di un comando SQL al database ed elaborazione dei risultati del comando SQL;
  3. chiusura della connessione;

La fase 1 viene eseguita una sola volta. Le fasi 2-4 vengono ripetute. Si noti che non si lascia aperta una connessione. La si chiude non appena non è più necessaria.

3.2.1. fase 1 - caricamento in memoria del driver JDBC

Il codice


        // caricamento del driver JDBC
        try {
            Class.forName(nom de la classe du pilote JDBC);
        } catch (ClassNotFoundException e1) {
             // gestione dell'eccezione
}

L'operazione della riga 3 ha lo scopo di caricare in memoria il driver JDBC dal database. Questa operazione deve essere eseguita una sola volta. Ripeterla, tuttavia, non causa alcun errore. La classe del driver JDBC viene cercata nel Classpath del progetto. È quindi necessario che nel progetto Eclipse il file [jar], contenente la classe del driver JDBC, sia stato incluso nel Classpath del progetto.

3.2.2. Fase 2 - Apertura di una connessione

Una volta installato il driver JDBC, gli viene richiesto di aprire una connessione con il driver BD:

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 {
            // apertura della connessione
            connexion = DriverManager.getConnection(url, user, passwd);
...
        } catch (SQLException e1) {
            // gestione dell'eccezione
            ...
        } finally {
         // chiusura della connessione
         if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e2) {
                // gestire l'eccezione
                ...
            }
         }
}
  • righe 3-7: le classi di implementazione dell'interfaccia JDBC si trovano tutte nel pacchetto [java.sql]. Inoltre, in caso di errore, tutte generano un'eccezione di tipo [SQLException] (righe 19, 27). Questa eccezione deriva dalla classe [Exception] ed è un'eccezione cosiddetta «controllata»: è necessario inserire un blocco try/catch per gestirla oppure, in alternativa, non gestirla e indicare che il metodo lascia passare l'eccezione completando la firma del metodo con [throws SQLException];
  • riga 17, [DriverManager.getConnection] è un metodo statico che richiede tre parametri:
    • [url]: il URL del database. Si tratta di una stringa di caratteri che dipende dal BD utilizzato. Per MySQL, ha la forma [jdbc:mysql://localhost:3306/nom_de_la_bd];
    • [user]: il proprietario della connessione;
    • [passwd]: la sua password;
  • righe 24-30: la connessione deve essere chiusa nella clausola [finally], in modo che venga chiusa indipendentemente dal verificarsi o meno di un'eccezione.

3.2.3. Fase 3 - Emissione dei comandi SQL e [SELECT]

Una volta ottenuta la connessione, è possibile inviare i comandi SQL. La gestione dei comandi di lettura [SELECT] differisce da quella utilizzata per le operazioni di aggiornamento [UPDATE, INSERT, DELETE]. Iniziamo con i comandi SQL e [SELECT]:

Il codice


Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // apertura connessione
            connexion = DriverManager.getConnection(url, user, passwd);
            // inizio transazione
            connexion.setAutoCommit(false);
            // in modalità di sola lettura
            connexion.setReadOnly(true);
            // si legge la tabella [PRODUITS]
            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)));
            }
            // conferma transazione
            connexion.commit();
        } catch (SQLException e1) {
            // gestione dell'eccezione
             doCatchException(connexion,e1);
        } finally {
            // si gestisce il 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 comandi SQL che o vanno tutti a buon fine o falliscono tutti. Pertanto, in una transazione composta da N comandi SQL, se il comando I+1 fallisce, i precedenti I comandi verranno annullati. Per un’operazione di lettura, non è necessaria una transazione. Tuttavia, la creazione di una transazione in sola lettura può consentire ad alcuni SGBD di effettuare determinate ottimizzazioni;
  • riga 12: utilizzo di un [PreparedStatement]. Un [PreparedStatement] presenta normalmente parametri indicati dal carattere ?. In questo caso non ne ha. Un [PreparedStatement] è un comando preparato dal SGBD. Questa preparazione ha un costo e viene eseguita una sola volta. Successivamente, questo comando preparato viene eseguito dal SGBD con diversi parametri effettivi che andranno a sostituire i parametri formali ?. Da notare che è preferibile specificare le colonne desiderate piuttosto che utilizzare il simbolo * per ottenere tutte le colonne. Specificando il nome delle colonne è poi possibile ottenere i loro valori in base alla loro posizione nella query SELECT;
  • riga 13: esecuzione di [PreparedStatement]. Si recupera un oggetto di tipo [ResultSet];

Un oggetto di tipo [ResultSet] rappresenta una tabella, ovvero un insieme di righe e colonne. In un dato momento, si ha accesso solo a una riga della tabella, denominata riga corrente. Al momento della creazione iniziale del [ResultSet], non esiste una riga corrente. È necessario 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 sarà possibile tornare indietro per recuperarla.

La tabella [ResultSet] presenta colonne denominate labelCol1, labelCol2, ... specificate 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] verrà inserita in una colonna del [ResultSet] denominata [myId];
  • la colonna [NOM] verrà trasferita in una colonna del [ResultSet] denominata [myNom];
  • ...

Nell’esempio sopra riportato, gli identificatori [myCol] sono denominati etichette di colonna. In assenza di tali etichette, i nomi delle colonne del [ResultSet] dipendono dal SGBD. Quando il [SELECT] opera su una singola tabella, le etichette delle colonne saranno per impostazione predefinita i nomi delle colonne richiesti dal SELECT. Il problema si presenta quando il [SELECT] opera su più tabelle e in queste sono presenti 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 [PRODUITS] abbia una chiave esterna verso la tabella [CATEGORIES], rappresentata dalla relazione [Produits].CATEGORIE_ID --> [CATEGORIES].ID, e che le tabelle [PRODUITS] e [CATEGORIES] abbiano entrambe un campo [NOM]. In questo caso, i nomi assegnati nel [ResultSet] alle colonne [PRODUITS.NOM] e [CATEGORIES.NOM] dipendono dal SGBD. Per garantire la portabilità tra i file SGBD, è quindi necessario utilizzare qui le etichette delle colonne e si scriverà:


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

Per utilizzare i diversi campi della riga corrente del [ResultSet], sono disponibili i seguenti metodi:

Type getType("labelColi") 

per ottenere la colonna denominata «labelColi» della riga corrente e quindi la colonna del [SELECT] con tale etichetta. Type indica il tipo del campo coli. È possibile utilizzare i seguenti metodi [getType]: getInt, getLong, getString, getDouble, getFloat, getDate, ... Anziché 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 nella BD;
  • riga 19: la transazione viene convalidata (si dice anche «committata»). Ciò la conclude e libera le risorse che la transazione SGBD aveva mobilitato per essa;
  • riga 25: le risorse vengono liberate nella transazione [finally]. Quest’ultima richiama il metodo [doFinally] seguente:

private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // chiusura ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // chiusura [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        if (connexion != null) {
            try {
                // chiudere la connessione
                connexion.close();
            } catch (SQLException e3) {
                 // gestire l'eccezione
            }
        }
    }
  • righe 3-9: chiusura del [ResultSet];
  • righe 11-17: chiusura di [PreparedStatement];
  • righe 18-27: chiusura della connessione;

Le chiusure alle righe 3-17 sembrano ridondanti, dato che la connessione viene chiusa alle righe 18-25. In realtà, in alcuni casi non lo sono 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) {
        // annullamento della transazione
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // gestire l'eccezione
        }
}
  • righe 4-6: la transazione viene annullata. Ciò la conclude e il metodo SGBD potrà liberare le risorse impiegate per essa;

3.2.4. fase 3 - emissione degli ordini SQL e [INSERT, UPDATE, DELETE]

Gli ordini SQL e [INSERT, UPDATE, DELETE] sono operazioni di aggiornamento: modificano il database ma non restituiscono alcuna riga. L’unica informazione fornita è il numero di righe interessate dall’operazione di aggiornamento.

Il codice


Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // apertura della connessione
            connexion = DriverManager.getConnection(url, user, passwd);
            // inizio transazione
            connexion.setAutoCommit(false);
            // in modalità lettura/scrittura
            connexion.setReadOnly(false);
            // si aggiorna la tabella
            ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
            // categoria 1
            ps.setInt(1, 10);
            // esecuzione
            int nbLignes=ps.executeUpdate();
            // conferma transazione
            connexion.commit();
        } catch (SQLException e1) {
            // gestiamo l'eccezione
            doCatchException(connexion, e1);
        } finally {
            // si gestisce il finally
            doFinally(null, ps, connexion);
        }
    }
  • riga 9: la connessione viene utilizzata in lettura e scrittura;
  • riga 11: un [PreparedStatement] con 1 parametro (simboleggiato da ?). È possibile avere più parametri. Sono numerati a partire da 1;
  • riga 13: si assegna il valore all’unico parametro. Il primo parametro di [setType] è la posizione del parametro nel [PreparedStatement] (1, 2, ...) e il secondo è il valore ad esso attribuito. È possibile utilizzare i metodi [setInt, setLong, setFloat, setDouble, setString, setDate, ...];
  • riga 15: si utilizza il metodo [executeUpdate] e non [executeQuery], riservato agli ordini SELECT. Il metodo restituisce il numero di righe interessate dall'operazione. Può essere 0.
  • riga 17: la transazione viene convalidata;

3.2.5. fase 4 - chiusura della connessione

In un contesto multiutente, una connessione deve essere chiusa il più rapidamente possibile, poiché un SGBD accetta un numero limitato di connessioni aperte. Negli esempi precedenti, veniva 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 del SGBD MySQL5

Esamineremo il progetto [mysql-config-jdbc] che configura il livello JDBC riportato di seguito:

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>
        <!-- dipendenze variabili ********************************************** -->
        <!-- driver JDBC del SGBD -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- dipendenze costanti ********************************************** -->
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- libreria 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>
        <!-- Test di Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!-- log -->
        <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>

In questa configurazione Maven sono stati raggruppati alcuni archivi necessari sia al progetto [mysql-config-jdbc] sia ai progetti che si baseranno su di esso:

  • righe 4-6: l’artefatto Maven generato dal progetto. Come già detto, tutti i progetti del tipo [*-config-jdbc] generano lo stesso artefatto. È quindi necessario che non vengano caricati contemporaneamente due progetti del tipo [*-config-jdbc];
  • 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 specificarle nei progetti che ne derivano;
  • righe 18-21: l’archivio del driver JDBC dei progetti SGBD e MySQL5. È l’unico archivio necessario al progetto [spring-jdbc-01];
  • righe 24-27: l'artefatto [tomcat-jdbc] fornisce un archivio necessario ai progetti JDBC e [spring-jdbc-02 à 04];
  • righe 29-36: forniscono le librerie necessarie alla gestione di jSON. Utilizzate in quasi tutti i progetti del documento;
  • righe 38-42: Google Guava è una libreria per la gestione delle collezioni. Utilizzata in quasi tutti i progetti del documento;
  • righe 43-52: le librerie che consentono la scrittura di test che integrano Spring e JUnit. Utilizzate in quasi tutti i progetti del documento;
  • righe 54-57: le librerie di log. Utilizzate in quasi tutti i progetti del documento;
  • righe 67-71: il plugin che consente di installare l'artifact 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 {

    // parametri di connessione
    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";
...
    // comandi 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')";

    // ordini 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] serve a configurare il livello JDBC dei quattro progetti [spring-jdbc-01 à 04]. La maggior parte della configurazione riguarda il progetto [spring-jdbc-04]. Tratteremo questa parte quando analizzeremo tale progetto. Quanto riportato sopra riguarda esclusivamente la configurazione dei progetti [spring-jdbc-01 à 03].

  • righe 14-17: i parametri di connessione al database MySQL5 [dbproduits];
  • righe 20-25: i comandi SQL utilizzati nei progetti [spring-jdbc-01 et 02];
  • righe 28-34: i comandi SQL utilizzati nel progetto [spring-jdbc-03];

Questi comandi SQL utilizzano la tabella [PRODUITS] del database MySQL5 [dbproduits], la cui struttura è la seguente:

 
  • [ID]: chiave primaria in modalità AUTO_INCREMENT (se non viene specificata una chiave primaria, SGBD la genera);
  • [NOM]: nome di un prodotto - univoco;
  • [CATEGORIE]: numero della categoria;
  • [PRIX]: il suo prezzo;
  • [DESCRIPTION]: una descrizione del prodotto;

3.3.4. La classe [Produit]

  

La classe [Produit] rappresenta l'immagine di una riga della tabella [PRODUITS]:


package generic.jdbc.entities.dbproduits;

public class Produit {

    // campi
    private int id;
    private String nom;
    private int categorie;
    private double prix;
    private String description;

    // costruttori
    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;
    }

    // getter e setter
...
}

In seguito avremo bisogno di confrontare due prodotti per verificare se sono uguali o meno. Diremo che due prodotti sono uguali se tutti i loro campi sono uguali. A tal fine, ridefiniremo il metodo [equals] della classe [Object] da cui deriva la classe [Produit]:


    // metodo di uguaglianza
    @Override
    public boolean equals(Object o) {
        // casi semplici
        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 si può stabilire immediatamente che i due oggetti non sono uguali. [Object].getClass() fornisce un'istanza del 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 tratta fisicamente dello stesso prodotto;
  • riga 9: se o e p sono due riferimenti diversi a due prodotti con gli stessi campi, si dirà che sono uguali. Poiché il prezzo è di tipo [double] e non esiste una rappresentazione esatta dei numeri reali in informatica, considereremo che due prezzi sono identici se differiscono di non più di 10⁻⁶;

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


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

I valori di hashCode di due prodotti devono essere uguali se il metodo [equals] ha dichiarato questi due prodotti uguali. Questo valore di hashCode viene utilizzato per distribuire oggetti in insiemi quali i dizionari. Come indicato sopra, se due prodotti sono identici, avranno lo stesso valore di hashCode.

3.3.5. L'eccezione [UncheckedException]

  

Consideriamo la seguente struttura:

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

Il livello [DAO] potrebbe limitarsi a far risalire il [SQLException] fino al livello di test. Ma poiché questa eccezione è non controllata (deriva direttamente da [Exception]), ciò implicherebbe che l'interfaccia [IDao] del livello [DAO] sia la seguente:


public interface IDao {

    // aggiungere prodotti
    public List<Produit> addProduits(List<Produit> produits) throws SQLException;

    // elenco di tutti i prodotti
    public List<Produit> getAllProduits() throws SQLException;

    // un prodotto specifico
    public Produit getProduitById(int id) throws SQLException;

    public Produit getProduitByName(String name) throws SQLException;

    // aggiornamento di più prodotti
    public int updateProduits(List<Produit> produits) throws SQLException;

    // eliminazione di tutti i prodotti
    public int deleteAllProduits() throws SQLException;

    // eliminazione di più prodotti
    public int deleteProduits(int[] ids) throws SQLException;
}

E questo è molto fastidioso perché ci impedisce di implementare l’interfaccia [IDao] tramite una classe che genererebbe un’eccezione diversa. Per aggirare questa difficoltà, il livello [DAO] genererà un'eccezione [DaoException] non controllata (derivata da [RuntimeException]), il che ci evita la clausola [throws] nella firma dei metodi dell’interfaccia. Di conseguenza, quest’ultima potrà essere implementata da qualsiasi classe che generi a sua volta un’eccezione non controllata, che potrà essere diversa dall’eccezione [DaoException]. La nostra architettura diventa ora la seguente:

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

  

package generic.jdbc.infrastructure;

import java.util.ArrayList;
import java.util.List;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

// classe di eccezione generica
// l'eccezione è non controllata

public class UncheckedException extends RuntimeException {

    // seriale ID generato
    private static final long serialVersionUID = -2924871763340170310L;

    // proprietà
    private int code;
    private String trace;
    private List<ShortException> exceptions;

    // costruttori
    public UncheckedException() {
        super();
    }

    public UncheckedException(int code, Throwable e, String simpleClassName) {
        super(e);
        // locale
        this.code = code;
        this.exceptions = getErreursForException(e);
        // traccia
        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);
    }

    // elenco dei messaggi di errore di un'eccezione
    private List<ShortException> getErreursForException(Throwable th) {
        // si recuperano gli elementi dello stack dell'eccezione
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // recupera l'eccezione corrente
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // eccezione successiva
            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;
        }
    }

    // getter e setter
...
}
  • riga 12: la classe deriva da [RuntimeException] ed è quindi un tipo di eccezione non controllata. Servirà a incapsulare un'eccezione controllata (SQLException) in un tipo di eccezione non controllata (UncheckedException);
  • per distinguere tra le diverse eccezioni di tipo [UncheckedException], sarà possibile assegnare loro un codice che verrà memorizzato nel campo privato della riga 18. Un codice Java che intercetta un'eccezione di tipo [UncheckedException] avrà accesso a questo codice di errore tramite il metodo [getCode] (righe 80 e successive);
  • riga 20: memorizza i messaggi di errore dello 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 derivato, in particolare il tipo [Exception];
  • righe 69-78: il metodo [toString] restituisce una stringa di caratteri che rappresenta l’eccezione. Per visualizzare l’elenco degli errori della riga 20, utilizza una libreria jSON. Questa è presente nelle dipendenze Maven del progetto:

        <!-- libreria 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]. Quest’ultimo restituisce qui 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] registra nel campo della riga 20 un elenco di eccezioni descritte dal seguente tipo [ShortException]:


package pam.dao.exceptions;

public class ShortException {

    // proprietà
    private String className;
    private String errorMessage;

    // costruttori
    public ShortException() {

    }

    public ShortException(String className, String errorMessage) {
        this.className = className;
        this.errorMessage = errorMessage;
    }

    // getter e setter
...
}
  • 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);
        // locale
        this.code = code;
        this.exceptions = getErreursForException(e);
        // traccia
        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: l'elenco di [ShortException] viene costruito a partire dal [Throwable e] passato come parametro;
  • righe 7-16: si esaminano le cosiddette tracce dell’eccezione. Un’eccezione iniziale si verifica in un punto preciso del codice, quindi risale al metodo che ha chiamato quello in cui si è verificata l’eccezione, e così via fino a quando un try/catch non la interrompe. In questa risalita, l’eccezione iniziale lascia delle tracce memorizzate nell’array [e.stackTrace] dell’eccezione e. Queste vengono qui ottenute alla riga 8, a partire dal [Throwable e] passato come parametro. Ogni elemento di tipo [StackTraceElement] è un oggetto che presenta, tra i propri campi, i seguenti:
    • [fileName]: il nome del file Java in cui si è verificata l'eccezione;
    • [lineNumber]: il numero di riga in quel file in cui si è verificata l'eccezione;
    • [methodName]: il nome del metodo all’interno di quel file in cui si è verificata l’eccezione;
  • le righe 10-16 cercano nell'array delle tracce dell'eccezione passata come parametro la prima occorrenza della condizione [trace.fileName==simpleClassName.java], dove [simpleClassName] è il terzo parametro del costruttore. L'idea è quella di memorizzare dove si è verificata l'eccezione nel codice utente. Quest'ultimo incapsulerà un'eccezione nel modo seguente:
1
2
3
4
5
6
7
try{
// codice che può generare un'eccezione controllata
...
}catch(UnTypeDexception e){
// si incapsula l'eccezione controllata e in un'eccezione non controllata
    throw new UncheckedException(189,e,getClass().getSimpleClassName())
}
  • riga 13: si crea una stringa di caratteri di tipo [fileName, methodName, lineNumber] che identifica il punto del codice utente in cui l’eccezione è stata intercettata;

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


    // elenco dei messaggi di errore di un'eccezione
    private List<ShortException> getErreursForException(Throwable th) {
        // si recuperano gli elementi dello stack dell'eccezione
        Throwable cause = th;
        List<ShortException> exceptions = new ArrayList<ShortException>();
        while (cause != null) {
            // si recupera l'eccezione corrente
            exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
            // eccezione successiva
            cause = cause.getCause();
        }
        return exceptions;
}

Durante la sua risalita verso il metodo che l’ha intercettata tramite un try/catch, l’eccezione iniziale e potrebbe essere stata incapsulata in un’altra eccezione. È quindi quest’ultima a risalire verso il metodo che la intercetterà definitivamente. Anche questa, quindi, può subire un incapsulamento. Alla fine, quando un metodo decide di intercettare un'eccezione th e di gestirla, troverà l'eccezione iniziale e sepolta in fondo a una pila di eccezioni. Pertanto, nell'esempio sopra riportato, il parametro [Throwable th] rappresenta solo la punta dell'iceberg delle eccezioni. Il suo attributo [th.cause] permette di identificare l’eccezione che essa stessa incapsula. E così via. Quando un’eccezione e ha il valore [e.getCause()==null], significa che e è l’eccezione iniziale.

  • riga 8: per ogni eccezione nella pila delle eccezioni di [Throwable th], vengono memorizzate due informazioni:
    • [getClass().getName()]: il nome completo dell'eccezione;
    • [getMessage()]: il messaggio di errore associato;

3.4. Exemple-01

3.4.1. L'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 nel paragrafo 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 /> <!-- ricerca del genitore dal repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- configurazione JDBC del 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];

È possibile verificare quest’ultimo punto in due modi, esaminando le dipendenze Maven del progetto:

  • in [2], si nota che il progetto [mysql-config-jdbc] è presente tra le dipendenze Maven del progetto. Poiché queste ultime si trovano nel Classpath del progetto, ciò significa che anche il progetto [mysql-config-jdbc] è presente in tale Classpath e che, 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 poter essere utilizzato da altri progetti Maven. È sufficiente che sia presente nel repository locale di Maven. A differenza di un progetto IDE come NetBeans, questa presenza non è automatica con Eclipse. È necessario forzarla:

Abbiamo visto le condizioni che rendono possibile questa generazione nel paragrafo 2.3.5. Una volta completata, è possibile rimuovere il progetto [mysql-config-jdbc] dalla scheda [Package Explorer]:

  • non bisogna selezionare [3], che elimina fisicamente il progetto dal disco rendendolo così irrecuperabile;

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

In questo caso, la dipendenza non riguarda più un progetto, ma l’artefatto Maven di quest’ultimo, ovvero l’artefatto [generic-config-jdbc] [1]. Si nota che si ha effettivamente accesso a tutte le classi e le interfacce di questo artefatto. Come già detto, questo artefatto verrà generato da tutti i progetti [*-config-jdbc]. Per evitare errori:

  • manterremo sempre un unico progetto [*-config-jdbc] nella scheda [Package Explorer];
  • aggiorneremo la configurazione Maven di tutti i progetti nella scheda [Package Explorer] (Alt-F5) affinché questi includano nelle loro dipendenze Maven il progetto [*-config-jdbc] utilizzato;

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 {

    // costanti
    final static ObjectMapper jsonMapper = new ObjectMapper();

    public static void main(String[] args) {
        // caricamento del driver JDBC del SGBD
        try {
            Class.forName(ConfigJdbc.DRIVER_CLASSNAME);
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // si svuota la tabella [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // la si riempie
        System.out.println(String.format("------------------------------ %s", "Remplissage de la table [PRODUITS]"));
        insert();
        // si legge
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // aggiornamento
        System.out.println(String.format("------------------------------ %s", "Mise à jour de la table [PRODUITS]"));
        update();
        // visualizzazione
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // si svuota la tabella [PRODUITS]
        System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
        delete();
        // visualizzazione
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // INSERTion di due elementi identici
        // l'operazione INSERTion deve fallire e nessuno dei due elementi viene inserito a causa della transazione
        System.out.println(String.format("------------------------------ %s",
                "Insertion de deux produits de même clé primaire dans la table [PRODUITS]"));
        insert2();
        // si verifica
        System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
        select();
        // finito
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }

    // elenco dei prodotti
    private static void select() {
    ...
    }

    // visualizzazione di un oggetto jSON
    private static void affiche(Object object) {
...
    }

    // eliminazione prodotti
    public static void delete() {
...
    }

    // aggiunta prodotti
    public static void insert() {
...
    }

    // aggiunta di 2 prodotti con le stesse chiavi primarie
    public static void insert2() {
...
    }

    // aggiornamento di alcuni prodotti
    public static void update() {
...
    }

    private static void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // chiusura ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // chiusura [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        if (connexion != null) {
            try {
                // chiudi la connessione
                connexion.close();
            } catch (SQLException e3) {
                // vengono visualizzati i messaggi di errore
                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) {
        // vengono visualizzati i messaggi di errore
        show(title, getErreursFromThrowable(th));
        // annullamento della transazione
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // visualizza i messaggi di errore
            show("Erreur lors de l'annulation de la transaction", getErreursFromThrowable(e2));
        }
    }

    private static List<String> getErreursFromThrowable(Throwable th) {
        // si recupera l'elenco dei messaggi di errore dell'eccezione
        List<String> erreurs = new ArrayList<String>();
        while (th != null) {
            // messaggio di errore del throwable
            erreurs.add(th.getMessage());
            // si passa alla causa del throwable
            th = th.getCause();
        }
        // risultato
        return erreurs;
    }

    private static void show(String title, List<String> messages) {
        // titolo
        System.out.println(String.format("%s : ", title));
        // messaggi
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }
}
  • righe 23-29: caricamento del driver JDBC da SGBD. Alla riga 25 si utilizza la costante [ConfigJdbc.DRIVER_CLASSNAME] definita nel progetto [mysql-config-jdbc];
  • righe 136-147: il metodo [getErreursFromThrowable] restituisce l'elenco dei messaggi di errore incapsulati in un oggetto di tipo [Throwable], che è la classe padre della classe [Exception]. Un'eccezione può contenerne un'altra, che si ottiene con il metodo [Throwable].getCause(). In questo modo si esaminano 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 connexion, Throwable th))] gestisce le eccezioni riscontrate dai metodi della classe. L'eccezione gestita è rappresentata dal parametro [Throwable th]. L'obiettivo del metodo è:
    • annullare la transazione in corso dell’oggetto [Connection connexion] (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 connexion)] gestisce il ramo [finally] dei metodi di accesso a SGBD. Il suo scopo è liberare le risorse utilizzate dalla connessione;

3.4.4. Eliminazione del contenuto della tabella dei prodotti

Il metodo [delete] elimina il contenuto della tabella:


    // eliminazione prodotti
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // apertura connessione
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // inizio transazione
            connexion.setAutoCommit(false);
            // in modalità lettura/scrittura
            connexion.setReadOnly(false);
            // si svuota la tabella [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V1_DELETE_PRODUITS);
            ps.executeUpdate();
            // conferma transazione
            connexion.commit();
        } catch (SQLException e1) {
            // gestione dell'eccezione
            doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
        } finally {
            // si gestisce il 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 = "";

Alla riga 13, l'ordine SQL preparato è il seguente:


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

Il metodo [delete] utilizza le transazioni. Una transazione consente di raggruppare gli ordini SQL che devono essere tutti eseguiti con successo o tutti annullati. È necessario conoscere quattro operazioni:

  • inizio di una transazione: [connexion.setAutoCommit(false)];
  • fine di una transazione con esito positivo: [connexion.commit()]. In questo caso, tutte le operazioni effettuate su BD durante la transazione vengono convalidate;
  • fine di una transazione con esito negativo: [connexion.rollback()]. In questo caso, tutte le operazioni effettuate su BD 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) {
        // visualizzazione dei messaggi di errore
        Static.show(title, Static.getErreursFromThrowable(th));
        // annullamento della transazione
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // visualizzazione dei messaggi di errore
            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 {
            // apertura della connessione
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // inizio transazione
            connexion.setAutoCommit(false);
            // in modalità lettura/scrittura
            connexion.setReadOnly(false);
            // compilazione della tabella
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_WITH_ID);
            for (int i = 0; i < 10; i++) {
                // preparazione
                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));
                // esecuzione
                ps.executeUpdate();
            }
            // conferma transazione
            connexion.commit();
        } catch (SQLException e1) {
            // gestione dell'eccezione
            doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
        } finally {
            // si gestisce il finally
            doFinally(null, ps, connexion);
        }
    }

Riga 12, l’ordine SQL preparato è il 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:


// elenco dei prodotti
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // apertura connessione
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // inizio transazione
            connexion.setAutoCommit(false);
            // in modalità di sola lettura
            connexion.setReadOnly(true);
            // si legge la tabella [PRODUITS]
            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)));
            }
            // conferma transazione
            connexion.commit();
        } catch (SQLException e1) {
            // gestione dell'eccezione
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // si gestisce il finally
            doFinally(rs, ps, connexion);
        }
    }

Riga 14, il comando SQL preparato è il seguente:


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

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


    // visualizzazione di un oggetto jSON
    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 (cfr. jSON, paragrafo 23.12).

3.4.7. Aggiornamento del contenuto della tabella

Il metodo [update] aggiorna alcuni prodotti:


    // aggiornamento di alcuni prodotti
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // apertura della connessione
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // inizio transazione
            connexion.setAutoCommit(false);
            // in modalità lettura/scrittura
            connexion.setReadOnly(false);
            // si aggiorna la tabella
            ps = connexion.prepareStatement(ConfigJdbc.V1_UPDATE_PRODUITS);
            // categoria 1
            ps.setInt(1, 1);
            // esecuzione
            ps.executeUpdate();
            // conferma transazione
            connexion.commit();
        } catch (SQLException e1) {
            // gestiamo l'eccezione
            doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
        } finally {
            // si gestisce il finally
            doFinally(null, ps, connexion);
        }
}

Riga 13, l'ordine SQL preparato è il 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, il che non è possibile. Poiché ci si trova all'interno di una transazione, il primo inserimento verrà annullato.


    // aggiunta di 2 prodotti con le stesse chiavi primarie
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // apertura connessione
            connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
            // inizio transazione
            connexion.setAutoCommit(false);
            // in modalità lettura/scrittura
            connexion.setReadOnly(false);
            // si aggiunge 1 riga
            ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_2);
            // esecuzione
            ps.executeUpdate();
            // si aggiunge la stessa riga una seconda volta, quindi con la stessa chiave primaria
            // la transazione INSERTion deve fallire e nessuno dei due elementi deve essere inserito a causa della transazione
            ps.executeUpdate();
            // conferma la transazione
            connexion.commit();
        } catch (SQLException e1) {
            // si gestisce l'eccezione
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire",
                    connexion, e1);
        } finally {
            // si gestisce il finally
            doFinally(null, ps, connexion);
        }
}

Riga 13, l’ordine SQL preparato è il 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

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

 

Si ottengono i seguenti risultati in 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 dell’inserimento dei due prodotti con la stessa chiave primaria, la tabella è vuota;
  • riga 35: dopo l'inserimento dei due prodotti con la stessa chiave primaria, la tabella è vuota. Ciò dimostra il ruolo della transazione:
    • il primo inserimento va a buon fine. Non c'è alcun motivo per cui debba fallire;
    • il secondo inserimento fallisce (riga 32). Di conseguenza, poiché questi due inserimenti fanno parte della stessa transazione, tutti gli ordini SQL di quest’ultima vengono annullati, compreso il primo inserimento.

3.4.10. Conclusione

Ciò che colpisce nei codici precedenti è l’ampio spazio dedicato alla gestione dell’eccezione [SQLException]. Poiché qualsiasi operazione JDBC può generarla, nel codice sono presenti numerosi blocchi try/catch.

3.5. Exemple-02

Riprendiamo l’applicazione precedente utilizzando una fonte di dati di tipo [javax.sql.DataSource]:

Image

Utilizzeremo 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. La fonte di dati verrà definita in una classe di configurazione Spring

3.5.1. L'architettura del progetto

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

3.5.2. Il progetto Eclipse

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

Si procede quindi a modificare il progetto da [6] a [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 /> <!-- ricerca del genitore dal repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- configurazione JDBC del 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 di dati di tipo [javax.sql.DataSource] (cfr. paragrafo 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 {
    // fonte dati
    @Bean
    public DataSource dataSource() {
        // fonte dati TomcatJdbc
        DataSource dataSource = new DataSource();
        // configurazione accesso JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // connessioni inizialmente aperte
        dataSource.setInitialSize(5);
        // risultato
        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 sono disponibili tutti i bean definiti da questo file di configurazione;
  • righe 14-27: il bean Spring che definisce la fonte dati;
  • riga 17: creazione della fonte dati, ancora non 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 {

    // mappatura jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();
    // fonte dati
    private static DataSource dataSource;

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = null;
        try {
            // recupero del contesto Spring
            ctx = new AnnotationConfigApplicationContext(AppConfig.class);
            // recupero della fonte dati
            dataSource = ctx.getBean(DataSource.class);
            // si svuota la tabella [PRODUITS]
            System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
            delete();
...
        // fine
        System.out.println(String.format("------------------------------ %s", "Travail terminé"));
    }

    // elenco dei prodotti
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // apertura connessione
            connexion = dataSource.getConnection();
            // inizio transazione
            connexion.setAutoCommit(false);
            // in modalità di sola lettura
            connexion.setReadOnly(true);
            // si legge la tabella [PRODUITS]
            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)));
            }
            // conferma transazione
            connexion.commit();
        } catch (SQLException e1) {
            // gestione dell'eccezione
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // si gestisce il finally
            doFinally(rs, ps, connexion);
        }
    }
...
  • riga 25: la fonte dati. Si noti che è di tipo [javax.sql.DataSource] (riga 13), che è un'interfaccia;
  • riga 31: istanziamento degli oggetti Spring;
  • riga 32: ottenimento di un riferimento alla fonte di dati. Si noti che in nessun momento viene citata la classe effettivamente utilizzata. Pertanto, in questo caso, nulla lascia supporre che si stia utilizzando un’implementazione [TomcatJdbc];
  • riga 49: ottenimento di una connessione aperta. È in questo modo che i diversi metodi di [IntroJdbc02] ottengono una connessione al database. Il resto del codice è identico a quello della classe [IntroJdbc01];

3.5.6. I test

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

 

Si ottengono gli stessi risultati di prima (paragrafo 3.4.9).

3.6. Exemple-03

3.6.1. L'architettura del progetto

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

3.6.2. Il progetto Eclipse

Il progetto Eclipse [spring-jdbc-03] è un progetto Spring / Maven realizzato come il precedente e poi completato nel modo seguente:

 

I diversi pacchetti hanno le seguenti funzioni:

  • [spring.jdbc.config]: configurazione del progetto Spring;
  • [spring.jdbc.dao]: implementazione del livello [DAO];
  • [spring.jdbc.infrastructure]: implementa l'eccezione non controllata [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 /> <!-- ricerca del genitore dal repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- configurazione JDBC del 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] presenta la seguente interfaccia [IDao]:


package spring.jdbc.dao;

import java.util.List;

import spring.jdbc.entities.Produit;

public interface IDao {

    // Aggiunta di prodotti
    public List<Produit> addProduits(List<Produit> produits);

    // elenco di tutti i prodotti
    public List<Produit> getAllProduits();

    // un prodotto specifico
    public Produit getProduitById(int id);

    public Produit getProduitByName(String name);

    // aggiornamento di più prodotti
    public int updateProduits(List<Produit> produits);

    // eliminazione di tutti i prodotti
    public int deleteAllProduits();

    // eliminazione di più prodotti
    public int deleteProduits(int[] ids);
}

3.6.5. La classe [DaoException]

La classe [DaoException] si limita ad estendere la classe [UncheckedException] presentata al paragrafo 3.3.5:

  

package spring.jdbc.infrastructure;

public class DaoException extends UncheckedException {

    private static final long serialVersionUID = 1L;

    // produttori
    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 dell'esempio [spring-jdbc-02], tranne che per la 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 {
    // fonte dati
    @Bean
    public DataSource dataSource() {
        // fonte dati TomcatJdbc
        DataSource dataSource = new DataSource();
        // configurazione dell'accesso JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
        // connessioni inizialmente aperte
        dataSource.setInitialSize(5);
        // risultato
        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]

  

Ricordiamo (paragrafo 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 {

    // aggiungere prodotti
    public List<Produit> addProduits(List<Produit> produits);

    // elenco di tutti i prodotti
    public List<Produit> getAllProduits();

    // un prodotto specifico
    public Produit getProduitById(int id);

    public Produit getProduitByName(String name);

    // aggiornamento di più prodotti
    public int updateProduits(List<Produit> produits);

    // eliminazione di tutti i prodotti
    public int deleteAllProduits();

    // eliminazione di più prodotti
    public int deleteProduits(int[] ids);
}

Entrambe le classi [Dao1, Dao2] implementano questa interfaccia. La classe [Dao2] è una variante della classe [Dao1] che introduce una novità sintattica. Ci concentreremo sulla classe [Dao1]. La struttura di quest’ultima è la 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 {

    // nome della classe
    private String simpleClassName = getClass().getSimpleName();
    // fonte dati
    @Autowired
    protected DataSource dataSource;

    // produttore
    public Dao1() {
        System.out.println("building Dao1...");
    }

    // ------------------------------- interfaccia
    @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) {
...
    }

    // ---------------------------------------- metodi locali
    // gestione finally
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        ...
    }

    // gestione catch
    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, viene utilizzato il nome della classe con la prima lettera maiuscola trasformata in minuscola;
  • riga 24: il nome della classe. Si evita di scrivere in modo fisso [Dao] per lasciare la possibilità di rinominare la classe senza dover ridefinire questo campo, che rimane così sempre valido;
  • righe 26-27: inserimento della fonte dati [tomcat-jdbc] definita nella classe di configurazione [AppConfig];
  • righe 36-68: implementazione dell'interfaccia [IDao];
  • righe 78-80: gestione centralizzata di catch dei diversi metodi;
  • righe 72-75: gestione centralizzata del finally dei diversi metodi;

Il catch dei diversi metodi viene gestito nel modo seguente:


    // gestione catch
    protected DaoException doCatchException(Connection connexion, Throwable th, int code) {
        // annullamento transazione
        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 necessariamente pubblico. Accetta i seguenti parametri:
    • [Connection connexion]: la connessione con SGBD – forse null;
    • [Throwable th]: l’eccezione che si è verificata e che verrà incapsulata in un tipo [DaoException];
    • [int code]: un codice di errore da utilizzare se il metodo crea un nuovo [DaoException];
  • righe 4-7: la funzione principale di questo metodo è quella di annullare la transazione associata alla connessione passata come parametro 1;
  • righe 8-10: se l’annullamento della transazione non va a buon fine, si scrive il log dell’eccezione sulla console. Non si può fare molto altro, dato che alla riga 12 verrà lanciata un’eccezione;

Il codice finally dei diversi metodi viene gestito nel modo seguente:


// gestione finally
    protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
            DaoException daoException) {
        // chiusura ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // chiusura [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        // chiusura connessione
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // si registra l'errore, se possibile
                if (daoException == null) {
                    daoException = new DaoException(code, e3, simpleClassName);
                }
            }
        }
        // risultato
        return daoException;
    }
  • riga 2: anche questo metodo è dichiarato come [protected]. Accetta i seguenti parametri:
    • [ResultSet rs]: l'eventuale [ResultSet] se è stata eseguita un'operazione [SELECT] - forse null;
    • [PreparedStatement ps]: il [PreparedStatement] che è stato eseguito – forse null;
    • [Connection connexion]: la connessione con SGBD - forse null;
    • [int code]: un codice di errore da utilizzare se il metodo crea un nuovo [DaoException];
    • [DaoException daoException]: l'eventuale [DaoException] se si è verificata un'eccezione prima del finally - forse null;
  • righe 21-30: l'obiettivo principale di questo metodo è chiudere la connessione (riga 23);
  • righe 24-29: se durante questa chiusura si verifica un'eccezione, allora si controlla lo stato del parametro [DaoException daoException] che ci è stato passato: se è [daoException == null], allora si crea un nuovo [DaoException] con il codice passato come parametro;
  • riga 32: come risultato viene restituito il vecchio o il nuovo [DaoException];

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

3.6.7.1. Il metodo [getProduitById]

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


@Override
    public Produit getProduitById(int id) {
        // risorse della connessione
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        // Inizialmente nessuna eccezione
        DaoException daoException = null;
        // il prodotto cercato
        Produit produit = null;
        try {
            // apertura della connessione
            connexion = dataSource.getConnection();
            // inizio transazione
            connexion.setAutoCommit(false);
            // in modalità di sola lettura
            connexion.setReadOnly(true);
            // si legge la tabella [PRODUITS]
            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));
            }
            // conferma transazione
            connexion.commit();
            // ritorno alla modalità predefinita
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // gestione dell'eccezione
            daoException = doCatchException(connexion, e1, 112);
        } finally {
            // si gestisce il finally
            daoException = doFinally(rs, ps, connexion, 113, daoException);
        }
        // eccezione?
        if (daoException != null) {
            throw daoException;
        }
        // risultato
        return produit;
    }
  • riga 10: il prodotto da restituire viene impostato su null;
  • riga 19: l'ordine SQL [ConfigJdbc.V2_SELECT_PRODUIT_BYID] è il seguente:

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

  • righe 22-24: se [ResultSet] ha una riga, la si utilizza per creare il prodotto da restituire, altrimenti il prodotto da restituire rimane null;
  • riga 41: si restituisce il prodotto;
  • riga 8: l'eccezione [DaoException] del metodo viene inizializzata a null;
  • riga 31: il metodo [doCatchException] genera un'eccezione [DaoException];
  • riga 34: il parametro [daoException] del metodo [doFinally] è o null, oppure l'eccezione generata dal metodo [doCatchException]. Il metodo [doFinally]:
    • lascia questo parametro invariato se riesce a chiudere la connessione;
    • lascia questo parametro invariato se non riesce a chiudere la connessione e se in precedenza è già stato generato un [DaoException];
    • crea un nuovo [DaoException] se non riesce a chiudere la connessione e non è stato eseguito in precedenza un [DaoException];
  • righe 37-39: se l'eccezione locale [daoException] non è uguale a null, allora viene generata; altrimenti viene restituito il risultato richiesto (riga 41);

3.6.7.2. Il metodo [deleteProduits]

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


@Override
    public int deleteProduits(int[] ids) {
        // risorse della connessione
        PreparedStatement ps = null;
        Connection connexion = null;
        // all'inizio nessuna eccezione
        DaoException daoException = null;
        // numero di prodotti aggiornati
        int nbProduits = 0;
        try {
            // apertura connessione
            connexion = dataSource.getConnection();
            // inizio transazione
            connexion.setAutoCommit(false);
            // in modalità lettura/scrittura
            connexion.setReadOnly(false);
            // si eliminano i prodotti
            ps = connexion.prepareStatement(ConfigJdbc.V2_DELETE_PRODUITS);
            for (int id : ids) {
                // parametri
                ps.setInt(1, id);
                // esecuzione
                nbProduits += ps.executeUpdate();
            }
            // conferma transazione
            connexion.commit();
            // ritorno alla modalità predefinita
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // gestione dell'eccezione
            daoException = doCatchException(connexion, e1, 171);
        } finally {
            // gestione del blocco finally
            daoException = doFinally(null, ps, connexion, 172, daoException);
        }
        // eccezione?
        if (daoException != null) {
            throw daoException;
        }
        // risultato
        return nbProduits;
    }
  • riga 18, l'ordine SQL [ConfigJdbc.V2_DELETE_PRODUITS] è il seguente:

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

  • righe 18-24: il codice di eliminazione dei prodotti. Si nota che l’ordine SQL viene preparato una volta (riga 18) ed eseguito 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: viene restituito il numero di prodotti aggiornati;

3.6.7.3. Il metodo [updateProduits]

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) {
        // risorse della connessione
        PreparedStatement ps = null;
        Connection connexion = null;
        // all'inizio nessuna eccezione
        DaoException daoException = null;
        // numero di prodotti aggiornati
        int nbProduits = 0;
        try {
            // apertura connessione
            connexion = dataSource.getConnection();
            // inizio transazione
            connexion.setAutoCommit(false);
            // in modalità lettura/scrittura
            connexion.setReadOnly(false);
            // si aggiorna la tabella [PRODUITS]
            ps = connexion.prepareStatement(ConfigJdbc.V2_UPDATE_PRODUITS);
            for (Produit produit : produits) {
                // parametri
                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());
                // esecuzione
                nbProduits += ps.executeUpdate();
            }
            // conferma transazione
            connexion.commit();
            // ritorno alla modalità predefinita
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // gestione dell'eccezione
            daoException = doCatchException(connexion, e1, 131);
        } finally {
            // gestione del blocco finally
            daoException = doFinally(null, ps, connexion, 132, daoException);
        }
        // eccezione?
        if (daoException != null) {
            throw daoException;
        }
        // risultato
        return nbProduits;
    }
  • riga 18: l'ordine SQL [ConfigJdbc.V2_UPDATE_PRODUITS] è il seguente:

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

3.6.7.4. Il metodo [addProduits]

Il metodo [addProduits] inserisce nel database i prodotti che gli vengono passati come parametri. Restituisce questi stessi prodotti con le loro chiavi primarie (prima dell’inserimento nel database, i prodotti non hanno una chiave primaria).


@Override
    public List<Produit> addProduits(List<Produit> produits) {
        // risorse della connessione
        PreparedStatement ps = null;
        Connection connexion = null;
        // all'inizio nessuna eccezione
        DaoException daoException = null;
        try {
            // apertura della connessione
            connexion = dataSource.getConnection();
            // in modalità lettura/scrittura
            connexion.setReadOnly(false);
            // inizio transazione
            connexion.setAutoCommit(false);
            // si aggiungono elementi alla tabella [PRODUITS]
            String generatedColumns[] = { ConfigJdbc.TAB_PRODUITS_ID };
            ps = connexion.prepareStatement(ConfigJdbc.V2_INSERT_PRODUITS, generatedColumns);
            for (Produit produit : produits) {
                // parametri
                ps.setString(1, produit.getNom());
                ps.setLong(2, produit.getCategorie());
                ps.setDouble(3, produit.getPrix());
                ps.setString(4, produit.getDescription());
                // esecuzione comando
                ps.executeUpdate();
                // chiave primaria generata
                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()));
                }
            }
            // conferma transazione
            connexion.commit();
            // ritorno alla modalità predefinita
            connexion.setAutoCommit(true);
        } catch (SQLException | RuntimeException e1) {
            // elaborazione dell'eccezione
            daoException = doCatchException(connexion, e1, 151);
        } finally {
            // elaborazione del blocco finally
            daoException = doFinally(null, ps, connexion, 152, daoException);
        }
        // eccezione?
        if (daoException != null) {
            throw daoException;
        }
        // risultato
        return produits;
}
  • riga 16, l'ordine SQL [ConfigJdbc.V2_INSERT_PRODUITS] è il seguente:

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

Nell’esempio sopra riportato, il comando di inserimento di un prodotto non include la chiave primaria [ID]. Poiché la chiave primaria del database MySQL ha l’attributo [AUTOINCREMENT], il SGBD genererà quindi una chiave primaria per ogni inserimento. Si pone quindi il problema di recuperarla. Si tratta di un aspetto importante poiché le operazioni sui prodotti avvengono tramite le loro chiavi primarie. È quindi necessario conoscerle;

  • righe 17-33: il ciclo di inserimento dei prodotti;
  • riga 16: una variante specifica del metodo [prepareStatement]. Il secondo parametro [generatedColumns] è un array contenente i nomi delle colonne di cui si desidera recuperare i valori dopo l'inserimento. Alla riga 16 abbiamo specificato che volevamo recuperare il valore della colonna [id]. Si noti qui che, sebbene i nomi delle colonne di una tabella non facciano distinzione tra maiuscole e minuscole, il SGBD PostgreSQL ha richiesto che questo nome fosse in minuscolo. Si tratta tipicamente del tipo di problema che si riscontra durante il porting di un codice da un SGBD a un altro;
  • riga 24: inserimento di una riga nel database;
  • riga 26: si recupera l'elenco dei valori delle colonne specificate alla riga 16 in un [ResultSet]. In questo caso, per 1 inserimento, il [ResultSet] avrà 1 riga e questa riga avrà un'unica colonna contenente la chiave primaria;
  • riga 28: si recupera la chiave primaria generata dal SGBD;
  • righe 29-32: se non si ottiene la chiave primaria generata, si avvia un [RuntimeException] che verrà incapsulato in un [DaoException] alle 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 rilasciate con il metodo [close] ne fanno parte. 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 ad esempio il metodo [getAllProduits] della classe [Dao2]:


    @Override
    public List<Produit> getAllProduits() {
        // eventuale eccezione
        DaoException daoException = null;
        // elenco dei prodotti
        List<Produit> produits = new ArrayList<Produit>();
        try (Connection connexion = dataSource.getConnection()) {
            // inizio transazione
            connexion.setAutoCommit(false);
            // in modalità di sola lettura
            connexion.setReadOnly(true);
            // si legge la tabella [PRODUITS]
            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)));
                    }
                }
                // fine transazione
                connexion.commit();
                // ritorno alla modalità predefinita
                connexion.setAutoCommit(true);
            } catch (SQLException e1) {
                // si annulla la transazione
                daoException = doRollback(connexion, e1, 111);
            }
        } catch (SQLException e2) {
            // elaborazione dell'eccezione
            if (daoException == null) {
                daoException = new DaoException(112, e2, simpleClassName);
            }
        }
        // eccezione?
        if (daoException != null) {
            throw daoException;
        }
        // risultato
        return produits;
}
  • riga 7: try con la risorsa [Connection]. Alla riga 27, si ha la certezza che questa sia chiusa;
  • riga 13: blocco try con la risorsa [PreparedStatement]. Alla riga 23, si ha la certezza che questa sia chiusa;
  • riga 14: try con la risorsa [ResultSet]. Alla riga 19, si ha la certezza che questa sia chiusa;
  • riga 25: la transazione viene annullata nel modo seguente:

    private DaoException doRollback(Connection connexion, Throwable e1, int code) {
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        // generazione dell'eccezione
        return new DaoException(code, e1, simpleClassName);
}

Alla fine, si ottiene un codice più semplice da leggere.

3.6.9. Implementazione del livello di test

3.6.9.1. Le classi di test

  
  • il test [JUnitTestDao1] è un test JUnit della classe [Dao1];
  • il test [JUnitTestDao2] è un test JUnit della classe [Dao2];
  • [AbstractJUnitTestDao] è la classe padre delle due classi di test precedenti;
  • [MainTestDao1] è una classe di test console della classe [Dao1];
  • [MainTestDao2] è una classe console di test della classe [Dao2];
  • [AbstractMainTestDao] è la classe padre delle due classi precedenti. Essa riprende il codice delle classi console [IntroJdbc01, IntroJdbc02] già presentate, pertanto non esamineremo tali 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 {

    // livello [DAO]
    @Autowired
    @Qualifier("dao1")
    private IDao dao;

    @Override
    IDao getDao() {
        return dao;
    }

}
  • le annotazioni delle righe 12-13 sono state illustrate nel paragrafo 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) analizzata nel paragrafo 2.4.3;
  • riga 14: la classe estende la classe [AbstractJUnitTestDao] che presenteremo di seguito. È in questa classe che si trovano i metodi di test JUnit;
  • righe 17-19: il bean denominato [dao1] (riga 18) viene iniettato (riga 17). Si tratta quindi di un’istanza della classe [Dao1] che viene qui iniettata;
  • righe 21-24: il metodo [getDao] ridefinisce il metodo omonimo nella classe padre;

In definitiva, lo scopo di questa classe è quello di restituire alla classe padre un riferimento al livello [DAO] che deve essere testato, in questo caso un'istanza di [Dao1]. Allo stesso modo, la classe [JUnitTestDao2] restituisce alla classe padre [AbstractJUnitTestDao] un'istanza della classe [Dao2].

La classe [AbstractJUnitTestDao] è una classe di test di 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 {

    // livello [DAO]
    abstract IDao getDao();

    // mappatura jSON
    final static ObjectMapper jsonMapper = new ObjectMapper();

    @Before
    public void clean() {
        // si pulisce la base prima di ogni 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() {
    ....
    }

    // -------------- metodi privati
...
}
  • riga 19, la classe [AbstractJUnitTestDao] è astratta;
  • riga 22: il metodo astratto [getDao] che consente di ottenere il riferimento al livello [DAO] da testare. Questo metodo è implementato dalle classi figlie;
  • riga 25: un mappatore jSON che ci consentirà di visualizzare sulla console il valore jSON dei prodotti;
  • righe 27-32: prima di ogni test (riga 27), la tabella [PRODUITS] viene svuotata;

3.6.9.2. Il metodo privato [fill]

Il metodo privato [fill] viene utilizzato per inserire i prodotti nella tabella [PRODUITS].


private List<Produit> fill(int nbProduits) {
        log("Remplissage de la base de données", 1);
        // si crea un elenco di prodotti
        List<Produit> produits = new ArrayList<Produit>();
        for (int i = 0; i < nbProduits; i++) {
            int n = i + 1;
            // int id, String nome, int categoria, double prezzo, String descrizione
            produits.add(new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format(
                    "DESC%s", n)));
        }
        // si salva il listato nel database - si recuperano i prodotti tramite la loro chiave primaria
        produits = getDao().addProduits(produits);
        // si crea un dizionario dei prodotti per poterli ritrovare più facilmente
        // la chiave del dizionario è la chiave primaria del prodotto nel database
        for (Produit produit : produits) {
            mapProduits.put(produit.getId(), produit);
        }
        // si restituiscono i prodotti
        return produits;
    }
  • riga 1: il metodo [fill] inserisce [nbProduits] nella tabella [PRODUITS], che si suppone 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 Prodotto(int id, String nome, int categoria, double prezzo, String descrizione). Il valore del primo parametro [id] (chiave primaria della tabella [PRODUITS]) non ha importanza poiché il metodo [addProduits] della riga 10 non lo inserisce nel database e lascia che sia SGBD a generare il suo valore;

  • riga 12: l'elenco dei prodotti viene salvato nel database. Ciascuno dei prodotti di questo elenco viene arricchito con una nuova chiave primaria [id]. Il metodo [addProduits] restituisce come risultato il proprio parametro [produits]. Si sarebbe quindi potuto non recuperare il risultato;
  • righe 15-17: si inseriscono i prodotti in un dizionario:

    // nel dizionario dei prodotti
    private Map<Integer, Produit> mapProduits = new HashMap<Integer, Produit>();

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

  • riga 19: si restituisce l'elenco dei prodotti;

3.6.9.3. Il test [getProduits]

È il seguente:


    @Test
    public void getProduits() throws JsonProcessingException {
        // compilazione
        fill(10);
        // elenco dei prodotti
        log("Liste des produits", 2);
        List<Produit> produits = getDao().getAllProduits();
        affiche(produits);
        // si verifica che l'elenco recuperato e quello salvato siano identici
        for (Produit produit : produits) {
            Produit found = mapProduits.get(produit.getId());
            Assert.assertEquals(found, produit);
            mapProduits.remove(found.getId());
        }
        // tutti i prodotti iniziali devono essere stati rimossi dal dizionario
        Assert.assertEquals(0, mapProduits.size());
}
}
  • riga 4: vengono inseriti 10 prodotti nel database;
  • riga 7: una volta fatto ciò, si richiede di visualizzare tutti i prodotti presenti nel database;
  • riga 8: li visualizziamo. Lo scopo è verificare che i prodotti siano stati correttamente salvati e che abbiano una chiave primaria;
  • righe 10-13: si verifica che i prodotti recuperati siano identici a quelli salvati e che siano reperibili nel dizionario [mapProduits];
  • riga 11: si recupera dal dizionario il prodotto con la stessa chiave primaria di quello recuperato dal database. Ciò dimostra che ai prodotti salvati è stata effettivamente assegnata una chiave primaria;
  • riga 12: ci si assicura che i due prodotti siano identici. Si ricorda che la classe [Produit] ha definito un metodo [equals] (cfr. paragrafo 3.3.4);
  • riga 13: si elimina dal dizionario l’elemento trovato;
  • riga 16: si verifica che il dizionario dei prodotti iniziali sia effettivamente vuoto, il che significa che tali prodotti iniziali erano tutti presenti nell’elenco dei prodotti recuperati dal database;

Il metodo [affiche] della riga 8 è il seguente metodo privato:


    // visualizzazione dell'elenco dei prodotti
    private <T> void affiche(List<T> elements) throws JsonProcessingException {
        for (T element : elements) {
            System.out.println(jsonMapper.writeValueAsString(element));
        }
}
  • riga 2: il metodo [affiche] è un metodo generico. È parametrizzato da un tipo T, indicato sintatticamente come <T>. Se fosse parametrizzato da due tipi, T1 e T2, si scriverebbe <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 si troveranno dati di tipo T. Il metodo m di un'istanza c di una classe C può quindi essere chiamato nel modo seguente:

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 in base agli argomenti del metodo m. Pertanto, l’istruzione precedente verrà spesso semplificata in:

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

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

Il metodo [affiche] avrebbe potuto essere scritto anche nel modo seguente:


    // visualizzazione dell'elenco dei prodotti
    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 generato la rappresentazione jSON di tale elenco. Ciò non equivale a generare, una per una, la rappresentazione di ciascuno dei suoi elementi.

Il risultato visualizzato dal test [getProduits] è 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 [getProduitBy]

Questo è il seguente:


    @Test
    public void getProduitBy() {
        // compilazione
        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 [getProduitByName] dell'interfaccia [IDao] viene utilizzato per recuperare il prodotto denominato [NOM3];
  • riga 7: il metodo [getProduitById] dell'interfaccia [IDao] viene quindi utilizzato per recuperare lo stesso prodotto, questa volta identificato tramite la sua chiave primaria;
  • righe 8-10: si verifica che [produit2] e [produit] 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);
        // si procede all’inserimento
        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;
        }
        // verifiche
        Assert.assertTrue(erreur);
        List<Produit> produits = getDao().getAllProduits();
        Assert.assertEquals(0, produits.size());
}
  • righe 5-7: si crea un elenco di due prodotti con lo stesso nome [x];
  • riga 10: questi due prodotti vengono inseriti nella tabella [PRODUITS], che è vuota (metodo [clean] annotato con [@Before]). Il primo inserimento verrà eseguito, ma non il secondo, poiché la tabella [PRODUITS] presenta un vincolo di unicità sul nome dei prodotti. Deve quindi verificarsi un'eccezione. Questa viene verificata alla riga 15;
  • poiché tutti i metodi dell’interfaccia [IDao] vengono eseguiti all’interno di una transazione, il fatto che il secondo inserimento fallisca comporterà l’annullamento dell’intera transazione e quindi anche del primo inserimento. Alla fine, non deve avvenire alcun inserimento nella tabella [PRODUITS];
  • righe 16-17: si verifica questo punto richiedendo l’elenco dei prodotti contenuti nella tabella [PRODUITS] e verificando che tale elenco sia vuoto;

3.6.9.6. Il test [updateProduits]

Il test è il seguente:


    @Test
    public void updateProduits() {
        // compilazione
        fill(10);
        log("Mise à jour du prix des produits de catégorie 1", 1);
        // recupero dei prodotti
        List<Produit> produits = getDao().getAllProduits();
        // si aggiornano quelli della categoria 1
        List<Produit> updated = new ArrayList<Produit>();
        int nbUpdated = 0;
        for (Produit produit : produits) {
            if (produit.getCategorie() == 1) {
                // int id, String nome, int categoria, double prezzo, String descrizione
                updated
                        .add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
                nbUpdated++;
            }
        }
        int nbProduits = getDao().updateProduits(updated);
        // verifiche
        // Assert.assertEquals(nbUpdated, nbProduits); -- non funziona con DB2
        for (Produit produit : updated) {
            Produit produit2 = getDao().getProduitById(produit.getId());
            Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
        }
}
  • riga 4: si inseriscono 10 prodotti nel database;
  • riga 7: li si recupera;
  • righe 9-18: si aumenta del 10% il prezzo dei prodotti della categoria n. 1;
  • riga 19: queste modifiche vengono salvate nel database;
  • righe 22-25: si scorre in memoria l'elenco dei prodotti utilizzato per l'aggiornamento. Per ciascuno di essi, si cerca nel database il prodotto con la stessa chiave primaria e si verifica che l'aggiornamento del prezzo sia stato effettivamente effettuato;
  • riga 19: si recupera il numero di prodotti aggiornati dall’operazione [updateProduits];
  • riga 21: si verifica che tale numero corrisponda effettivamente a quello previsto. Questo test ha esito positivo per tutti i SGBD tranne che per SGBD e DB2. Pertanto, è stato messo in commento;

3.6.9.7. Il test [deleteProduits]

Questo è il seguente:


    @Test
    public void deleteProduits() {
        // riempimento
        fill(10);
        log("deleteProduits", 1);
        // elenco dei prodotti
        List<Produit> produits = getDao().getAllProduits();
        // eliminazione di due prodotti
        Produit produit0 = produits.get(0);
        Produit produit5 = produits.get(5);
        int nbDeleted = getDao().deleteProduits(new int[] { produit0.getId(), produit5.getId() });
        // verifiche
        // Assert.assertEquals(2, nbDeleted); -- non funziona con DB2
        Assert.assertNull(getDao().getProduitById(produit0.getId()));
        Assert.assertNull(getDao().getProduitById(produit5.getId()));
        Assert.assertEquals(produits.size() - 2, getDao().getAllProduits().size());
}
  • riga 4: si inseriscono 10 prodotti nel database;
  • righe 7-11: si recuperano tutti i prodotti presenti nel database e si eliminano da esso i prodotti recuperati nelle posizioni 0 e 5;
  • righe 14-16: si verifica che i due prodotti non siano più presenti nel database e che questo contenga due prodotti in meno;
  • il test della riga 13 non viene superato con il codice SGBD DB2. Viene superato con gli altri codici SGBD;

3.6.9.8. I test delle prestazioni

Nei test sono stati inclusi tre metodi il cui unico scopo è valutare le prestazioni del SGBD:


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

    @Test
    public void perf2() {
        // riempimento
        fill(10000);
        // modifica
        List<Produit> produits = getDao().getAllProduits();
        // si aggiornano quelli della categoria 1
        List<Produit> updated = new ArrayList<Produit>();
        for (Produit produit : produits) {
            // int id, String nome, int categoria, double prezzo, String descrizione
            updated.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
        }
        getDao().updateProduits(updated);
    }

    @Test
    public void perf3() {
        // compilazione
        fill(10000);
        // eliminazione
        List<Produit> produits = getDao().getAllProduits();
        // chiavi primarie
        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 degli stessi tramite le loro chiavi primarie;
  • righe 23-34: inserimento di 10.000 prodotti e successiva eliminazione degli stessi tramite le loro chiavi primarie;

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

I risultati del test [JUnitTestDao1] sono i seguenti:

In [1] si ottengono 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 dura 4,80 secondi;
  • l'inserimento di 10.000 prodotti seguito dalla loro eliminazione richiede 4,40 secondi;
  • quindi l'operazione più dispendiosa è l'inserimento;