MokaByte Numero  36  - Dicembre 99
 
Un SDK per scrivere
driver JDBC
di 
Serafino
Tanfo
L’analisi dell’architettura JDBC consente di delineare una serie di caratteristiche  generali dei driver JDBC che possono essere incapsulate in opportune classi  alleggerendo il più possibile lo sviluppo di nuovi driver


Se vi siete mai cimentati con la scrittura di driver JDBC avrete certamente notato il gran  numero di interfacce che è necessario scrivere. In questo articolo viene presentato un SDK che riduce notevolmente il lavoro

Introduzione
Quasi tutte le applicazioni software hanno a che fare con il trattamento dei dati a prescindere dal linguaggio con cui sono scritte. Per questa ragione Java è stato presto dotato di una apposita API, detta JDBC, contenuta nel package java.sql. JDBC è costituito da una serie di interfacce per l'interrogazione e la manipolazione della base dati e da una classe DriverManager che gestisce i driver di accesso ai diversi DBMS.
Come vedremo nel seguito, tutti i driver JDBC hanno una struttura comune
che è indipendente dal database stesso. Ciò significa che è possibile realizzare un driver JDBC generico o, in altre parole, una sorta di SDK che costituisce la base per implementare driver di accesso alle fonti dati più disparate. Questo è esattamente lo scopo dell’articolo e come esempio realizzeremo un driver JDBC per Mini SQL. Chi avesse bisogno di rinfrescare le proprie nozioni JDBC può farlo attraverso [1].
 
 

Architettura di JDBC
La Figura 1 mostra le varie componenti in gioco con JDBC ed è volutamente ispirata a un precedente articolo su OLE DB [2] apparso sempre su CP. 
 
 
Figura 1 - Architettura di JDBC

(L’idea alla base di questo articolo, infatti, ha diversi punti in comune con l’architettura OLE DB.) Dalla figura si nota immediatamente una separazione logica piuttosto netta tra due famiglie di classi: quella di destra riguarda i driver; quella di sinistra gestisce le connessioni e manipola i dati. DriverManager crea tutte le connessioni al DBMS utilizzando il driver più adatto allo scopo. Quando una connessione ha successo esso restituisce al chiamante un oggetto Connection. Da questo istante è possibile interagire direttamente con il database, impostando e leggendo le proprietà della sessione (tipo di transazioni, meta-dati relativi al database) e creando i vari statement che JDBC consente. Generalmente l'esecuzione di un'istruzione SQL restituisce un insieme di dati che JDBC vede attraverso un oggetto ResultSet. Quest'ultimo è corredato di tutti i metodi necessari per scorrere le righe e ottenere i valori delle colonne. L'architettura è piuttosto semplice e lineare; l'unico punto un po' oscuro è il meccanismo mediante il quale DriverManager individua i driver che sono installati e decide quale tra questi utilizzare.
Durante la propria inizializzazione, DriverManager carica all'interno della virtual machine (VM) i driver JDBC elencati dalla proprietà di sistema jdbc.drivers. Per ognuno di essi crea un’istanza e ne memorizza il riferimento. Questo meccanismo permette di avere sempre a disposizione i driver JDBC utilizzati dalle proprie applicazioni senza dover scrivere il codice per caricare un driver piuttosto che un altro. Esiste un secondo metodo (probabilmente il più utilizzato) che consiste nel caricare esplicitamente la classe per mezzo del class loader. Generalmente gli oggetti che implementano java.sql.Driver sono fatti in modo tale per cui è sufficiente caricare o istanziare la classe per registrare il riferimento nel DriverManager. L'istruzione seguente, per esempio, usa il class loader corrente per caricare il driver sun.jdbc.odbc.JdbcOdbc:

