JDBC: Java e database. Introduzione all’architettura e esempi di utilizzo 1.0 INTRODUZIONE ALL’ARCHITETTURA JDBC è (anche se non ufficialmente riconosciuto da Sun) l’acronimo per Java DataBase Connectivity. In questa dicitura è racchiusa l’essenza dell’architettura. JDBC in pratica è una API Java che permette, a partire da un qualsiasi programma scritto in java di accedere ad una sorgente di dati esterna: nella maggior parte dei casi un DBMS relazionale. Quello che ha voluto fare Sun con la creazione di JDBC è stato fornire delle api standard che permettessero l’accesso in maniera uniforme a tipi di dati differenti tra loro. In pratica JDBC costituisce una sorta di layer che si interpone tra applicazione e database vero e proprio. Architettura JDBC Le applicazioni infatti che utilizzano JDBC nella maggior parte dei casi sono utilizzabili con differenti tipi di database senza la necessità di particolari modifiche al codice. E’ naturale però che in fase di progettazione deve essere stata prestata la massima attenzione al fatto che un database supporti o meno certe caratteristiche, predisponendo di conseguenza adeguati meccanismi di gestione degli errori e delle eccezioni. Elevata portabilità quindi e riutilizzo del codice che guarda caso sono proprio due delle caratteristiche vincenti di Java che come sappiamo basa il proprio successo sul paradigma WORE (Write Once Run Everywhere). JDBC svolge il suo lavoro mediante alcune interfacce, le quali vengono implementate da un set di classi specifico per ogni differente tipo di database andando così a costituire il cosiddetto “driver JDBC”. Il programmatore che vuole scrivere applicazioni che utilizzano i database non si deve in alcun modo preoccupare di come queste classi vengano implementate: a lui interessa usare il driver per interfacciarsi con i dati e poterli così manipolare a proprio piacimento. Ecco quindi che come accennavamo sopra JDBC maschera la struttura e l’implementazione di tutto ciò che sta “sotto di lui”, mostrando una chiara e semplice interfaccia con dei metodi da invocare. Ma cosa permette di fare JDBC? Sostanzialmente un driver JDBC consente di effettuare tre operazioni: 1. stabilire una connessione con una sorgente di dati (un database relazionale per es.) 2. inviare comandi SQL come update, insert, select, etc. 3. elaborare e manipolare i risultati ottenuti Nell’accedere ad una basi di dati possiamo individuare sostanzialmente due tipi di architetture o modelli che dir si voglia. Il primo modello è il “two-tier model” mentre il secondo è il “three-tier model”. Il two-tier model prevede che l’applicazione dialoghi in maniera diretta con il database sottostante. L’utente in questa maniera sfruttando il driver JDBC adatto agisce direttamente sulla sorgente di dati. In particolare i comandi dell’utente vengono inviati al database il quale ritorna all’utente i risultati opportunamente costruiti. E’ chiaro come questo modello sia l’esempio più classico di architettura client/server in cui l’host dell’utente è il client, mentre la macchina che ospita il database svolge la parte del server. In questo particolare modello proprio perché l’interazione con la sorgente di dati è stretta e diretta, l’applicazione potrebbe sfruttare eventuali conoscenze sulle specifiche di implementazione del database per migliorare le performance o utilizzare caratteristiche o proprietà non standard. E’ logico però, che questa pratica, ha anche l’immediato svantaggio di limitare fortemente la portabilità e il riuso del codice con sorgenti di dati differenti. Il two-tier model L’architettura three-tier model è studiata con lo scopo fondamentale di fornire migliori performance, scalabilità e disponibilità di servizi/controlli aggiuntivi: è per questo che è tipicamente usata per lo sviluppo di applicazioni enterprise. Il three-tier model Come si può chiaramente vedere in figura si possono individuare tre componenti fondamentali: 1. Client tier – essa rappresenta lo strato presentazione con il quale l’utente interagisce (un browser, una gui di un programma, etc.). Non c’è alcuna necessità che questa componente conosca dettagli implementativi del database sottostante. 2. Middle tier server – questo strato intermedio comprende tutti quei servizi posti tra base di dati e utente. A questo livello c’è la possibilità di controllare e gestire l’accesso alla base di dati e altresì gestire in maniera adeguata le risposte provenienti dalla stessa. I servizi aggiuntivi vengono per l’appunto forniti da applicazioni di diverso tipo che possono interagire fra loro. 3. Data Source Tier – il livello a cui sono collocati i dati veri e propri: il database relazionale o qualsiasi altra sorgente di dati. La cosa fondamentale è che questa base di dati sia accessibile mediante un driver che rispetti le specifiche JDBC. Dopo questa breve analisi si sarà capita l’importanza che ha il driver JDBC come intermediario tra utente e dati. Senza driver infatti bisognerebbe di volta in volta scrivere codice (anche molto complicato) che dialoga e lavora con la base di dati. Ciò oltre ad essere improponibile non è spesso possibile, perché richiederebbe una conoscenza dettagliata e approfondita dell’implementazione del database con il quale di volta in volta si lavora. Come sappiamo questo non è possibile per la maggior parte dei database commerciali. Ecco quindi che i driver opportunamente implementati (a seconda del diverso db) permettono al programmatore di invocare quei metodi esposti dalle API JDBC e di non preoccuparsi di come queste effettivamente funzionino. Trasparenza prima di tutto! I driver però non sono tutti uguali e possono essere divisi fondamentalmente in quattro categorie: 1. JDBC-ODBC bridge: si tratta in pratica di driver JDBC che fanno da “ponte”, fornendo accesso ai driver ODBC di Microsoft. 2. Native-API partly Java driver: questi driver convertono le chiamate JDBC nelle corrispondenti chiamate dei client dei relativi DBMS (Oracle, Sybase, Informix, etc.). Questo significa che il driver per effettuare l’accesso alla base di dati contiene codice java che invoca metodi nativi C/C++ messi a disposizione dai produttori del database. 3. JDBC-Net pure Java driver: è la soluzione forse più flessibile, visto che consente la connessione a database differenti. Questo perché le chiamate JDBC sono convertite in un protocollo generico, per poi essere riconvertite lato server nelle API specifiche del database. In pratica il client invoca mediante i socket una applicazione middleware sul server che traduce le richieste in base alle specifiche del driver. 4. Native-protocol pure Java driver: questo tipo di driver dialoga direttamente con il DBMS mediante socket, convertendo le chiamate JDBC nel protocollo di rete utilizzato. Si tratta della soluzione tipicamente adottata dagli stessi produttori del database, visto che richiede una conoscenza diretta e approfondita della struttura e dell’architettura del DBMS. 2.0 ESEMPI INTRODUTTIVI ALL’USO DI JDBC In questa sezione cercheremo di vedere attraverso alcuni esempi pratici, con frammenti di codice i passaggi fondamentali che un’applicazione dovrebbe seguire (o può seguire) nell’accedere a basi di dati mediante l’interfaccia JDBC. 2.1 Recuperare una connessione Prima di tutto occorre creare una connessione in maniera da poter dialogare col database, inviando comandi sql e ricevendo risultati delle query o delle update. In questo passaggio sono coinvolte tipicamente una classe (java.sql.DriverManager) e due interfacce (java.sql.Driver e java.sql.Connection). La prima parte dell’operazione prevede il caricamento del Driver che si vuole utilizzare: basta una semplice riga di codice. Nel caso ad esempio del driver per Mysql: Class.forName(“com.mysql.jdbc.Driver”); Questa semplice istruzione non fa altro che registrare presso un’istanza della classe DriverManager (creata mediante l’invocazione di questo metodo), il driver specificato. Come è possibile ciò? Basti pensare al fatto che ogni Driver deve contenere una sezione speciale di codice al suo interno detta per l’appunto “static initializer” e che è simile a questa: static { try{ java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { E.printStackTrace(); } } Come si nota viene invocato il metodo registerDriver() della classe DriverManager. La classe DriverManager ha infatti la funzione di mantenere una lista dei vari driver presenti, che possono essere usati di volta in volta dall’applicazione. Oltre al metodo registerDriver(), da segnalare anche deregisterDriver() che consente di rimuovere dalla lista il driver specificato. Per avere infine una lista completa dei driver registrati presso la classe DriverManager basta invocare il metodo DriverManager.getDrivers(). Ora che il driver è registrato e caricato, possiamo passare a negoziare la connessione. Per far questo basta usare una istruzione simile a questa (arricchita in questo caso del controllo delle eccezioni): try { Connection con = DriverManager.getConnection(url_jdbc, username, password); } catch (SQLException exc) { exc.printStackTrace(); } L’url jdbc è un parametro molto importante ed è legato al tipo di driver che stiamo utilizzando. Tipicamente ha una sintassi di questo tipo : jdbc:<protocollo>://<hostname o ipaddress>:<numero_porta>/<nome_db> Un esempio concreto potrebbe essere questo (sempre rifacendoci a Mysql): jdbc:mysql://192.168.0.24:3306/utenti Se tutto va a buon fine e non vengono lanciate eccezioni allora significa che la connessione verso il DBMS è stata aperta con successo e ora possiamo inviare statement SQL. 2.2 Inviare comandi SQL al server Ora possiamo creare degli oggetti del tipo Statement o PreparedStatement e inviare al database una serie di comandi SQL, siano essi semplici query o comandi di update come UDPATE, DELETE, INSERT. Prendiamo per esempio il caso di voler creare la nuova tabella STUDENTI. Ogni entry conterrà svariate informazioni, quali nome e cognome, numero di matricola, indirizzo email e così via. Vediamo il codice SQL come si presenta: CREATE TABLE Studenti ( username VARCHAR(8) NOT NULL, nome VARCHAR(30) NOT NULL, cognome VARCHAR(30) NOT NULL, email VARCHAR(20) NOT NULL, matricola (6) NOT NULL, PRIMARY KEY (matricola)) Questo è un puro e semplice esempio. Ora non ci resta altro da fare che assegnare quel codice che abbiamo scritto poco sopra ad una stringa: String createTableStudenti = codice_sql; Il prossimo passo fondamentale è quello di creare un oggetto Statement che mi permetta di inviare il codice SQL al DBMS. Con gli oggetti Statement sostanzialmente facciamo uso di due metodi: Statement.executeQuery() per inviare le query e ottenere in risposta un ResultSet. Statement.executeUpdate() per inviare comandi di creazione o modifica di tabelle. Per creare un’istanza di Statement, sfruttiamo l’oggetto Connection, quindi: Statement stmt = con.createStatement(); Una volta fatto questo, dovremmo passare all’oggetto il codice SQL da inviare al dbms, nel nostro caso il codice sql definito sopra (la stringa createTableStudenti). Usiamo executeUpdate() visto che ci interessa creare una nuova tabella. Stmt.executeUpdate(createTableStudenti); Fatto questo possiamo passare quindi a inserire i dati all’interno di questa tabella. Lo possiamo fare semplicemente come segue: stmt.executeUpdate(“INSERT INTO Studenti ” + "VALUES (‘mrossi’, ‘Mario’ , ‘Rossi’, ‘[email protected]’, ‘788909')"); E’ chiaro che operazioni del genere possono essere ripetute più volte: nella fattispecie potremmo voler inserire nel database ad esempio una lista di 100 nuovi studenti (iscritti quest’anno), i cui dati sono stati recuperati da un apposito file formattato. E’ chiaro che potremmo tranquillamente studiare, ad esempio un ciclo while con opportuni controlli che legge dal file e inserisci gli studenti mediante operazioni tipo quella che abbiamo visto sopra. Esiste tuttavia una maniera molto più efficace e performante per affrontare situazioni tipo questa: consiste nell’usare oggetti di tipo PreparedStatement. La caratteristica principale è per l’appunto che oggetti di tipo PreparedStatement, a differenza degli Statement, quando vengono creati viene passato loro in input del codice SQL. Il vantaggio sta nel fatto che il codice SQL in questione viene passato al DBMS, e se questo supporta funzionalità di precompilazione, l’oggetto restituito sarà non un semplice statement ma uno statement con codice SQL precompilato. Questo implica che il DBMS non dovrà ricompilarlo una seconda volta nel caso di chiamate del tipo execute() o executeQuery(). Vediamo come si crea un oggetto di tipo PreparedStatement: PreparedStatement insertStudenti = con.prepareStatement( “INSERT INTO Studenti VALUES ( ? , ? , ? , ? ,?)”); Come si nota chiaramente al posto di quelli che dovrebbero essere i valori da inserire nel database sono presenti dei punti di domanda che indicano la presenza di parametri che dovranno essere inseriti con opportuni metodi (come vedremo tra breve). Per impostare i parametri mancanti basta fare quanto segue: insertStudenti.setString(1, “mbianchi”); insertStudenti.setString(2, “Marco”); insertStudenti.setString(3, “Bianchi”); insertStudenti.setString(4, “[email protected]”); insertStudenti.setString(5, “789000”); insertStudenti.executeUpdate(); I parametri vengono impostati mediante l’uso di metodi del tipo setXXX(<numero_colonna>, <valore_da_inserire>); dove XXX dipende sostanzialmente dal tipo di dato da inserire nel dbms, anche in relazione al tipo di dati che questo supporta. Nel nostro caso i VARCHAR corrispondono in pratica al tipo Java String. Infine il tutto viene inviato al database mediante il metodo executeUpdate(). E’ chiaro che non ha molto senso utilizzare i PreparedStatement quando il numero di query/update simili da eseguire sono una o due. Tuttavia quando cominciamo a ragionare in termini di molte operazioni simili, magari sfruttando l’uso di cicli for o while, allora l’uso dei PreparedStatement è la soluzione ottimale. 2.3 Elaborare e manipolare i risultati ottenuti Finora abbiamo visto per lo più comandi di update o modifica al database. Vediamo invece ora qualche esempio di query sql, che hanno come scopo principale quello di ritornare un tabella o meglio un istanza di ResultSet contenente i dati che soddisfano i criteri specificati all’interno della query. Poniamo ad esempio di voler recuperare le informazioni di tutti gli studenti che si chiamano Marco. Ecco come potrebbe essere il codice. String queryName = “SELECT * FROM Studenti WHERE nome = ‘Marco’”; Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(queryName); Ora che abbiamo a disposizione un ResultSet, su di questo possiamo lavorare e muoverci consultando i risultati in esso contenuti e eventualmente utilizzarli a nostro piacimento. Pensiamo al resultset proprio come ad una tabella, o anche volendo ad un array bidimensionale composto da colonne e righe per l’appunto. Nel caso di una query come quella che abbiamo presentato precedentemente otterremmo un ResultSet simile a questo: username nome cognome email matricola mrossi Marco Rossi [email protected] 788999 mivaldi Marco Ivaldi [email protected] 788823 mtorrett Marco Torretta [email protected] 778452 … … … … … … … … … … msarpi Marco Sarpi [email protected] 786310 Per spostarci all’interno del ResulSet non basta far altro che utilizzare uno dei tanti metodi messi a disposizioni dalle API JDBC. Una cosa importante da ricordare è che quando viene creato il ResultSet il cursore è posizionato non sulla prima riga, ma prima di questa: in un certo senso fuori dal ResultSet stesso. Per questo tipicamente il primo metodo che si invoca è next() che consente di posizionarci sulla prima riga del ResultSet e quindi all’interno dello stesso. Spesso per visitare un result set si utilizza un ciclo for o un ciclo while (preferibilmente quest’ultimo). while (rs.next()) { String username = rs.getString("username"); String nome = rs.getString("nome"); String cognome = rs.getString("cognome"); String email = rs.getString("email"); String matricola = rs.getString("matricola"); //altro codice ... ... } Per recuperare i dati dal result set si fa uso di funzioni del tipo getXXX in base al tipo di dato memorizzato nel database. Il parametro delle funzioni getXXX può essere a scelta o il numero di colonna del campo da recuperare oppure il nome della colonna stessa. Nel nostro esempio sopra abbiamo utilizzato quest’ultima soluzione. Attenzione che quando si fa uso della prima soluzione la numerazione delle colonne parte da 1, a differenza di come siamo abituati a considerare gli array in cui la numerazione sappiamo parte da 0. Esistono molti metodi, che come dicevamo consentono di spostarsi tra le righe del result set. Metodi per il posizionamento assoluto o relativo, metodi che permettono di spostarsi all’indietro (invece di next() si usa previous()) e metodi che permettono di posizionarsi direttamente alla prima o all’ultima riga del result set o addirittura prima o dopo delle stesse. Questi metodi sono stati fondamentalmente introdotti a partire da JDBC 2.0. Vediamo i più importanti: boolean absolute(int row); void afterLast(); void beforeFirst(); boolean first(); boolean isAfterLast(); boolean isBeforeFirst(); boolean isFirst(); boolean isLast(); boolean last(); boolean previous(); boolean relative(); Il fatto che questi metodi possano essere utilizzati o meno dipende anche dal tipo di ResultSet che viene restituito, a seconda ad esempio che sia un result set scorrevole (novità JDBC 2.0) o di tipo forward-only. Questa parte introduttiva all’uso di JDBC si conclude. BIBLIOGRAFIA [1] “JDBC Data Access API For Driver Writers”: http://java.sun.com/products/jdbc/driverdevs.html [2] “JDBC 2.0 Core Api”: http://java.sun.com/products/jdbc/jdbc20.pdf [3] “JDBC 2.0 Standard Extension Api”: http://java.sun.com/products/jdbc/jdbc20.stdext.pdf [4] M. FISHER, J. ELLIS, J. BRUCE. “JDBC Api Tutorial and Reference. Third Edition”. Addison Wesley, 2003. [5] G. REESE. “Database Programming with JDBC and Java. Second Edition”. O’Reilly and Associates, 2000. Questo documento può essere liberamente copiato e distribuito da chiunque, ma a nessuno è permesso di cambiarlo in alcun modo. Ogni copia o documento derivato dovrà riportare l'indicazione alla fonte. Qualsiasi commento o suggerimento è benvenuto e può essere inviato al seguente indirizzo: [email protected]. Eventuali correzioni di questa pubblicazione saranno disponibili sul sito internet http://www.techtown.it