Introduzione a JDBC Carpini Dania, Oliva Giuseppe, Petruzzi Marianna Commenti ai lucidi Lucido 1 (premessa) Dopo avere analizzato in dettaglio, durante il corso di Basi di Dati Distribuite, l'API ODBC, pensata per consentire la scrittura di applicazioni in maniera indipendente dal DBMS, introduciamo uno strumento analogo sviluppato successivamente: l'API JDBC. Siamo quindi nell'ambito dei gateway per basi di dati. Lucido 2 JDBC è una API scritta intermante in linguaggio Java che implementa, come ODBC, lo standard SQL CLI (Call Level Interface), il quale definisce l'insieme di funzioni per dialogare con le basi di dati. La versione attuale dell'API JDBC è la 3.0, inclusa nella libreria standard del J2SE, e le cui classi sono suddivise in due package: java.sql e javax.sql. Lucidi 3 e 4 Mentre ODBC è scritta in linguaggio C, per cui una sua implementazione è dipendente dalla piattaforma per la quale essa viene compilata, JDBC sfrutta le caratteristiche di portabilità proprie del linguaggio Java, per cui può essere considerata come una versione portabile di ODBC. Inoltre ODBC è stata concepita per essere interfacciabile con svariati linguaggi di programmazione, ma questa "eterogeneità di linguaggio" si paga in termini di una maggiore complessità della API, soprattutto nello scambio dei dati con il DB (vedi conversione dei tipi SQL). JDBC invece, essendo pensata per integrarsi con un unico linguaggio, semplifica molto l'interazione tra l'applicazione e la API. Sia ODBC che JDBC permettono di richiamare funzionalità specifiche di un particolare DBMS. In generale tale capacità non è una scelta consigliabile, poiché compromette la portabilità dell'applicazione (dal punto di vista del DBMS utilizzato), andando anche contro la filosofia alla base di questi strumenti. Lucido 5 Un'implementazione di JDBC consiste in un DriverManager e un insieme di Driver. Le applicazioni Java (client) richiamano le funzioni messe a disposizione dall'API, le quali vengono passate al driver opportuno dal DriverManager. Quest'ultimo ha il compito di creare e gestire le associazioni tra applicazioni e driver. Le principali classi presenti nell'API JDBC sono delle "interface", dei cui metodi va fornita un'implementazione effettiva. I driver realizzano esattamente ciò, e hanno il compito di tradurre le chiamate JDBC in richieste al particolare DBMS; essi sono solitamente forniti dagli stessi produttori dei DBMS, ma anche da terze parti. Lucidi 6 e 7 Esistono 4 tipologie di driver JDBC. 1) Driver nativi: sono scritti in un linguaggio dipendente dalla piattaforma (es. C); 2) JDBC/ODBC Bridge: questo tipo di driver si occupa di tradurre le chiamate JDBC in chiamate ODBC, ed è particolarmente utile per interfacciarsi con tutti quei DBMS per i quali non sono ancora disponibili driver JDBC; è inoltre l'unico driver fornito insieme alla libreria Java standard; 3) Middleware-server: serve a tradurre le richieste JDBC in un formato indipendente dal DBMS; le richieste così tradotte sono poi interpretate da uno strumento di middleware, solitamente un'applicazione server Java, che le riformula ad un DBMS specifico; 4) Driver Java: è un'implementazione diretta delle interfacce della API; quindi si tratta di un driver interamente scritto in linguaggio Java; Per quanto detto si capisce che le soluzioni 3 e 4 sono le uniche a poter garantire la completa portabilità dell'applicazione. Lucido 8 Lo schema di utilizzo di JDBC è quello classico previsto dall'SQL CLI, che verrà descritto nei lucidi seguenti. Bisogna tenere presente che le classi che implementano le funzionalità di base fanno parte della Core API, per cui è necessario importare il package java.sql. Lucido 9 Il passo preliminare al collegamento ad un DB è quello di "caricare" il driver. Al momento in cui si richiede una connessione ad un DBMS il DriverManager cerca il driver all'interno di una lista contenente quelli disponibili nel sistema. Non è necessario inserire esplicitamente il driver in tale elenco, in quanto è sufficiente forzare il caricamento della classe corrispondente (la classe Driver) per far sì che il driver si "auto-registri" al DriverManager. Il metodo da invocare è forName della classe Class, al quale va passato come argomento il percorso (valido a partire dal CLASSPATH) della classe che implementa il Driver. Lucidi 10 e 11 A questo punto è possibile richiedere una connessione, tramite il metodo static getConnection della classe DriverManager, al quale si devono passare tre argomenti di tipo String: l'URL per la connessione, la cui sintassi è descritta nel lucido 11 e che varia a seconda del driver; uno UserName e una Password necessari per l'autenticazione al DBMS (se non necessari possono essere delle stringhe vuote). Il metodo restituisce un oggetto di tipo Connection, che rappresenta un canale di comunicazione con il DB, sul quale, una volta terminate le operazioni, è consigliabile richiamare esplicitamente il metodo close, poiché il Garbage Collector della Java Virtual Machine non ha la facoltà di rilasciare risorse esterne alla memoria centrale (come quelle occupate sul DBMS). Lucidi 12 e 13 Per poter inviare comandi al DBMS e ricevere le conseguenti risposte è necessaria la creazione di un oggetto Statement, tramite il metodo createStatement della classe Connection. Esistono due differenti metodi da utilizzare, a seconda del tipo di comando che si vuole eseguire: executeUpdate ed executeQuery, descritti in dettaglio nel lucido. In realtà è presente un ulteriore metodo execute, che è utile quando non si conosce a priori se si deve effettuare una interrogazione o un comando di inserimento/modifica: questo metodo restituisce quindi un valore booleano true se è stata eseguita una query, false altrimenti; è possibile a questo punto richiamare uno dei due metodi getResultSet e getUpdateCount rispettivamente nel primo e nel secondo caso. Come si può notare dagli esempi del lucido 13, non è necessario inserire il carattere terminatore per i comandi SQL che si inviano al DBMS, poiché è il driver ad occuparsi di ciò. Questa particolarità è dovuta al fatto che DBMS diversi possono usare caratteri terminatori diversi. Lucidi 14 e 15 Quando si esegue una query (SELECT) viene restituito un oggetto ResultSet, mediante il quale si possono recuperare le righe ottenute in risposta, nello stesso ordine con cui vengono restituite dal DBMS. All'insieme dei risultati, che è una tabella composta da varie righe, è associato un cursore che tiene conto della riga corrente. Inizialmente tale cursore è posizionato su una riga fittizia che precede la prima riga del risultato. Richiamando il metodo next si fa avanzare il cursore alla riga successiva: tale metodo restituisce true se tale riga è presente, false altrimenti. Una volta posizionato il cursore su una riga, si richiamano i metodi getXXX (XXX= Int, String, Float, ...) per recuperare i valori dei campi selezionati. Questi metodi ricevono come parametro o il nome della colonna di interesse o la posizione all'interno della tabella risultato (partendo da 1). La seconda modalità è particolarmente utile quando non si vuole codificare direttamente nel codice dell'applicazione i nomi delle colonne del DB. Lucido 16 I metodi getXXX a disposizione sono molteplici e permettono la conversione dei valori SQL recuperati dal DB in valori JAVA leciti. Quella riportata in questo lucido è un sottoinsieme della tabella generale presente sulla documentazione ufficiale del J2SE. In tabella sono indicate con una X più marcata le conversioni consigliate, mentre con quelle meno marcate si indicano conversioni comunque lecite. Lucido 17 Un'applicazione Java può mantenere contemporaneamente più connessioni a più database (o eventualmente allo stesso) e per ciascuna di esse può avere diversi Statement, che a loro volta però possono tenere aperto al più un oggetto ResultSet. Quest'ultima caratteristica di JDBC è dovuta al fatto che ad ogni Statement è associato un unico canale di comunicazione con il DBMS, che rimane aperto fino a quando non sono state recuperate tutte le righe del risultato (oppure si chiude esplicitamente il ResultSet con il metodo close); se infatti si esegue una nuova query su uno Statement prima di avere recuperato le tutte le righe di una query precedente, le nuove informazioni sovrascrivono le vecchie. Lucido 18 A partire dalla versione 2.0 di JDBC si possono ottenere dei ResultSet con maggiori funzionalità, richiamando la versione con due parametri del metodo createStatement: 1) scrollableResultSet: la scansione delle righe risultato di una query può essere fatta in entrambe le direzioni (richiamando i metodi previous e next); consentono inoltre il posizionamento assoluto su una particolare riga e altre funzionalità; 2) updatableResultSet: mettono a disposizione anche una serie di metodi updateXXX con i quali è possibile modificare i valori dei campi sulla riga corrente, facendo sì che i cambiamenti si riflettano automaticamente sulle tabelle del database. Perché un ResultSet sia updatable ci sono delle restrizioni dovute al tipo di query che si effettua: essa deve fare riferimento ad una sola tabella, non deve contenere operazioni di join o calusole group by, deve selezionare necessariamente anche la chiave primaria della tabella, ecc. Lucido 19 Molti dei metodi presenti nelle classi finora descritte possono sollevare eccezioni di tipo SQLException: negli oggetti di questo tipo sono memorizzate anche informazioni sul tipo dell'errore che ha causato l'eccezione, è presente infatti sia il codice previsto dallo standard XOPEN o SQL 99 sia il codice d'errore del particolare DBMS. Come tutte le eccezioni Java, esse vanno gestite all'interno dell'applicazione con un blocco try-catch. A volte è utile avere informazioni sul database che si sta utilizzando, come il nome, il numero massimo di connessioni concorrenti verso di esso, il nome e la versione del driver: queste e altre informazioni possono essere recuperate da un oggetto di tipo DataBaseMetaData, ottenibile dalla connessione. Da un ResultSet è invece possibile recuperare, tramite la creazione di un oggetto ResultSetMetaData, informazioni come il numero, il nome, il tipo delle colonne restituite da una query. Lucidi 20, 21 e 22 Una transazione è una sequenza di comandi logicamente indivisibili, i quali devono essere eseguiti come un'unità atomica. Per permettere la gestione delle transazioni i sistemi mettono a disposizione i comandi commit e rollback: con il primo si rendono permanenti gli effetti delle operazioni eseguite (la transazione è andata a buon fine), con il secondo vengono invece annullate (la transazione non è andata a buon fine). Quando si ottiene una connessione al DB mediante le classi del package java.sql, questa è in modalità autocommit. Ciò significa che il commit viene eseguito automaticamente al completamento dei comandi. I comandi di inserimento/modifica e quelli DDL sono considerati completi appena vengono eseguiti, mentre le interrogazioni lo sono soltanto una volta recuperate tutte le righe del risultato. Per gestire manualmente il commit dei comandi, si deve innanzitutto disabilitare l'autocommit, quindi richiamare sulla connessione i metodi commit e rollback al momento opportuno. È inoltre possibile creare dei checkpoint durante una transazione, cioè dei punti di ancoraggio a cui ritornare (nel senso delle modifiche al DB) con un'operazione di rollback. Per ottenere ciò si crea un oggetto SavePoint sulla connessione, il quale può essere passato come parametro al metodo rollback. Nel caso in cui si richiami la versione di rollback senza parametri vengono disfatte tutte le operazioni successive all'ultimo commit o rollback. Impostando inoltre il livello di isolamento delle transazioni si può stabilire il grado di concorrenza tra esse. La scelta del livello, tra i 5 disponibili (5 costanti definite nella classe Connection), si effettua in base a quali operazioni "pericolose" si vogliono concedere (dirty reads, phantom reads, nonrepetable-reads, ...). I livelli sono quelli standard previsti dall'SQL. Lucido 23 Il package javax.sql fornisce funzionalità più avanzate rispetto a java.sql, come i pool di connessioni e le transazioni distribuite. Per sfruttare tali potenzialità è necessario ottenere la connessione al DB mediante l'interfaccia DataSource invece che nel modo precedentemente descritto. I pool di connessioni sono un meccanismo per riutilizzare delle connessioni esistenti, piuttosto che crearne di nuove ad ogni richiesta; poiché la creazione di nuove connessioni è un'operazione particolarmente costosa, questo meccanismo permette un notevole incremento delle prestazioni. Con le transazioni distribuite è possibile effettuare transazioni che coinvolgono più database server; in questo caso le connessioni ai DBMS non sono in modalità autocommit e non è consentito chiamare i metodi commit e rollback esplicitamente, poiché tutte le operazioni sono gestite da un Transaction Manager. Un aspetto particolarmente importante è che l'uso di questi meccanismi avviene in maniera trasparente all'applicazione, poiché essi sono implementati ad un livello sottostante.