Skip to content

6. [Cours]: Introduzione a API JDBC

Parole chiave: database relazionali, API JDBC, SQLException.

6.1. Support

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

6.2. Architecture

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

6.3. Le fasi di gestione di un database

Nell’architettura sopra descritta, l’esecuzione di un database da parte del programma da console prevede le seguenti fasi:

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

Il passaggio 1 viene eseguito una sola volta. I passaggi 2-4 vengono ripetuti. Si noti che non si lascia una connessione aperta. La si chiude non appena non è più necessaria.

6.3.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.

6.3.2. Fase 2 - Apertura di una connessione

Una volta installato il driver JDBC, gli viene richiesto di aprire una connessione con il 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) {
            // si gestisce l'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 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] affinché venga chiusa indipendentemente dal verificarsi o meno di un'eccezione.

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

Una volta ottenuta la connessione, è possibile inviare 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 sessione
            connexion = DriverManager.getConnection(url, user, passwd);
            // inizio transazione
            connexion.setAutoCommit(false);
            // in modalità 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 di [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 richieste 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 di [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 delle 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;

6.3.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 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) {
            // si gestisce 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;

6.3.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.

6.4. Un progetto di esempio

6.4.1. Supporto

La cartella [support / chap5] contiene i progetti Eclipse di questo capitolo [1, 2]. La cartella [database] contiene lo script SQL che consente di creare il database di esempio MySQL di questo capitolo [1, 3].

6.4.2. Il database utilizzato

Gli esempi che seguono utilizzano il seguente database MySQL:

 
  • [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;

Lo creeremo con lo strumento [WampServer] nel modo seguente [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 di SGBD MySQL5;
  • righe 13-17: una libreria in grado di gestire il jSON (Javascript Object Notation) (cfr. paragrafo 22.6). La useremo per visualizzare in formato jSON i prodotti presenti nel database;

6.4.4. La classe dei prodotti

La classe [Produit] è la seguente:


package istia.st.jdbc;

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

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 String
    public String toString() {
        try {
            return new ObjectMapper().writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
}
  • riga 34: si utilizza la libreria jSON per visualizzare la stringa jSON del prodotto. Il risultato è una visualizzazione simile alla 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 indicato è che, se si aggiungono o si rimuovono campi dalla classe, il suo metodo [toString] rimane sempre valido. Inoltre, se i campi sono essi stessi oggetti (liste, array, dizionari, oggetti utente), le librerie jSON sono in grado di trasformarli a loro volta in stringhe jSON;

6.4.5. La classe [Static]

La classe [Static] raggruppa in metodi il 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) {
        // 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;
    }
    
    public 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 8-19: consente di ottenere l'elenco degli errori incapsulati in un oggetto di tipo [Throwable], che è la classe madre della classe [Exception];
  • righe 21-28: visualizza sullo schermo un elenco di messaggi;

Questo codice potrebbe trovarsi nella classe principale, poiché in questo caso è l'unica a utilizzarlo. Ci troviamo qui in un contesto 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 {

    // costanti
    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) {
        // caricamento del driver JDBC da MySQL
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // si svuota la tabella [PRODUITS]
        delete();
        // la si riempie
        insert();
        // si legge
        select();
        // aggiornamento
        update();
        // visualizzazione
        select();
        // inserimento di due elementi identici
        // l'inserimento non va a buon fine e nessuno dei due elementi viene inserito a causa della transazione
        insert2();
        // si verifica
        select();
        // fine
        System.out.println("Travail terminé");
    }

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

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

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

    // aggiunta di 2 prodotti
    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) {

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

6.4.7. Cancellazione 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(url, user, passwd);
            // inizio transazione
            connexion.setAutoCommit(false);
            // svuotamento della tabella [PRODUITS]
            ps = connexion.prepareStatement(delete);
            ps.executeUpdate();
            // conferma transazione
            connexion.commit();
            // ritorno alla modalità predefinita
            connexion.setAutoCommit(true);
        } 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, null, connexion);
        }
    }