Class.forName("sun.jdbc.odbc.JdbcOdbc");
La classe DriverManager ha anche un'altra importante funzione, cioè quella di instaurare la connessione con il database. Il servizio è offerto dal metodo getConnection che nelle sue varie forme richiede di specificare un URL di connessione. L’URL è importante per due motivi: è l’informazione in base a cui viene scelto il driver da utilizzare e specifica i parametri necessari al driver per aprire la connessione (nome del database, login, password). DriverManager reagisce a una richiesta di connessione interrogando tutti i driver noti alla ricerca di quello che può interpretare correttamente l’URL di connessione. Il formato canonico di un URL di connessione prevede che il protocollo sia "jdbc" e il sottoprotocollo sia specifico per ogni driver; per esempio il driver JdbcOdbcDriver risponde all’URL:
jdbc:odbc:<DSN>
dove <DSN> è un Data Source Name ODBC di sistema associato al database. Per esempio:
 
DriverManager.getConnection("jdbc:odbc:miodb");
Le interfacce di JDBC
Per quanto riguarda il ruolo delle varie interfacce di JDBC, la Figura 1 è già sufficientemente esplicativa. Quando un programma chiede al driver manager di aprire una connessione, questo invoca il metodo connect del driver. La classe concreta dell'oggetto Connection restituito deve essere realizzata dal produttore del driver in base alle specifiche del database; infatti questo è l'oggetto che incapsula la "conoscenza" di come interfacciarsi con il DBMS e che mantiene il riferimento alla connessione aperta. Per esempio potrà utilizzare metodi nativi realizzati sulla base della API di programmazione del database o, in un ambiente client/server, aprire una connessione TCP/IP con il server di database. Per espletare i servizi propri dell'interfaccia Connection, una classe deve implementare diversi metodi, che è possibile suddividere in tre gruppi:
  • Gestione delle transazioni
  • Creazione degli statement
  • Interrogazioni e modifiche dello stato della connessione
Alla prima categoria appartengono commit, getAutoCommit, rollback, getTransactionIsolation, setAutoCommit, setTransactionIsolation. La trattazione di questi metodi esula dagli scopi di questo lavoro e quindi non verranno ulteriormente approfonditi.
Fanno parte della categoria b) le funzioni createStatement, nativeSQL, prepareCall, prepareStatement. Per semplicità nel seguito considererò la sola createStatement. Infine, nella categoria c) si trovano clearWarnings, close, getCatalog, getMetaData, getWarnings, isClose, isReadOnly, setCatalog, setReadOnly. Di questi tratterò i più importanti.
I metodi getMetaData e createStatement hanno una valenza particolare. Il primo fornisce un oggetto DatabaseMetaData. Questa classe deve fornire tutte le informazioni che riguardano le caratteristiche del DBMS. Per esempio si può interrogare un oggetto di questo tipo per conoscere se il motore SQL supporta le stored procedure o se permette di eseguire certi tipi di query o ancora per conoscere le tabelle presenti in un particolare database. Naturalmente una classe DatabaseMetaData non può che essere specifica per ogni sorgente dati.
Più interessante è, invece, createStatement perché fornisce un oggetto che implementa l'interfaccia Statement. Questa è utilizzata per l'esecuzione dei comandi SQL e per la restituzione dei risultati in forma di ResultSet. Naturalmente la realizzazione completa di un oggetto Statement è ancora a carico di chi crea il driver; tuttavia si noti che questa classe non deve interagire con il DBMS se non per comandare l'esecuzione di una query, dopodiché deve semplicemente occuparsi di gestire in modo opportuno altri oggetti di "alto livello", ossia i ResultSet. Sono infatti questi ultimi che restituiscono i dati, indipendentemente dall'implementazione di Statement. Una classe che realizzi ResultSet deve avere diversi metodi che possono ancora una volta essere suddivisi come segue:
  •  Metodi per lo "scorrimento" delle righe del risultato
  •  Metodi per l'estrazione del valore delle colonne di una riga
