Skip to content

6. [Corso]: Introduzione all'API JDBC

Parole chiave: database relazionali, API JDBC, SQLException.

6.1. Supporto

La cartella [support / chap-06] contiene i progetti Eclipse relativi a questo capitolo.

6.2. Architettura

Il livello JDBC (Java Database Connectivity) è un'interfaccia universale di accesso al database. Presenta sempre la stessa interfaccia al livello [DAO]. Se si cambia il DBMS, è sufficiente cambiare il driver JDBC. Il livello [DAO] rimane invariato.

6.3. Passaggi per l'utilizzo di un database

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

  1. caricare il driver JDBC del database;
  2. aprire una connessione al database;
  3. eseguire un'istruzione SQL sul database ed elaborare i risultati dell'istruzione SQL;
  4. chiudere la connessione;

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

6.3.1. Passaggio 1 - Caricamento del driver JDBC in memoria

Il codice


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

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

6.3.2. Passaggio 2 - Apertura di una connessione

Una volta che il driver JDBC è a posto, gli diamo l'istruzione di aprire una connessione al database:

Il codice


package spring.jdbc;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
 
public class IntroJdbc01 {
 
...
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
...
        } catch (SQLException e1) {
            // we handle the exception
            ...
        } finally {
         // close connection
         if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e2) {
                // handle the exception
                ...
            }
         }
}
  • Righe 3–7: Le classi che implementano l'interfaccia JDBC si trovano tutte nel pacchetto [java.sql]. Inoltre, in caso di errore, generano tutte una [SQLException] (righe 19, 27). Questa eccezione deriva dalla classe [Exception] ed è una cosiddetta eccezione controllata: è necessario utilizzare un blocco try/catch per gestirla o, in alternativa, scegliere di non gestirla e indicare che il metodo consente la propagazione dell'eccezione aggiungendo [throws SQLException] alla firma del metodo;
  • Riga 17: [DriverManager.getConnection] è un metodo statico che accetta tre parametri:
    • [url]: l'URL del database. Si tratta di una stringa che dipende dal database utilizzato. Per MySQL, ha il formato [jdbc:mysql://localhost:3306/db_name];
    • [user]: il proprietario della connessione;
    • [passwd]: la password dell'utente;
  • righe 24–30: la connessione deve essere chiusa nella clausola [finally] in modo che venga chiusa indipendentemente dal fatto che si verifichi o meno un'eccezione.

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

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

Il codice


Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read-only mode
            connexion.setReadOnly(true);
            // table [PRODUITS] is read
            ps = connexion.prepareStatement("SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS");
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                System.out.println(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
             doCatchException(connexion,e1);
        } finally {
            // we treat the finally
            doFinally(rs, ps, connexion);
        }
 
    private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
....
}
  • Righe 8, 10: apertura di una transazione (riga 8) in modalità di sola lettura (riga 10). Una transazione è una sequenza di istruzioni SQL che o vanno tutte a buon fine o falliscono tutte. Pertanto, in una transazione contenente N istruzioni SQL, se la (I+1)-esima istruzione fallisce, le I istruzioni precedenti verranno annullate. Per un'operazione di lettura, una transazione non è necessaria. Tuttavia, la creazione di una transazione in sola lettura può consentire ad alcuni DBMS di eseguire determinate ottimizzazioni;
  • Riga 12: uso di un [PreparedStatement]. Un [PreparedStatement] ha normalmente dei parametri indicati dal carattere ?. Qui, invece, non è così. Un [PreparedStatement] è un'istruzione preparata dal DBMS. Questa preparazione ha un costo e viene eseguita una sola volta. L'istruzione preparata viene quindi eseguita dal DBMS con i parametri effettivi che sostituiscono i parametri formali ?. Si noti che è preferibile specificare le colonne desiderate piuttosto che utilizzare la notazione * per recuperare tutte le colonne. Specificando i nomi delle colonne, i loro valori possono quindi essere recuperati in base alla loro posizione nell'istruzione SELECT;
  • Riga 13: esecuzione del [PreparedStatement]. Viene recuperato un oggetto [ResultSet];

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

    boolean next()

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

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

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

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

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

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


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

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

Type getType("labelColi") 

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

Type getType(i) 

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

  • righe 15–17: recupero dei valori letti dal database;
  • riga 19: la transazione viene convalidata (operazione nota anche come commit). Ciò la conclude e libera le risorse che il DBMS aveva allocato per essa;
  • riga 25: le risorse vengono rilasciate nel blocco [finally]. Questo richiama il seguente metodo [doFinally]:

private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // closure ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {
 
            }
        }
        // closure [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {
 
            }
        }
        if (connexion != null) {
            try {
                // close connection
                connexion.close();
            } catch (SQLException e3) {
                 // handle the exception
            }
        }
    }
  • righe 3-9: chiudere il [ResultSet];
  • righe 11–17: chiudere il [PreparedStatement];
  • righe 18–27: chiudere la connessione;

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

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

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

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

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

