MokaByte Numero 11 - Settembre 1997
Foto
 
JDBC - La pratica 
III parte
 
di
Giovanni Puliti
Finalmente molti esempi per imparare a fare transazioni

 



Dopo gli articoli dei mesi scorsi in cui è stato affrontato l'argomento della gestione di database per mezzo delle DBC API da un punto di vista strettamente teorico, in questo numero vediamo finalmente come mettere in pratica le nozioni apprese




L'impostazione che ho dato all'articolo è prettamente schematica, e i vari argomenti sono stai affrontati in sequenza; non è stato riportato un esempio completo per dare maggior spazio all'esposizione di piccoli porzioni di codice con i quali analizzare tutte le varie casistiche di accesso e di gestione di basi di dati.
Si presume una certa familiarità del lettore con i concetti di base legati alla gestione di database e con le librerie di base per gestire tali oggetti (vedi JDBC).
Tutta la trattazione che segue è del tutto generica ed adattabile ad una qualsiasi tipo di DB e DBMS, anche se in questa sede abbiamo utilizzato un database di tipo mdb (in particolare gli esempi fanno riferimento all'ormai stranoto biblio.mdb) gestito da ODBC. Per maggior semplicità e per permettere a tutti di mettere in pratica gli esempi proposti senza dover acquistare driver particolari, si è utilizzato il bridge JDBC-ODBC, direttamente disponibile in JDK.
Rispetto agli articoli precedenti in un certo senso il presente è una ripetizione più superficiale degli argomenti già trattati.
Senza riprendere in mano tutta la teoria di JDBC API ricordo brevemente che esse non permettono la gestione diretta di un database, ma solo l'invio di istruzioni in un linguaggio standard (SQL), e la manipolazione degli eventuali risultati. Affronteremo quindi tutti gli oggetto necessari per instaurare questo coloquio bidirezionale.
Molto sinteticamente i passi necessari da compiere sono i seguenti:

La prima cosa da fare prima di iniziare una qualsiasi è caricare il driver necessario per poter utilizzare il db: è la classe DriverManager che si occupa in genere di tale operazione andando a scorrere la lista jdbc.drivers ed utilizzando il primo driver che risponde ai requisiti necessari. E' possibile consultare tale lista per mezzo del metodo getDrivers().
Di seguito sono riportati due esempi che mostrano due modi per caricare un driver ed aggiungerlo alla lista di quelli disponibili; successivamente è mostrato anche come controllare che il driver sia stato correttamente caricato. Per ragioni di brevità la sezione finale di gestione delle eccezioni non verrà più riproposta, essendo la stessa per tutti gli esempi che seguono.
La classe DriverManager non è l'unica che permette la connessione con un DB, ma è possibile utilizzare anche l'interfaccia Driver che permette di accedere a tutte le informazioni che riguardano il db, come ad esempio se esso è JDBC-Compliant, la versione, o se accetta un determinato URL. La manipolazioni di tali proprietà può essere eseguita per mezzo di un generico oggetto Properties, anche se in questo caso specifico è più indicata la classe DriverPropertyInfo.
Ecco un esempio di utilizzazione della interfaccia Driver per conoscere le caratteristiche del database
 

Per poter eseguire istruzioni SQL le JDBC API mettono a disposizione tre metodi

Il metodo execute()  si differenzia per essere il più complesso è il più generico: permette di eseguire una qualsiasi operazione SQL e reperire così informazioni di ogni genere dal database; è possibile anche innoltrare anche interrogazioni che prevedono la resituzione di risultati multipli.  Resituisce valori di tipo booleano che indicano se la query ha prodotto oppure no ResultSet, disponibili poi per mezzo della getResultSet().
Se non si sono prodotti recordset in uscita, (come nel caso di modifiche su una tabella), col metodo GetUpdateCount() si ottiene il numero di righe influenzate dalla modifica. Se il risultato di tale metodo è -1 significa che l'operazione eseguita non ha modificato nessun valore in tabella.
Ecco come creare una tabella inviando una stringa SQL con execute()


Ecco due esempi che mostrano l'uso della execute() in abbinamento con la getUpdateCount() e con e senza resituzione di recordset (ResultSet).

//  Esempio di execute() di una Statement senza ResultSet: SELECT SQL

try{

        DriverManager.setLogStream(null);

        Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");

        connessione=DriverManager.getConnection("jdbc:odbc:biblio");

        connessione.setAutoCommit(false);

        Statement st= connessione.createStatement();

        sql="SELECT name  FROM Authors";

        risult = st.execute(sql);

        numris=st.getUpdateCount();

        connessione.rollback();
 
 

        // execute()  di una Statement con ResultSet:  UPDATE SQL

        sql = "UPDATE Authors SET Name = \'Pippo\' WHERE Name=\'Topolino\'";

        ResultSet rs =  st.getResultSet()

        risult = st.execute(sql);

        numris = st.getUpdateCount();

}

Il metodo executeQuery() è molto simile al precedente ma più semplice e limitato (ad esempio non può inviare istruzion SQL che prevedano risultati multipli). Esso permette semplicemente l'esecuzione di una istruzione SQL rappresentata dal suo argomento di tipo stringa. Restituisce un valore singolo di tipo ResultSet. L'uso di tale metodo sia del tutto equivalente al metodo execute().
Infine il metodo executeUpdate() permette di innoltrare istruzioni SQL del tipo DELETE UPDATE o INSERT che effettuano operazioni sui dati in modo da modificarne i valori.  Il valore restituito è il numero di record modificati dalla istruzione inviata: esso corrisponde al valore ottenibile per mezzo del getUpdateCount() dopo l'esecuzione di execute(). Non permette di eseguire istruzioni con risposte multiple.
 
// Esempio di executeUpdate di una Statement.

try{

        DriverManager.setLogStream(null);

        Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");

        connessione=DriverManager.getConnection("jdbc:odbc:biblio");

        connessione.setAutoCommit(false);

        Statement st= connessione.createStatement();

        String sql="UPDATE Authors SET Name = \'Topolino\' WHERE age < 30";

        numris = st.executeUpdate(sql);

        numris2=st.getUpdateCount();

        risult = st.getResultSet();

        connessione.rollback();

}


Esecuzione di istruzioni precompilate.
La volta scorsa abbiamo visto che è possibile ottimizzare le operazioni di colloquio con il database per mezzo di istruzioni SQL precompilate.
Questa operazione può essere effettuata per mezzo della classe PreparedStatement, che permette di velocizzare di molto le transazioni utilizzando stringhe già precompilate e variando di volta in volta solo alcuni parametri: questo permette di eseguire tutte le operazioni interne al DBMS per la gestione della transazione una sola volta, e di riutilizzare, con poche modifiche, il lavoro già fatto per le esecuzioni successive.
Anche in questo caso per eseguire istruzioni SQL si possono utilizzare i tre metodi execute(), executeQuery() e executeUpdate(), che però in questo caso devono essere invocati senza parametri (è l'oggetto statement che ingloba la stringa SQL precompilata).
Un oggetto PreparedStatement viene creato a partire da un oggetto Connection per mezzo del metodo prepareStatement(), al quale si deve passare in input una stringa rappresentante lo statemente SQL precompilato.
Per creare un PreparedStatement si deve prima costruire l'istruzione SQL, lasciando dei caratteri jolly "?" (placeholder ) dove si dovrà di volta in volta introdurre un parametro diverso.
Per settare tali placeholder utilizzano metodi del tipo setXXX() dove XXX indica il tipo del valore che deve essere inserito.
Il compito di questi metodi è quello di convertire automaticamente il tipo Java in tipo SQL: ad esempio setString() coverte il tipo del valore passato da String a CHAR, VARCHAR o LONGVARCHAR a seconda della lunghezza della variabile stessa.
I metodi di questo tipo ricevono due parametri: il primo indica la posizione del placeholder, mentre il secondo contiene il valore.
Se non si conosce il tipo del valore da immettere si può alternativamente utilizzare il metodo setObject(), ed in questo caso il driver si occupa del riconoscimento del tipo e della relativa conversione in tipo SQL.
Ecco un esempio analogo ai precedenti ma in cui è prevista la creazione ed il completamento di un PreparedStement.
 

// Esempio di preparazione di una PreparedStatement e di successiva

// istanziazione

try{

        Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");

        connessione=DriverManager.getConnection("jdbc:odbc:catalogocd");

        connessione.setAutoCommit(false);

        String sql= "UPDATE  SET Name = \'Pippo\' WHERE age < ?";

        PreparedStatement pst= connessione.prepareStatement(sql);

        pst.setInt(1,30);

        pst.execute();

        connessione.rollback();

}


E' interessante a tal punto osservare le prestazioni di esecuzione di statement SQL normali a confronto con preparedstatement precompilati. Le fasi da eseguire sono
 

    1. ottenimento della connessione
    2. disabilitazione del autocommit
    3. creare il PreparedStatement
    4. settare i placeholder
    5. eseguire il metodo execute()
    6. ripetere i passi 4 e 5 un numero arbitrario di volte
    7. eseguire il commit o rollback a seconda del risultato delle operazioni
    8. analisi dei risultati
    9. rilascio della connessione
// Esempio di preparazione di una PreparedStatement.

// Confronto delle prestazioni

try{

        DriverManager.setLogStream(null);

        Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");

        connessione=DriverManager.getConnection("jdbc:odbc:biblio");

        connessione.setAutoCommit(false);

        String sql="UPDATE Authors  SET Name = \'Pippo\' WHERE age < ?";

        Statement st= connessione.createStatement();

        inizio_st = (new java.util.Date()).getTime();

        for(int i=0; I<times; i++)

                st.execute(sql);

        fine_st = (new java.util.Date()).getTime();

        System.out.println("Tempo per "+ times+" execute "+fine_st-inizio_st);

        PreparedStatement pst= connessione.prepareStatement(sql)

        pst.setInt(1,40);

        inizio_pst = (new java.util.Date()).getTime();

        for(int i=0; I<times; i++)

                pst.execute();

        fine_pst  = (new java.util.Date()).getTime();

        System.out.println("Tempo per "+ times+" execute "+fine_pst-inizio_pst);

        connessione.rollback();

}


Esecuzione di QUERY
L'interfaccia CallableStatement è una ulteriore estensione delle altre viste in precedenza, e permette di richiamare istruzioni definite nel DBMS. Il nome con cui queste funzioni (stored procedure) dipende dal particolare gestore di DB: ad esempio in MS Access (o più precisamente in ODBC per mdb) si chiamano Query. Le considerazioni relative ai metodi di manipolazione del db sono analoghe al caso precedente, tranne per la modalità di creazione dell'oggetto CallableStatement a partire da un Connection e per il fatto che adesso non solo è possibile manipolare parametri in entrata (parametri IN), ma anche in uscita (parametri OUT).

Per utilizzare un CallableStatement si devono seguire i seguenti passi, come fatto nell'esempio seguente:

    1. creare una connessione
    2. creare la CallableStatement per mezzo del metodo
    3. connection.prepareStatement(); ad esempio String sql="{? =ProvaCall(?,?)}"
    4. registrare i parametri OUT attraverso il metodo registerOutParameter()
    5. eseguire l'istruzione per mezzo del metodo execute() della PreparedStatement()
    6. analizzare i risultati attraverso i metodi getXXX()
    7. Rilasciare la connessione.
// Creazione di una CallableStatement                          *

try{

        DriverManager.setLogStream(null);

        Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");

        connessione=DriverManager.getConnection("jdbc:odbc:biblio");

        connessione.setAutoCommit(false);

        String sql= "{ ? = call provacall ? }";

        CallableStatement pst= connessione.prepareCall(sql);

        pst.setInt(2,1990);

        pst.registerOutParameter(1,Types.NUMERIC);

        pst.execute();

        ris=pst.getInt(1);

        connessione.rollback();

}

ELABORAZIONE DI RISULTATI
Negli esempi visti in precedenza abbiamo visto che dopo aver innoltrato una stringa SQL al DBMS, esistono vari modi per elaborare gli eventuali risultati, ed più semplice è quello di utilizzare l'oggetto ResultSet.
Questo oggetto è costituito da una lista di record ottenuti dalla esecuzione dello statement SQL: il l'estrapolazione dei risultati da un ResultSet può essere fatta per mezzo di metodi appositi. Internamente esso mantiene un puntatore al record corrente (detto cursore) che può essere spostato al fine di scorrere tutto il recordset. Per accedere ai valori contenuti nei singoli campi si usano metodi del tipo getXXX() dove le XXX al solito indicano il tipo. Per accedere alla colonna voluta si può passare sia il nome della stessa che l'ordinale identificativo.
Lo shift del cursore viene eseguito col metodo next(), che nel caso sia stato già raggiunto l'ultimo record restituisce false.
La gestione di ResultSet è attualmente limitata essendo ristretta al solo metodo next().
Nell'esempio seguente è riportato un esempio di elaborazione dei risultati per mezzo di ResultSet e next().
try{

        DriverManager.setLogStream(null);

        Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");

        connessione=DriverManager.getConnection("jdbc:odbc:biblio");

        connessione.setAutoCommit(false);

        String sql=     "Select AuID,Name FROM Authors  WHERE age<30";

        Statement       st= connessione.createStatement();

        ResultSet ris = st.executeQuery(sql);

        while (ris.next()){

                ResultSetMetaData rsmd = ris.getMetaData();

                int num_col=rsmd.getColumnCount();

                System.out.println("Numero colonne "+num_col);

                for (int i=0; i<num_col; i++){

                        System.out.println("Nome Colonna "+i+" = "+rsmd.getColumnName(i+1));

                        System.out.println("Label Colonna "+i+" = "+rsmd.getColumnLabel(i+1));

                }

                System.out.print("AuID ="+ris.getString(1));

                System.out.println("Name ="+ris.getString(2));

        }

        connessione.rollback();

}

Reperire Maggiori Informazioni
Attraverso il metodo getMetaData() dell'oggetto ResultSet è possibile avere tutte le informazioni relative alla costituzione dei risultati ottenuti: con tale metodo infatti si ottiene un oggetto di tipo ResultSetMetaData, che fornisce la configurazione (numero colonne, nome e tipo della colonna i-esima, etc..). Queste informazioni sono utili nel caso in cui non si conosca la struttura del database e si vuole ad esempio creare una interfaccia di gestione il più generale possibile.
In maniera equivalente si possono conoscere tutte le caratteristiche del database utilizzando l'oggetto DataBaseMetaData. Esso permette di accedere ad un insieme molto vasto di dati (i metodi per leggere le proprietà del db sono ben 133). Molto importanti ad esempio sono le informzioni relative al tipo di SQL utilizzato dal DBMS, in quanto sappiamo che purtroppo tale linguaggio di interrogazione è tutt'altro che standard. E' perfino ottenere la lista delle istruzioni SQL permesse, delle operazioni logiche e matemtatiche utilizzabili.
Nell'esempio che segue è mostrato come intercettare il valore di alcune delle proprietà; ovviamente per brevità non sono utilizzati tutti i metodi di indagine:
// Ritorna SI se vero e NO se falso

private String siono(boolean val){

        return (val)?"SI":"NO";

}

try{

        DriverManager.setLogStream(null);

        Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");

        connessione=DriverManager.getConnection("jdbc:odbc:biblio");

        DatabaseMetaData dati= connessione.getMetaData();

        System.out.println("--------------------");

        System.out.println("            DATABASEMETADATA");

        System.out.println("--------------------");

        System.out.println("allProceduresAreCallable? "+siono(dati.allProceduresAreCallable()));

        System.out.println("allTablesAreSelectable? "+siono(dati.allTablesAreSelectable()));

        System.out.println("URL "+dati.getURL() );

        System.out.println("UserName "+dati.getUserName());

        System.out.println("isReadOnly? "+siono(dati.isReadOnly()));

}

TRANSAZIONI
Andiamo adesso ad analizzare un aspetto importante, quello dell gestione delle transazioni. Per transazione si intende un insieme di operazioni SQL in cui l'ultima è un commit (che fissa le modifiche effettuate) o un rollback (che ripristina la situazione recedente all'inizio della transizione).  Il metodo setAutoCommit() permette di impostare la modalità di commit in automatico.
 
 

GESTIONE DELLE ECCEZIONI
Ogni volta che si esegue una istruzione relativa ad una connessione con un DBMS, si può incorrere nella generazione di una eccezione o di un warning, a causa di una qualche irregolarità.
Le JDBC API mettono a disposizione una particolare classe detta SQLException che offre tutte le informazioni specifiche dell'inconveniente verificatosi. Ad esempio è possibile conoscere:

Dopo ogni operazione SQL si possono verificare anche dei warnings che però come noto non alterano la linearità del programma. Per controllare gli eventuali warning prodotti su utilizza il metodo getWarnings(), invocabile sugli oggetti ResultSet, Statement, Connection. Anche in questo caso si ha una lista di allarmi visitbile per mezzo di getNextWarning().
 

Conclusioni
Termina qui questo articolo pratico su JDBC e gestione database con Java, e termina anche la miniserie iniziata tre mesi addietro. Dal colloquio con alcuni lettori via mail e direttamente dal vivo in occasione del Borland Forum tenutosi a Milano in Giugno, noi della redazione abbimao avuto modo di constatare l'alto interesse che l'argomento suscita per cui spero di esser riuscito ad affrontarlo in maniera sufficientemente approfodita.
Un ringraziamento particolare va a Massimo Carli che si è preoccupato della stesura degli esempi e senza il quale non sarei riuscito a realizzare l'articolo in tempo utile per la pubblicazione.
 

  

 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it