L'interfaccia ResultSet rappresenta due entità nello stesso momento: da un lato è il corrispettivo JDBC di un cursore di database, cioè di un puntatore alla riga corrente di un insieme di dati in forma tabellare; dall'altro astrae il concetto di "riga" della tabella risultante, che può essere interrogata campo per campo per ottenere i valori di ogni colonna. Come cursore, un oggetto ResultSet deve implementare i metodi di tipo a) next e close. Come riga deve invece implementare quelli di categoria b), cioè la lunga serie di funzioni getXXX. Ancora una volta si può notare che l'interazione con il database è necessaria solo per le funzioni di tipo a); al contrario le altre, una volta ottenuta e memorizzata la riga corrente, non hanno alcun bisogno di interagire con il motore SQL.
Come un database fornisce dei meta-dati per descriversi, così a ogni ResultSet è associato un ResultSetMetaData che dà informazioni su come è fatta la tabella risultante: numero di campi, tipo, dimensione. Il fatto che il Resultset sia creato da un database piuttosto che da un altro, da questo punto di vista, è del tutto irrilevante. Supponendo di partire da tabelle identiche, risultati uguali creati da database diversi hanno comunque la stessa struttura e quindi possono avere la medesima descrizione.
Un'ulteriore considerazione riguarda gli aspetti di multithreading di Java. Dall'architettura presa in esame si ha piena libertà di implementazione, ma realizzare un driver JDBC non thread-safe ne limiterebbe fortemente l'utilizzo e l'utilità. Ancora una volta, però, tutte le problematiche di sincronizzazione di accesso di più thread alle risorse condivise dagli oggetti Connection, Statement, ResultSet sono prettamente dominio di Java e non del DBMS. In altre parole, la gestione della concorrenza sugli oggetti che costituiscono un driver JDBC può essere affrontata in modo generico e non dipendente dal database.
 
 
 

Architettura di un driver JDBC generico
Sulla base di queste analisi possiamo delineare un’architettura più generale per i driver JDBC che sia più svincolata dal DBMS a cui fanno riferimento. Da questa si può poi partire per creare driver per le fonti dati più diverse. Un altro modo di vedere la cosa consiste nel considerare questo driver come un SDK: da solo non è molto utile, ma fornisce una base per realizzare più velocemente le componenti software per le quali è pensato. In quest'ottica, i requisiti del JDBC SDK sono i seguenti:

  • Realizzare un framework che implementi la maggior parte delle specifiche di JDBC in modo indipendente dal database;
  • Essere thread-safe;
  • Mantenere la semplicità e la flessibilità di JDBC;
  • Essere facilmente estendibile (per realizzare un driver JDBC completo deve essere sufficiente implementare poche classi e non tutte quelle delle specifiche JDBC).
Una possibile architettura per realizzare gli obbiettivi preposti è quella illustrata in Figura 2
 
Figura 2 - Architettura del JDBC SDK

 

Lo scopo principale è quello di isolare l'accesso ai dati dal modello di dati astratto realizzato dalle classi di JDBC. Il modello di dati viene realizzato una volta per tutte e si occupa di implementare le specifiche JDBC che riguardano la gestione delle connessioni, degli statement e dei resultset, nonché la restituzione dei dati e dei meta-dati. Tutto questo deve essere realizzato avendo cura degli aspetti di multithreading.
L'elemento centrale di questa strategia è l'oggetto SQLEngine che realizza lo strato intermedio di cui sopra e che fornisce la "vista" SQL della sorgente dati. Questa classe 
deve permettere di:

  • Aprire e chiudere una connessione al database;
  • Eseguire statement SQL;
  • Ottenere le proprietà di connessione;
  • Ottenere i meta-dati del database.
Tali compiti sono codificati nella definizione dell'interfaccia SQLEngine come è possibile osservare dal Listato 1. A seconda del tipo di JDBC driver che si vuole realizzare i metodi di SQLEngine possono essere nativi, completamente in Java o un mix tra i due.
Una seconda e ultima componente intermedia introdotta dall'SDK è l'interfaccia Cursor (Listato 2). Questa rappresenta il comportamento di un cursore di database e viene creata quando l'esecuzione di una query restituisce un risultato in forma tabellare. L'introduzione di questo elemento rende meno ambiguo il ruolo di ResultSet in quanto separa le funzioni di interrogazione dei valori delle colonne della riga corrente dalla gestione dell'insieme delle righe. In questo modello un ResultSet è costituito da due elementi: un ResultSetMetaData che descrive la struttura della tabella e un Cursor che permette di ottenere le singole righe del risultato. Queste ultime sono incapsulate in un oggetto di tipo BaseRow avente il compito di nascondere la struttura dati di memorizzazione dei valori delle colonne di una riga e di esporre i metodi per estrarli.
Vediamo adesso il ruolo delle classi del package ste.jdbc.SDK.
 
 