Questo esempio 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:

  • avvio di una transazione: [connexion.setAutoCommit(false)];
  • conclusione di una transazione con esito positivo: [connexion.commit()]. In questo caso, tutte le operazioni effettuate su BD durante la transazione vengono convalidate;
  • conclusione di una transazione con esito negativo: [connexion.rollback()]. In questo caso, tutte le operazioni effettuate su BD durante la transazione vengono annullate;
  • ritorno alla modalità [auto-commit], che è la modalità predefinita di API JDBC: [connexion.setAutoCommit(true)]. In questa modalità, ogni ordine SQL è oggetto di una transazione. Pertanto, se si effettuano due inserimenti e il secondo fallisce:
    • in modalità [AutoCommit=true], il primo inserimento rimane (è stato convalidato 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) {
        // visualizzazione dei messaggi di errore
        Static.show(title, Static.getErreursFromThrowable(th));
        // annullamento della transazione
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // vengono visualizzati i messaggi di errore
            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:


// aggiunta prodotti
    public static void insert() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // apertura della connessione
            connexion = DriverManager.getConnection(url, user, passwd);
            // inizio transazione
            connexion.setAutoCommit(false);
            // compilazione della tabella
            ps = connexion.prepareStatement(insert);
            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();
            // ritorno alla modalità predefinita
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // gestione dell'eccezione
            doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
        } finally {
            // gestione del blocco finally
            doFinally(null, null, connexion);
        }
    }

6.4.9. 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(url, user, passwd);
            // inizio transazione
            connexion.setAutoCommit(false);
            // si legge la tabella [PRODUITS]
            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)));
            }
            // conferma transazione
            connexion.commit();
            // ritorno alla modalità predefinita
            connexion.setAutoCommit(true);
        } 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(null, null, connexion);
        }
}

6.4.10. 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(url, user, passwd);
            // inizio transazione
            connexion.setAutoCommit(false);
            // aggiornamento della tabella
            ps = connexion.prepareStatement(update);
            // categoria 1
            ps.setInt(1, 1);
            // esecuzione
            ps.executeUpdate();
            // conferma transazione
            connexion.commit();
            // ritorno alla modalità predefinita
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // si gestisce l'eccezione
            doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
        } finally {
            // elaborazione del 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 è 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 della connessione
            connexion = DriverManager.getConnection(url, user, passwd);
            // inizio transazione
            connexion.setAutoCommit(false);
            // si aggiunge 1 riga
            ps = connexion.prepareStatement(insert2);
            // esecuzione
            ps.executeUpdate();
            // si aggiunge la stessa riga una seconda volta, quindi con la stessa chiave primaria
            // l'inserimento deve fallire e nessuno dei due elementi deve essere inserito a causa della transazione
            ps.executeUpdate();
            // commit della transazione
            connexion.commit();
            // ritorno alla modalità predefinita
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // si gestisce l'eccezione
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout", connexion, e1);
        } finally {
            // si gestisce il finally
            doFinally(null, null, connexion);
        }
    }

6.4.12. Risultati

L'esecuzione del metodo [main] produce i seguenti risultati:

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 di tipo [DataSource]

Riprendiamo l'applicazione precedente utilizzando una fonte 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

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>
        <!-- libreria 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 la fonte dei dati;

La classe di configurazione di 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 {

    // costanti
    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() {
        // fonte dati TomcatJdbc
        DataSource dataSource = new DataSource();
        // configurazione accesso JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // una connessione inizialmente aperta
        dataSource.setInitialSize(1);
        // risultato
        return dataSource;
    }
}
  • righe 11-19: le costanti precedentemente definite in [IntroJdbc01] sono state spostate in [AppConfig];
  • righe 31-34: il bean Spring che definisce la fonte dati;
  • riga 24: creazione della fonte dati, ancora non 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 {
    // fonte dati
    private static DataSource dataSource;

    public static void main(String[] args) {
        // recupero del contesto Spring
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        // recupero della fonte dati
        dataSource = ctx.getBean(DataSource.class);
        // si svuota la tabella [PRODUITS]
        delete();
...
        // fine
        ctx.close();
        System.out.println("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);
...
        } catch (SQLException e1) {
            // gestione dell'eccezione
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // elaborazione del finally
            doFinally(null, null, connexion);
        }
    }
...
}
  • riga 14: la fonte 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 in nessun momento viene citata la classe effettivamente utilizzata. Pertanto, in questo caso, nulla lascia supporre che si stia utilizzando un'implementazione [TomcatJdbc];
  • riga 36: ottenimento di una connessione aperta;
  • il resto del codice è identico a quello della classe [IntroJdbc01];

6.6. Conclusion

Ulteriori informazioni sulla gestione dei database sono disponibili nel documento [Exploiter une base relationnelle avec l'écosystème Spring].