Introduzione
Molte
volte l’ostacolo più grande nell’accettazione
di una tecnologia risiede nella sua parziale o completa
incompatibilità con le preesistenti tecniche,
per cui un eventuale utilizzo di un nuovo strumento
informatico comporta uno sforzo notevole di re-engineering
della parte implicata, il che automaticamente ne innalza
i costi.
É
proprio quello che é successo con i Database
ad Oggetti, che richiedono l’utilizzo di un nuovo
prodotto ed il redesign (a volte quasi completo) degli
schemi relazionali: se si pensa a legacy System di
vecchia data, notevole mole e di elevata disponibilità
(e.g. 24h sette giorni su sette), come ad esempio
le Banche Dati in sistemi informativi operanti in
ambito finanziario, si vede subito come una strategia,
che implichi un oneroso redesign, comporti dei costi
e dei tempi insostenibili.
In
questi articoli viene descritto un approccio alternativo
in cui vengono effettivamente usati Oggetti nel Database,
ma viene lasciato inalterato il modello relazionale
preesistente. Ovviamente non si tratta di una panacea
e ha i suoi limiti, svantaggi e caveat, ma é
sicuramente una soluzione scalabile, modulare ed efficace,
nonché in buona parte automatizzabile (il che
visti i Time-to-Market attuali non guasta per niente).
SQL3
Objects
Il
DBMS di Oracle dispone già da diverso tempo
della possibilità di creare tipi definiti dall’utente
e di utilizzarli come colonne nelle tabelle, di manipolarli
nelle query e nel codice PL-SQL. Questo approccio
ha gli svantaggi summenzionati ed all’atto pratico,
su stessa ammissione della casa produttrice, risulta
poco efficiente a livello di Performance e problematico
(in particolare su macchine Linux o Windows).
D’altronde
la presenza di oggetti direttamente nel DB ha una
notevole attrattiva: basti pensare ad aspetti come
il design, in cui il Business Object Model può
venire riutilizzato nel design della base relazionale
di dati, oppure al più elevato livello di astrazione
che si può raggiungere nell’interfaccia con
il DB. La soluzione qui proposta consiste nel definire
questi tipi di dati in Oracle come oggetti SQL3 (ovvero
tipi di dati SQL definiti dall’utente e conformi allo
standard SQL3) senza salvarli fisicamente nelle tabelle,
con il vantaggio di non dover modificare minimamente
lo schema relazionale sottostante.
Come
si fa a creare un oggetto SQL3? Semplicissimo: così
come l’istruzione DDL create table definisce una nuova
tabella, analogamente create type <nome_tipo> as
object definisce un nuovo tipo di dato. Ad esempio
le seguenti righe di codice SQL
CREATE
OR REPLACE TYPE O_UTENTE AS OBJECT (
NOME VARCHAR2
(16),
COGNOME VARCHAR2 (16),
DATA_DI_NASCITA DATE,
CODICE NUMBER (9)
);
crea
un nuovo tipo di dato ’O_UTENTE’ che ha come attributi
due stringhe (nome e cognome) una data (di nascita)
ed un codice numerico. Il tipo di dato così
creato é un oggetto a tutti gli effetti: ha
un costruttore (o più di uno), può avere
dei metodi ed i suoi attributi sono riferibili come
<nome_istanza>.<nome_attributo>.
Ad
esempio le seguenti righe di codice SQL sono perfettamente
corrette:
SELECT
O_UTENTE( ESURNAME, ENAME, BIRTHDATE, CODE)
FROM
EMPLOYEE
WHERE
ENAME LIKE 'SCOTT';
É
ben lungi dallo scopo di quest’articolo la descrizione
dell’uso di tipi di dati SQL definiti dall’utente
in Oracle: infatti, l’argomento é vastissimo
e la documentazione Oracle a riguardo davvero ben
fatta. Per maggiori dettagli si vedano quindi i riferimenti
riportati nel paragrafo
Resources.
Wrapper
Dal
lato Java però, necessitiamo di una controparte
in grado di interpretare e manipolare questi nuovi
tipi di dati SQL: così come un’istanza di java.lang.String
rappresenta un Varchar2, abbiamo bisogno di una classe
Java che possa essere usata in uno Statement o letto
da un ResultSet, vale a dire di un Wrapper. Fin dalle
prime versioni del JDBC 2.0, il JDK offre la possibilità
di leggere tipi di dati SQL definiti dall’utente tramite
l’interfaccia java.sql.Structs, meccanismo sicuramente
funzionante, ma poco utile a mio avviso a livello
pratico. Sempre JDBC 2.0 introduce un’altra interfaccia
java.sql.SQLData che permette di scrivere il proprio
Wrapper semplicemente come una classe che implementi
quest’interfaccia.
SQLData
espone tre metodi:
- public
String getSQLTypeName()
- public
void readSQL(SQLInput stream, String typeName)
- public
void writeSQL(SQLOutput stream)
Il
primo metodo viene chiamato dal driver JDBC solo in
fase di scrittura nel DB dell’oggetto: la stringa
restituita deve essere esattamente identica (Case
Sensitive!) al nome con cui abbiamo dichiarato l’oggetto
nel DB (nell’esempio precedente “O_UTENTE”). Se avessimo
scritto “O_Utente” o qualcosa di simile, il driver
avrebbe riportato un’eccezione del tipo “Oggetto XYZ
non dichiarato”.
In
fase di lettura, il meccanismo d’associazione Oggetto
SQL3-Wrapper é differente: in pratica si tratta
i fornire all’oggetto java.sql.Connection oppure java.sql.Statement
una Mappa dei Tipi (TypeMap) che altro non é
se non una HashTable contenente come chiavi i nome
dei tipi di dato e come valore la classe Java del
Wrapper. Se ad esempio il nostro Wrapper si chiamasse
UtenteWrap, avremmo:
Hashtable
typeMap = new Hashtable();
typeMap.put(“O_UTENTE”, UtenteWrap.class);
[...]
Nel
caso d’oggetti annidati, come vedremo meglio più
avanti, devono essere dichiarati tutti i tipi di dato
utilizzati dall’oggetto in questione.
Il
nome del nostro oggetto SQL3 lo troviamo anche come
parametro nel metodo readSQL : quando una Stored Procedure
oppure un SQL Statement restituisce un tipo “O_UTENTE”,
Oracle incapsulerà il nostro oggetto in uno
Stream SQLInput e chiamerà il metodo readSQL
con parametri lo Stream contenente l’oggetto e la
Stringa “O_UTENTE”.
A
questo punto dobbiamo leggere semplicemente dall’SQLInput
Stream i dati necessari, usando i metodi readXXX,
in altre parole:
public
void readSQL(SQLInput stream,String typeName){
String
nome = stream.readString();
String
cognome = stream.readString();
java.sql.Date
data_di_nascita = stream.readDate();
long
codice = stream.readLong();
[...]
}
Importante:
la sequenza con cui vengono letti gli attributi dell’oggetto,
deve essere esattamente uguale all’ordine in cui erano
stati dichiarati in SQL. In fondo é esattamente
come nella serializzazione a file di oggetti Java:
solo che lo Stream usato questa volta é di
tipo SQL .
In
maniera analoga funziona il metodo writeSQL, con due
per così dire particolarità. La prima
é che, contrariamente a quanto ci si poteva
aspettare, l’ordine con cui si scrivono gli attributi
verso l’SQLOutputStream é esattamente lo stesso
con cui venivano letti. La seconda, già anticipata,
é che il nome dell’oggetto SQL3 che corrisponde
al Wrapper attuale, viene letto dal driver tramite
la chiamata a getSQLTypeName: dopodiché il
driver si limita a passare la referenza ad un SQLOutputStream,
aspettandosi che l’implementazione del metodo si occupi
di inserire gli appropriati valori nel giusto ordine.
Ad esempio:
public
void writeSQL(SQLOutput stream){
Employee obj = (Employee)_obj;
stream.writeLong(obj.empno);
stream.writeString(obj.ename);
stream.writeString(obj.job);
stream.writeInt(obj.mgr);
stream.writeDate(obj.hiredate);
stream.writeDouble(obj.sal);
stream.writeDouble(obj.comm);
stream.writeLong(obj.deptno);
}
In
verità molto semplice: basta scrivere uno dopo
l’altro gli attributi nello stream.
I
più attenti si saranno accorti di due problemi:
- Da
dove viene l’istanza _obj di tipo Employee nel metodo
writeSQL?
- Come
faccio ad estrarre dal Wrapper l’istanza letta dall’SQLInputStream?
Ci
sarebbero molti modi, in effetti, ma la soluzione
qui proposta é di scrivere semplicemente una
classe astratta Wrapper.java da cui poi verranno derivate
tutte le altre classi xxxWrapper.
In
questa classe, che implementerá l’interfaccia
java.sql.SQLData, compariranno due oggetti, di cui
uno sarà il generico oggetto letto dal DB o
da scrivere nel medesimo, l’altro il nome del tipo
SQL3 creato in Oracle. In codice:
public
abstract class Wrapper implements java.sql.SQLData
{
protected Object _obj;
protected String _type;
public Object getWrappedObj()
{
return _obj;
}
public String getSQLTypeName()
{
return _type;
}
}
Completato
così il Wrapper, non ci resta che usarlo. Ricordandoci
che il Wrapper altro non é che la contro parte
Java di un tipo di dato SQL e che quindi possiamo
leggerlo da un ResultSet oppure specificarlo
come Input od Output Parameter in un Callable Statement.
Esempio:
statement.registerOutParameter(1, OracleTypes.STRUCT,
“O_EMPLOYEE”);
statement.execute();
EmployeeWrapper wrap = (EmployeeWrapper)statement.getObject(1,
type_map);
Una
dettagliata descrizione dell’uso di una classe che
implementi SQLData si trova nella documentazione dei
Driver JDBC di Oracle citata nel paragrafo
Resources.
Esempio
N.B.
Il seguente esempio non é esaustivo, né
si propone come riferimento completo e perfetto, in
quanto manca di una vera gestione degli errori (error.printStackTrace()
mi sembra un poco inutile...), é scritto in
maniera ‘semplicistica’ in quanto si vuole solo mostrare
l’uso di una tecnica.
Nel
nostro primo esempio, vogliamo leggere un oggetto
di tipo ‘Employee’, così come viene definito
nelle Tabelle del database di default di Oracle. Dalla
Tabella ‘EMP’ dello schema ‘Scott’, possiamo definire
l’oggetto ‘O_EMPLOYEE’ nel seguente modo:
CREATE
OR REPLACE TYPE O_EMPLOYEE AS OBJECT (
EMPNO
NUMBER (4) ,
ENAME
VARCHAR2 (10),
JOB
VARCHAR2 (9),
MGR
NUMBER (4),
HIREDATE
DATE,
SAL
NUMBER (7,2),
COMM
NUMBER (7,2),
DEPTNO
NUMBER (2)
);
Si
può facilmente notare come la dichiarazione
degli attributi dell’oggetto, in questo caso, segua
biunivocamente la definizione degli attributi della
tabella ‘EMP’.
Osservazioni:
- la
scelta della convenzione dei nomi degli oggetti
SQL3, é libera: qui si é preferito
usare ‘O_’<Nome Uppercase>.
- queste
righe di codice SQL non creano nessuna tabella nel
DB, ma solo la definizione di un tipo od oggetto
all’interno del medesimo.
- la
definizione é fittizia e momentanea, nel
senso che non é la soluzione migliore e verrà
migliorata nel corso dell’articolo.
- un
oggetto SQL3 non deve necessariamente essere la
mappatura della definizione di una Tabella.
Fatto
questo, completiamo la parte SQL con un paio di funzioni
PL/SQL che ci consentano di manipolare quest’oggetto
e di metterlo in relazione con la tabella.
Innanzi
tutto un Package ‘P_GLOBAL’ di comodità in
cui viene definito unicamente il tipo Cursore per
esportare più oggetti:
CREATE
OR REPLACE PACKAGE P_Global IS
TYPE
RefCursor IS REF CURSOR;
END
P_Global;
In
seguito si definisce il Package ‘P_TEST’ con due funzioni:
CREATE
OR REPLACE PACKAGE P_Test IS
FUNCTION
GET_EMPLOYEE RETURN P_Global.RefCursor;
FUNCTION
GET_EMPLOYEE (KEY NUMBER) RETURN O_EMPLOYEE;
END
P_Test;
CREATE
OR REPLACE PACKAGE BODY P_Test IS
FUNCTION
GET_EMPLOYEE RETURN P_Global.RefCursor IS
crs
P_Global.RefCursor;
BEGIN
OPEN
crs FOR
SELECT
O_EMPLOYEE(EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO)
FROM
EMP;
RETURN
crs;
END
GET_EMPLOYEE;
FUNCTION
GET_EMPLOYEE (KEY NUMBER) RETURN O_EMPLOYEE IS
o_emp
O_EMPLOYEE;
BEGIN
SELECT
O_EMPLOYEE(EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO)
INTO
o_emp
FROM
EMP
WHERE
empno = KEY;
RETURN
o_emp;
END
GET_EMPLOYEE;
END
P_Test;
La
prima funzione legge tutti gli Employees dalla tabella
e li riporta in una variabile di tipo RefCursor che
é l’equivalente in Java di un ResultSet. La
seconda riporta l’Employee il cui attributo ‘EmpNo’
risulti uguale ad una data chiave di ricerca, indicata
nella variabile numerica ‘Key’.
A
questo punto la parte SQL del primo esempio é
completata: non resta che connettersi al database
sotto l’utente ‘SCOTT’ ed eseguire gli script nell’ordine
corretto, vale a dire prima la definizione degli oggetti
SQL3 e poi quella dei Packages.
La
parte Java dell’esempio consiste in un oggetto che
rappresenti un Employee e nel suo Wrapper, rispettivamente
le classi ’Employee.java’ e ‘EmployeeWrap.java’. Sulla
prima non c’é nulla da dire, altro non essendo
che un semplice contenitore di dati.
Analizziamo
invece più attentamente la seconda.
Come
già detto, un Wrapper deve avere un costruttore
senza parametri, affinché il JDBC Driver
possa crearne un’istanza tramite la chiamata a Class.forName(<Nome
Wrapper>). Inoltre abbiamo aggiunto un costruttore
che risulterà molto utile in fase di scrittura
nel DB, che richiede un oggetto di tipo Employee come
parametro.
Il
metodo getSQLTypeName, restituisce il nome dell’oggetto
SQL così come lo abbiamo poc’anzi definito
nel DB: anche questo verrà usato in fase di
scrittura. Da notare che i nomi di oggetti vanno usati
sempre in modo Case Sensitive.
Sempre
derivato dall’interfaccia SQLData, troviamo il metodo
readSQL dove avviene la lettura da SQLInputStream
dell’oggetto:
public
void readSQL(SQLInput stream, String typeName) {
long empno = stream.readLong();
String ename = stream.readString();
String job = stream.readString();
int mgr = stream.readInt();
java.sql.Date hiredate = stream.readDate();
double sal = stream.readDouble();
double comm = stream.readDouble();
long deptno = stream.readLong();
_obj
= new Employee(empno, ename, job, mgr, hiredate, sal,
comm, deptno);
}
É
importante osservare come la sequenza delle operazioni
di lettura degli attributi rispecchi esattamente l’ordine
con cui gli stessi sono stati dichiarati: come già
evidenziato precedentemente, questo é una parte
imprescindibile del ‘contratto’ tra Wrapper ed oggetto.
La
stessa cosa avviene, nello stesso identico ordine
anche nel metodo di scrittura.
Alla
fine delle chiamate readXYZ viene creato un oggetto
di tipo Employee e salvato nell’istanza del Wrapper,
in modo da poter essere poi utilizzato da altre classi.
Per
controllare se un attributo conteneva il valore NULL
(che in SQL é un valore valido anche per tipi
di dati come il long, che in Java sono primitive),
basta chiamare il metodo stream.wasNull() analogamente
al ResultSet.
Giunti
a questo punto, disponiamo di tutte le classi di cui
abbiamo bisogno per leggere un oggetto di tipo Employee
dal Database, o a partire da una determinata chiave
di ricerca, oppure semplicemente come elenco di tutti
quelli che si trovano nel DB: ci serve solo una Main
Class che faccia la chiamata. Ed eccola qua:
DriverManager.registerDriver(new
OracleDriver());
Connection
conn = DriverManager.getConnection(url, user, pass);
CallableStatement
statement = conn.prepareCall( “? = call P_Test.get_Employee(?)”);
statement.registerOutParameter(1,
OracleTypes.STRUCT, “O_EMPLOYEE”);
statement.setLong(2,
7788L);
statement.execute();
Hashtable
map = new HashMap();
map.put("O_EMPLOYEE",
EmployeeWrap.class);
Wrapper
wrap = (Wrapper)statement.getObject(1, map);
Employee
the_emp = (Employee)wrap.getWrappedObj();
statement.close();
conn.close();
La
prima cosa da notare é l’uso di una Type Map,
cioè di una mappa dei tipi (leggi oggetti)
SQL3 che verranno usati nelle operazioni effettuate
con questa connessione. Nel nostro caso esiste solo
il tipo ‘O_EMPLOYEE’ la cui controparte Java é
proprio la classe EmployeeWrap.
La
mappa é usata in maniera esplicita al momento
di leggere dallo Statement Object il Wrapper: questo
fa si che il driver chiami da sé la classe
EmployeeWrap.
La
seconda particolarità é l’uso del metodo
registerParameter con tre argomenti, l’indice all’interno
della Stored Procedure, il tipo di dato (che i questo
caso é la generica Struct) ed infine il nome
esplicito del tipo SQL.
In
maniera del tutto analoga si può definire anche
una Stored Procedure per scrivere un oggetto di tipo
Employee nel Database: a tale proposito si vedano
i sorgenti allegati all’articolo.
Risorse
- JDC
on JDBC: http://developer.java.sun.com/developer/Books/JDBCTutorial/index.html
- SQL3
Objects Tutorial: http://java.sun.com/docs/books/tutorial/jdbc/jdbc2dot0/sql3.html
- Oracle
Site OTN (Oracle Technology Network) JDBC Developer’s
Guide and Reference: http://download-west.oracle.com/otndoc/oracle9i/901_doc/java.901/a90211.pdf
Allegati
Gli
esempi completi si possono scaricare qui
|