JDBC SDK
La realizzazione di un driver JDBC comporta l'implementazione di diverse interfacce del package java.sql. Un driver minimale, come quello presentato in questo articolo, deve creare le classi per almeno le seguenti interfacce: Driver, Connection, Statement, ResultSet, DatabaseMetaData, ResultSetMetaData. Data la mole di codice, non è possibile entrare nel dettaglio delle singole classi e quindi si rimanda ai sorgenti disponibili sul sito FTP di Infomedia.
Le classi facenti parte dell'SDK sono generalmente identificate dal prefisso "Base" (BaseDriver, BaseConnection, BaseStatement ecc.) a indicare che sono i mattoncini basilari con i quali costruire i propri driver.
È fondamentale l'installazione del driver. Come già detto, esso si deve "presentare" al driver manager non appena la classe è caricata nella VM, quindi le istruzioni di caricamento devono essere poste all'interno di un blocco di codice di inizializzazione "di classe". Con questa espressione si intende la parte di codice compresa nello scope di un modificatore static, ma all'esterno di tutti i metodi. La registrazione del driver avviene grazie al metodo DriverManager.registerDriver:
 

public class BaseDriver implements java.sql.Driver {
 ...
 static {
  try {
   DriverManager.registerDriver(new BaseDriver());
  }
  catch (Exception e) {
   /* non può generare eccezioni */
  }
 }
}
Tra i metodi che un oggetto Driver deve possedere, i più interessanti sono acceptsURL e connect. Entrambi sono invocati da DriverManager quando un programma richiede una connessione al database. L'identificazione di una risorsa viene effettuata attraverso un URL. acceptsURL deve verificare se l’URL utilizzato per la connessione è in forma comprensibile dal driver in questione e di conseguenza restituire true o false. Se il driver manager ottiene una risposta negativa cercherà un altro driver che possa gestire tale tipo di connessione. Nel caso di BaseDriver, l’URL ha la sintassi:
 
jdbc:jdbcSDK:<sqlengine>:<parametro1>=<valore>:...:<parametroN>=valore


e quindi
 

public boolean acceptsURL(String url) {
  return url.startsWith("jdbc:jdbcSDK:");
}


Il driver manager invoca connect quando acceptsURL restituisce true e riceve in ingresso l’URL di connessione e un oggetto Properties contenente eventuali proprietà fornite esplicitamente da getConnection. A queste si devono aggiungere quelle specificate nell’URL dalle coppie <parametro>=<valore>. Inoltre, tra le proprietà incluse di default da DriverManager nel parametro prpInfo ci sono, se si usa la chiamata getConnection(URL, user, password), anche lo username e la password dell'utente con cui l'applicazione si deve presentare al database server.
Oltre a ciò, il metodo connect di BaseDriver crea l'oggetto SQLEngine che finalmente apre la connessione:
 

public Connection connect(String stURL, Properties prpInfo) throws SQLException {
 try {
  parseURL(stURL, prpInfo);
 }
 catch (MalformedURLWarning muw) {
  return null;
 }

 Class cls = null; Connection c = null;
 try {
  cls = Class.forName(prpInfo.getProperty("source", ""));
  SQLEngine sql = (SQLEngine)cls.newInstance();
  c = (Connection)sql.connect(prpInfo);
 }
 catch (Exception e) { ... }
 return c;
}


Come si vede, il motore SQL della sorgente dati viene caricato dinamicamente a partire dal nome della classe. Di fatto, ciò rende indipendente BaseDriver dalla realizzazione dei metodi di accesso al database che vengono forniti da una specifica implementazione dell'interfaccia SQLEngine.
Il metodo connect di SQLEngine deve costruire e restituire un oggetto Connection che descriva lo stato e le caratteristiche della connessione. Per facilitare il programmatore, il JDBC SDK ne fornisce una versione pronta all'uso: BaseConnection. Questa memorizza le proprietà della connessione e il riferimento all'istanza di SQLEngine che viene utilizzato dai metodi che interagiscono con il DBMS. Tra questi vi sono i già citati createStatement() e getMetaData():
 