Il codice


Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // ouverture connexion
            connexion = DriverManager.getConnection(url, user, passwd);
            // début transaction
            connexion.setAutoCommit(false);
            // en mode lecture / écriture
            connexion.setReadOnly(false);
            // on met à jour la table
            ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
            // catégorie 1
            ps.setInt(1, 10);
            // exécution
            int nbLignes=ps.executeUpdate();
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // on traite l'exception
            doCatchException(connexion, e1);
        } finally {
            // on traite le finally
            doFinally(null, ps, connexion);
        }
    }
  • riga 9: la connessione viene utilizzata per la lettura e la scrittura;
  • riga 11: un [PreparedStatement] con 1 parametro (rappresentato da ?). I parametri possono essere più di uno. Sono numerati a partire da 1;
  • riga 13: il suo valore viene assegnato al singolo parametro. Il primo parametro di [setType] è la posizione del parametro nel [PreparedStatement] (1, 2, ...) e il secondo è il valore ad esso assegnato. È possibile utilizzare i metodi [setInt, setLong, setFloat, setDouble, setString, setDate, ...];
  • riga 15: viene utilizzato il metodo [executeUpdate], non [executeQuery], che è riservato alle istruzioni SELECT. Il metodo restituisce il numero di righe interessate dall'operazione. Può essere 0.
  • riga 17: la transazione viene confermata;

6.3.5. Fase 4 - Chiusura della connessione

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

6.4. Un progetto di esempio

6.4.1. Supporto

La cartella [support / chap5] contiene i progetti Eclipse relativi a questo capitolo [1, 2]. La cartella [database] contiene lo script SQL per la creazione del database MySQL di esempio relativo a questo capitolo [1, 3].

6.4.2. Il database utilizzato

Gli esempi seguenti utilizzano il seguente database MySQL:

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

Lo creeremo utilizzando lo strumento [WampServer] come segue [1-9]:

6.4.3. Il Progetto Eclipse

  

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


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.jdbc</groupId>
    <artifactId>intro-jdbc-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.34</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.5.1</version>
        </dependency>
    </dependencies>
</project>
  • righe 8–12: il driver JDBC per il DBMS MySQL5;
  • righe 13–17: una libreria in grado di gestire JSON (JavaScript Object Notation) (vedi Sezione 22.6). La useremo per visualizzare i prodotti dal database in formato JSON;

6.4.4. La classe Product

La classe [Product] è la seguente:


