MokaByte 57 - 9mbre 2001 
Mapping object-relational
come mappare un database relazionale
con framework ad oggetti
I parte
di
Carlo
de Rossi
In questa serie di articoli si analizza l’uso di oggetti SQL3 all’interno del DBMS Oracle8i/9i, allo scopo di realizzare un’interfaccia interamente ad Oggetti tra Java e lo schema relazionale di Oracle, senza alterare quest’ultimo. Partendo dalla descrizione dei fondamenti di questa tecnica e seguendo esempi via via più complessi, si giungerà alla realizzazione di un Framework basato su questo principio e se ne vedrà una possibile applicazione con CORBA

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


MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems; tutti i diritti riservati 
E' vietata la riproduzione anche parziale 
Per comunicazioni inviare una mail a info@mokabyte.it