public Statement createStatement() throws SQLException {
  return (Statement)new BaseStatement(sql);
}
public DatabaseMetaData getMetaData() throws SQLException {
  return (DatabaseMetaData)sql.getMetaData();
}


Una volta creato un BaseStatement, questo può essere utilizzato per eseguire uno o più comandi SQL. È possibile che più thread di uno stesso programma condividano la stessa istanza di un oggetto BaseStatement. Inoltre lo stesso oggetto può essere utilizzato più volte per eseguire query diverse. Per questo BaseStatement possiede il flag fOccupato che indica che l'oggetto in questione ha già eseguito una query e i Resultset contenuti nell'array ars ne costituiscono i risultati. Tali dati vengono liberati alla chiusura del Resultset.
I metodi per eseguire gli statement SQL sono executeQuery, executeUpdate ed execute tutti basati sul metodo execute di SQLEngine. Uno statement SQL può fornire uno o più risultati nel caso di executeQuery o il numero di righe interessate dal comando nel caso di executeUpdate. In entrambi i casi si impostano opportunamente i valori di ars e iContaRS che vengono poi utilizzati da getMoreResults, getResultSet, getUpdateCount. I metodi executeXXX ricevono in ingresso la query da eseguire sotto forma di stringa. Tale stringa viene passata così com’è al motore SQL. Anche per l'interfaccia ResultSet il JDBC SDK mette a disposizione una versione base della classe: BaseResultSet. Un Resultset può essere visto come un ResultSetMetaData associato a un Cursor. Cursor scorre le righe del result set restituendo quella corrente attraverso un'operazione di fetch. In realtà, per motivi di efficienza, molti database permettono di ottenere più righe del risultato con una sola operazione di fetch. Per questo il metodo fetchRows di Cursor restituisce un array di righe sotto forma di oggetti BaseRow anziché una singola riga. Questi oggetti vengono memorizzati nell'apposito array righe di BaseResultSet così da poter essere acceduti singolarmente mediante il contatore iRigaCorrente. Il metodo che permette di scorrere le righe è next:
 

public synchronized boolean next() throws SQLException {
  if ((iRigaCorrente+1) == righe.length) {
    righe = cursore.fetchRows();
    iRigaCorrente = -1;
  }
  ++iRigaCorrente;
  return (righe.length > iRigaCorrente);
}


next esegue il fetch solo se l'ultima riga recuperata è l'ultima riga nel buffer, mentre se fetchRows restituisce un array di dimensione 0 significa che non ci sono più righe.
BaseResultSet, come tutte le classi di tipo ResultSet, possiede numerosi metodi getXXX per l'estrazione dei valori delle singole colonne della riga corrente. Questi non fanno altro che invocare l'analogo metodo dell'oggetto BaseRow. Per esempio, per ottenere un valore sottoforma di stringa si usa getString:
 

public synchronized String getString(int columnIndex) throws SQLException {
  valida(); // il result set e' valido ?
  return righe[iRigaCorrente].getString(columnIndex);
}


Un'ultima importante funzionalità di BaseResultSet è quella di fornire i meta-dati del result set. Di questo si occupa getMetaData che non fa altro che restituire l'oggetto ResultSetMetaData fornito al costruttore. Ancora una volta il JDBC SDK offre un'implementazione di base di questa classe. Essa è utilizzabile per realizzare SQLEngine senza nessuna modifica. Infatti non si avvale di funzionalità specifiche di un database, ma si basa su una descrizione della tabella in termini di oggetti BaseColumn. In pratica, per rappresentare l'intestazione del Resultset impiega un array di oggetti BaseColumn, ognuno dei quali ha tutte le informazioni utili per descrivere un singolo campo della tabella, cioè nome, etichetta, dimensione di visualizzazione, tipo, precisione e scala, schema e tabella di appartenenza, attributi.
A questo punto la domanda è: "Cosa devo fare per realizzare un JDBC driver?" Basta implementare tre interfacce: SQLEngine, Cursor e DatabaseMetaData. Nel paragrafo seguente ne verrà data prova concreta realizzando un driver JDBC per accedere a database Mini SQL.

 