package istia.st.jdbc;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class Produit {
 
    // fields
    private int id;
    private String nom;
    private int categorie;
    private double prix;
    private String description;
 
    // manufacturers
    public Produit() {
 
    }
 
    public Produit(int id, String nom, int categorie, double prix, String description) {
        this.id = id;
        this.nom = nom;
        this.categorie = categorie;
        this.prix = prix;
        this.description = description;
    }
 
    // getters and setters
    ...
 
    // to String
    public String toString() {
        try {
            return new ObjectMapper().writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
}
  • Riga 34: Utilizziamo la libreria JSON per visualizzare la stringa JSON del prodotto. Questo produce un output simile al seguente:
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"}

Il vantaggio del metodo [toString] sopra riportato è che, se alla classe vengono aggiunti o rimossi dei campi, il suo metodo [toString] rimane valido. Inoltre, se i campi stessi sono oggetti (liste, array, dizionari, oggetti utente), le librerie JSON possono a loro volta convertirli in stringhe JSON;

6.4.5. La classe [Static]

La classe [Static] raggruppa i metodi contenenti codice utilizzato frequentemente nella classe principale:


package istia.st.jdbc;
 
import java.util.ArrayList;
import java.util.List;
 
public class Static {
 
    public static List<String> getErreursFromThrowable(Throwable th) {
        // retrieve the list of exception error msgs
        List<String> erreurs = new ArrayList<String>();
        while (th != null) {
            // throwable error message
            erreurs.add(th.getMessage());
            // we move on to the cause of throwable
            th = th.getCause();
        }
        // result
        return erreurs;
    }
 
    public static void show(String title, List<String> messages){
        // title
        System.out.println(String.format("%s : ",title));
        // messages
        for(String message : messages){
            System.out.println(String.format("- %s",message));
        }
    }
}
  • righe 8–19: restituisce un elenco di errori racchiusi in un oggetto di tipo [Throwable], che è la superclasse della classe [Exception];
  • righe 21-28: visualizza un elenco di messaggi sullo schermo;

Questo codice potrebbe trovarsi nella classe principale poiché è l'unica a utilizzarlo in questo contesto. Tuttavia, stiamo considerando uno scenario più ampio in cui altre classi potrebbero aver bisogno di questo codice.

6.4.6. Lo scheletro della classe principale


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

6.4.7. Eliminazione del contenuto della tabella dei prodotti

Il metodo [delete] elimina il contenuto della tabella:


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

Questo esempio utilizza le transazioni. Una transazione consente di raggruppare istruzioni SQL che devono essere tutte eseguite con successo o tutte annullate. Ci sono quattro operazioni da tenere presenti:

  • avvio di una transazione: [connection.setAutoCommit(false)];
  • conclusione di una transazione con esito positivo: [connection.commit()]. In questo caso, tutte le operazioni eseguite sul database durante la transazione vengono confermate;
  • conclusione di una transazione con errore: [connection.rollback()]. In questo caso, tutte le operazioni eseguite sul database durante la transazione vengono annullate;
  • Ritorno alla modalità [auto-commit], che è la modalità predefinita per l'API JDBC: [connection.setAutoCommit(true)]. In questa modalità, ogni istruzione SQL fa parte di una transazione. Pertanto, se si eseguono due inserimenti e il secondo fallisce:
    • in modalità [AutoCommit=true], il primo inserimento rimane (è stato confermato dal primo AutoCommit);
    • in modalità [AutoCommit=false], il primo inserimento viene annullato;

Nei nostri esempi, ogni volta che si verifica un'eccezione, annulliamo la transazione nel metodo [doCatchException]:


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

6.4.8. Creazione del contenuto della tabella dei prodotti

Il metodo [insert] crea il contenuto della tabella:


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

6.4.9. Visualizzazione del contenuto della tabella dei prodotti

Il metodo [select] visualizza il contenuto della tabella:


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

6.4.10. Aggiornamento del contenuto della tabella

Il metodo [update] aggiorna determinati prodotti:


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

6.4.11. Ruolo della transazione

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


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

6.4.12. Risultati

I risultati dell'esecuzione del metodo [main] sono i seguenti:

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

6.5. Utilizzo di una fonte dati [DataSource]

Rivedremo l'applicazione precedente utilizzando una fonte dati [javax.sql.DataSource]:

Image

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

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

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

6.5.1. Il progetto Eclipse

  

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


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.jdbc</groupId>
    <artifactId>intro-jdbc-02</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.34</version>
        </dependency>
        <!-- library jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.5.1</version>
        </dependency>
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.1.3.RELEASE</version>
        </dependency>
        <!-- Tomcat Jdbc -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
            <version>8.0.20</version>
        </dependency>
    </dependencies>
</project>
  • righe 21–25: dipendenza da Spring;
  • righe 27–31: dipendenza dalla libreria che fornisce l'origine dati;

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


package istia.st.jdbc;
 
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class AppConfig {
 
    // constantes
    final static String URL = "jdbc:mysql://localhost:3306/dbIntroJdbc";
    final static String USER = "root";
    final static String PASSWD = "";
    final static String INSERT = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
    final static String DELETE = "DELETE FROM PRODUITS";
    final static String SELECT = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    final static String UPDATE = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
    final static String INSERT2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";
    final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
 
    @Bean
    public DataSource dataSource() {
        // source de données TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration accès JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // une connexion ouverte initialement
        dataSource.setInitialSize(1);
        // résultat
        return dataSource;
    }
}
  • righe 11–19: le costanti precedentemente definite in [IntroJdbc01] sono state spostate in [AppConfig];
  • righe 31–34: il bean Spring che definisce l'origine dati;
  • riga 24: creazione della fonte dati, che non è ancora configurata;
  • righe 26–29: le informazioni che consentono alla fonte dati di connettersi al database;
  • riga 31: crea un pool di 1 connessione. Qui non ne servono di più. Non ci sono mai più connessioni simultanee;

6.5.2. La classe principale

La classe principale [IntroJdbc02] è la seguente:


package istia.st.jdbc;
 
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
 
import javax.sql.DataSource;
 
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
public class IntroJdbc02 {
    // data source
    private static DataSource dataSource;
 
    public static void main(String[] args) {
        // spring context retrieval
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        // data source recovery
        dataSource = ctx.getBean(DataSource.class);
        // empty table [PRODUITS]
        delete();
...
        // finish
        ctx.close();
        System.out.println("Travail terminé");
    }
 
    // product list
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // opening connection
            connexion = dataSource.getConnection();
            // start of transaction
            connexion.setAutoCommit(false);
...
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, null, connexion);
        }
    }
...
}
  • riga 14: l'origine dati. Si noti che è di tipo [javax.sql.DataSource], che è un'interfaccia;
  • riga 18: istanziazione degli oggetti Spring;
  • riga 20: ottenimento di un riferimento alla fonte dati. Si noti che la classe effettivamente utilizzata non viene mai menzionata. Pertanto, in questo caso, nulla suggerisce che venga utilizzata un'implementazione [TomcatJdbc];
  • riga 36: ottenimento di una connessione aperta;
  • il resto del codice è identico a quello della classe [IntroJdbc01];

6.6. Conclusione

Ulteriori informazioni sulla gestione dei database sono disponibili nel documento [Utilizzo di un database relazionale con l'ecosistema Spring].