Un driver JDBC per Mini SQL
Mini SQL, o come spesso viene chiamato mSQL, è un DBMS relazionale studiato per accedere velocemente a dati relazionali, con il minor overhead di sistema possibile. Esso è costituito da un server al quale le applicazioni client si possono connettere via socket TCP/IP. mSQL è disponibile su varie piattaforme tra cui Unix, Windows e OS/2. Maggiori informazioni sono su www.hughes.com.au.
Il driver di questo esempio utilizza l'API nativa di mSQL per accedere al DBMS ed è quindi un quasi-JDBC driver di livello 2. Dico "quasi-JDBC" perché per conformarsi pienamente alle specifiche, anche il database deve possedere determinate caratteristiche di base che mSQL non possiede. Inoltre, questa versione dell'SDK non include alcune specifiche JDBC come i prepared e i callable statement. Le classi che costituiscono il JDBC driver per mSQL sono: mSQLEngine, mSQLCursor, mSQLDatabaseMetaData. Tra queste, l'ultima è la più semplice perché richiede la realizzazione di una serie di metodi che forniscono informazioni specifiche sul database. Conoscendo le caratteristiche del database non ci sono particolari problemi.
La classe mSQLEngine implementa SQLEngine e quindi realizza lo strato di collegamento con il DBMS. Oltre a fornire l'implementazione dei metodi dell'interfaccia (connect, close, execute, getConnectionParameter, getMetaData), essa fornisce alcuni metodi aggiuntivi di utilità generale, utilizzati anche da mSQLDatabaseMetaData. Tra questi, listDB elenca tutti i database presenti, get/setDB restituisce/imposta il database corrente, listTables restituisce la lista delle tabelle presenti nel database corrente, listFields fornisce i campi di una tabella.
Per usare il driver all'interno del proprio programma basta utilizzare la giusta URL di connessione:
 

Connection conn = null;
String url = "jdbc:jdbcSDK:ste.jdbc.msql.mSQLEngine:db=test";
try {
  conn = DriverManager.getConnection(url, "ste", "stepwd");

catch (Exception e) {
  e.printStackTrace();

finally {
  if (conn != null) conn.close();
}


Il driver mSQL richiede che si specifichi nella stringa di connessione il database sul quale si vuole lavorare (db=test), mentre le informazioni di login vengono fornite direttamente al metodo getConnection.
In Figura 3 si può vedere il driver all'opera con una semplice applicazione di esempio fornita con swing (eseguita in ambiente OS/2 Warp con IBM JVM), la nuova API di componenti GUI che verrà inclusa nella JDK 1.2.
 
 
 
Figura 3 -  Esempio di utilizzo del driver JDBC per mSQL con swing

 
 
 

Conclusioni
Il semplice framework presentato in queste colonne è sicuramente a uno stadio embrionale, ma permette già di realizzare un JDBC driver scrivendo un numero relativamente basso di linee di codice. Ci si potrebbe chiedere quale sia l'utilità di creare driver JDBC dal momento che molti costruttori di database ne forniscono la loro versione ufficiale. Il punto fondamentale è che non è necessario che un driver JDBC acceda a un DBMS vero e proprio. In questo articolo si è mostrata la semplicità con cui si può costruire un driver per l'accesso a svariate fonti dati. L'unico vincolo è che i dati possano essere rappresentati in forma tabellare e che si abbia un linguaggio di interrogazione per manipolarli. Ciò è particolarmente interessante in quanto non è affatto necessario che tale linguaggio sia un vero e proprio SQL. Il metodo execute di SQLEngine può, in verità, utilizzare un proprio parser per interpretare il comando e restituire Resultset. Questo rende estremamente flessibile il modello di accesso ai dati basato su JDBC, pur mantenendo un elevato grado di semplicità.
 

Bibliografia
[1] Michele Sciabarrà - "Java: il JDBC", Computer Programming, Settembre 1997
[2] Natale Fino - "Architettura di OLE DB: verso l'accesso unificato ai dati", Computer Programming, Febbraio 1998
 

Serafino Tanfo è laureato in Ingegneria Informatica e attualmente si occupa di applicazioni web. 
Può essere contattato tramite e-mail all'indirizzo serafino_tanfo@bigfoot.com

MokaByte rivista web su Java

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