Introduzione
Considerato l'utilizzo di un database che permette
di lavorare sia a oggetti che in modo classico
relazionale, non si vogliono evidenziare le sue
caratteristiche e potenzialità specifiche,
bensì mostrare come questo sia facilmente
integrabile con altri strumenti che riguardano
la progettazione del software e i linguaggi di
programmazione.
Uno degli obiettivi principali di questo articolo
consiste nel fornire le principali indicazioni
su come procedere nel momento in cui si volessero
utilizzare in modo ciclico e integrato un database
orientato agli oggetti, uno strumento di progettazione
software e un linguaggio di programmazione.
Definizione
del problema
In modo particolare si vuole realizzare uno schema
logico di un database a oggetti e mostrare come
sia semplice la sua implementazione per mezzo
di una proiezione Java delle diverse classi. Grazie
a questa tecnica è possibile ottenere in
modo veloce prototipi di applicazioni funzionanti
- riferimento a un approccio di tipo RAD - in
grado di aiutare nella scelta definitiva di un
eventuale tecnologia.
Ciò che risulta essere interessante é
l'ottima coesione che esiste fra i vari strumenti
e linguaggi utilizzati, ognuno dei quali con le
proprie definizioni e caratteristiche, ma tutti
perfettamente coerenti con le specifiche di progettazione
e programmazione a oggetti. Di seguito l'elenco
degli strumenti utilizzati:
Progettazione
software : Rational Rose - Rational XDE
Database a oggetti : Caché - Intersystem
Linguaggio di programmazione : Java
Vi
sono tre possibili livelli di interazione con
il progetto a cui ogni singola funzionalità
viene sottoposta ciclicamente. Il primo riguarda
la fase di progettazione dello schema logico delle
classi, in cui il progettista può realizzare
- secondo i requisiti funzionali - le caratteristiche
principali delle singole classi; un secondo livello
nel momento in cui le classi vengono importate
all'interno dell' ambiente di sviluppo del database,
esattamente all'interno di un namespace (contenitore
nel quale è possibile inserire i vari progetti
con le relative classi, precedentemente definito
e configurato). A questo punto servendosi di una
sintassi proprietaria del database si possono
raffinare le strutture delle classi precedentemente
create, inserendo codice a piacimento. Per mezzo
di un linguaggio specifico, è possibile
definire nuovi metodi e attributi, specificando
quali caratteristiche devono avere le varie classi.
Un terzo livello in cui è possibile intervenire
riguarda i codici sorgenti Java, generati direttamente
con la tecnica della proiezione. Questa tecnica
consente di trasformare le classi precedentemente
definite e in seguito modificate, in codice Java.
Così facendo il programmatore non dovrà
fare altro che continuare a lavorare direttamente
sugli oggetti creati applicando le caratteristiche
classiche della programmazione a oggetti.
Di seguito un piccolo schema delle tre fasi sopra
descritte, che ciclicamente vengono considerate
durante tutto il ciclo di vita del progetto.
Figura 1 - Le tre fasi dell'applicazione
Schema
logico delle classi di partenza
L'esempio prevede la definizione di tre classi
a contatto fra loro per mezzo di due relazioni,
una di associazione e una particolare di aggregazione.
Vengono pure definite delle cardinalità
in grado di offrire valore aggiunto alle relazioni
stesse. Si tratta di una classe Person con all'interno
tre attributi di tipo %String, di una classe System,
con anch'essa due attributi di tipo %String, e
di una terza classe di nome Mouse aggregata alla
classe System.
Per
evitare problemi di compatibilità di tipi
di dati durante la fase di esportazione delle
classi, prima di iniziare con la definizione dello
schema logico bisogna importare all'interno di
Rational Rose e utilizzare in seguito il set di
tipi riconosciuti e trattati dal database. Come
si può notare nello schema sotto riportato,
i tipi definiti introducono una sintassi particolare
rispetto a quella standard conosciuta, ossia l'utilizzo
del segno "%" nella definizione. Per
esempio, un tipo Integer dovrà essere definito
come %Integer, un tipo String verrà definito
come %String, e così per tutti gli altri
tipi di dati disponibili.
La scelta di queste due relazioni non è
stata casuale, bensì pensata per mostrare
come vengono gestiti all'interno della base dati
i concetti che stanno a monte delle relazioni.
Come detto, per poter memorizzare all'interno
del database le informazioni che riguardano i
vari oggetti, bisogna fare in modo che le classi
portino al loro interno l'informazione che indichi
la loro persistenza, e questo vale per le classi
Person e System.
Figura 2 - Screen Shoot della definizione
delle classi
Il
discorso è leggermente diverso per la classe
Mouse. Definita come classe aggregata, dovrà
essere gestita diversamente, ciò significa
che dal punto di vista degli oggetti verrà
trattata come classe indipendente, mentre all'interno
del database verrà considerata come classe
serializzata della classe System. Si tratta di
due classi ben distinte, i dati della classe aggregata
(Mouse) saranno però inglobati e gestiti
all'interno della classe aggregante (System).
Così facendo, per questo tipo di classi
non sarà necessario indicare nessuna persistenza.
Come anticipato nei punti precedenti, internamente
il database differenzia la gestione delle classi
associate da quelle aggregate. Di seguito un piccolo
esempio che mostra ciò che si intende quando
una classe aggregata viene serializzata all'interno
di un'altra classe:
Figura 3 - Screen Shoot degli attributi serializzati
Come
si può notare, gli attributi inizialmente
definiti all'interno della classe Mouse (type,
val), sono stati serializzati all'interno della
classe aggregante, in questo caso System. Dal
punto di vista della programmazione saranno due
classi fisiche distinte, ma dal punto di vista
dei dati e del contenuto saranno considerate come
una classe (tabella) unica.
Esportazione
delle classi all'interno di Caché
Una
volta definita una prima bozza dello schema logico
delle classi che rappresentano la struttura del
database a oggetti che si vuole realizzare, si
è pronti per l'esportazione della struttura
fisica delle classi. Questa operazione avviene
in modo molto semplice e veloce, basta selezionare
la voce Caché dal menù Tools di
Rational Rose, che subito appaiono le funzionalità
messe a disposizione da Rose per interagire direttamente
con Caché. Le possibilità previste
sono quelle di import (da Caché a Rose),
di export (da Rose a Caché) e di import
del data type dictonary per poter definire correttamente
gli attributi all'interno delle varie classi.
Nel
nostro caso si tratta della seconda opzione, quella
che riguarda l'esportazione delle classi. Questa
operazione richiede davvero pochi secondi, anche
se la struttura delle classi definita presenta
un numero elevato di elementi. Un punto interessante
sta nel fatto che non si tratta di una semplice
operazione di conversione, ma attraverso questo
procedimento è per esempio possibile verificare
eventuali errori di cattiva definizione dei tipi
di dati. In caso ve ne fossero, non sarà
possibile portare a termine l'operazione di esportazione,
e questo potrebbe tornare utile per avere la certezza
che una volta esportate le classi siano definite
correttamente secondo le specifiche.
Struttura
delle classi esportate
Una
volta esportate le classi, si può iniziare
immediatamente il lavoro di definizione del codice
all'interno dei vari metodi precedentemente definiti,
come pure modificare le definizioni o crearne
di nuovi.
In sostanza, da questo momento in poi ci si trova
in un ambiente di sviluppo che permette di interagire
a tutti gli effetti con le classi ottenute. Vi
sono due possibili metodi per interrogare i vari
oggetti, il primo è quello implicito offerto
gratuitamente e automaticamente dalla proiezione,
ossia l'utilizzo di metodi definiti come "metodi
di sistema", il secondo riguarda quello classico
definendo dei metodi espliciti in grado di interagire
sugli oggetti in funzione delle varie esigenze.
Esempio
della struttura ottenuta dopo l'esportazione delle
classi viste nell'esempio:
Classe
PERSON
Class
User.Person Extends %Persistent [ ClassType =
persistent ]
{
Property city As %String;
Property country As %String;
Property name As %String;
Relationship theSystem As User.System [ Cardinality
= one, Inverse = theUser, Required ];
}
Classe
SYSTEM
Class
User.System Extends %Persistent [ ClassType =
persistent ]
{
Property identName As %String;
Property label As %String;
Property theMouse As User.Mouse [ Required ];
Relationship theUser As User.Person [ Cardinality
= many, Inverse = theSystem, Required ];
}
Classe
MOUSE
Class
User.Mouse Extends %Library.SerialObject [ ClassType
= serial ]
{
Property type As %String;
Property val As %String;
}
Come si può notare dalla definizione delle
varie classi, due portano la proprietà
Persistent mentre una viene identificata come
Serial.
Una volta ottenute le definizione delle classi,
attraverso un semplice menù è possibile
specificare tutta una serie di proprietà
specifiche della classe che la caratterizzeranno
durante la proiezione Java e la gestione all'interno
del database. Per indicare che una classe è
di tipo Serial basta modificare le seguenti proprietà:
Class
Type : serial
Class Super : %Library.SerialObject
Dal momento in cui le classi vengono compilate
senza errori, verranno create fisicamente all'interno
del database e saranno utilizzabili attraverso
gli strumenti disponibili, per esempio l'SQL manager
messo a disposizione dal database, il quale permette
immediatamente di lavorare attraverso il linguaggio
SQL sui vari oggetti.
Per
meglio capire quanto spiegato in precedenza, si
consideri l'esempio seguente che tratta la definizione
di un nuovo metodo per l'interrogazione dei dati
sulla classe Person. Questo metodo lo potremo
utilizzare normalmente una volta eseguita la proiezione
Java delle classi interessate, come se fosse un
normale metodo appartenente a una classe java.
Nuovo metodo per l'accesso ai dati di PERSON
//
lookup a name using embedded SQL
Method myGetCity(id As %String) As %String
{
Set name = "name"
&sql(SELECT city INTO :name FROM Person WHERE
ID = :id)
Quit name
}
L'esempio
recupera il nome della città a cui appartiene
l'OID dell'istanza dell'oggetto desiderato. Più
avanti verrà mostrato l'utilizzo di questo
metodo attraverso il linguaggio Java.
Proiezione Java
delle classi definite
Una
volta definito il diagramma delle classi, esportato
all'interno dell'ambiente di sviluppo e in seguito
esteso secondo le funzionalità desiderate,
è possibile passare alla fase finale, proiettando
le classi generate nel linguaggio java. Questa
operazione di regola viene effettuata partendo
dall'ambiente di sviluppo, inserendo all'interno
delle definizioni delle classi il seguente codice:
Projection
java As %Projection.Java(ROOTDIR = "C:\CacheSys\Dev\Java\esempio\Exe");
Ogni
classe di cui si vuole ottenere le proiezione
dovrà contenere al suo interno una dichiarazione
di questo tipo. Informazione molto importante,
riguarda il fatto che tutte le classi java (sorgenti
compresi) ottenute dalla proiezione, vengono memorizzate
all'interno di un direttorio nel file system.
Questa cartella verrà trattata esattamente
come un package all'interno del proprio progetto.
Per un utilizzo corretto delle classi che compongono
il database a oggetti, ogni volta che si ha la
necessità di utilizzo delle stesse s i
deve importare questo package, solo in questo
modo potrò lavorare creando e gestendo
i vari oggetti con i relativi dati, utilizzando
i metodi definiti precedentemente.
Struttura
delle classi dopo la proiezione
Una
volta proiettate le classi interessate all'interno
del package sopra indicato, ottengo il codice
che rappresenta la definizione di tutto ciò
che ho ottenuto (classi, metodi, attributi) all'interno
dell'ambiente di sviluppo del database. Di seguito
un estratto che riguarda la classe Person:
package user;
import com.intersys.*.*;
import user.System;
import user.Mouse;
public
class Person extends Persistent {
private static String CACHE_CLASS_NAME = "User.Person";
// NB: DO NOT USE IN APPLICATION!
// Use <code>Person._open</code> instead.
// <p>
// Used to construct a Java object, corresponding
// to existing object
// in Cache database.
// @see #_open(com.intersys.objects.Database,
// com.intersys.objects.Oid)
// @see #_open(com.intersys.objects.Database,
// com.intersys.objects.Id)
public Person (CacheObject ref) throws CacheException
{
super (ref);
}
}
A seguire alcuni esempi di metodi di sistema ottenuti
e direttamente utilizzabili dalla proiezione Java.
Metodi
di sistema per la gestione degli oggetti ottenuti
Per
capire cosa si intende per metodi di sistema,
di seguito vengono riportati alcuni esempi:
//
Apertura dell'oggetto attraverso l'OID
public
static RegisteredObject _open (Database db, Id
id) throws CacheException {
CacheObject cobj = (((SysDatabase)db).openCacheObject(CACHE_CLASS_NAME,
id.toString()));
return (RegisteredObject)(cobj.newJavaInstance());
}
// Interrogazione di un oggetto
public
User.Person getCity() throws CacheException {
Dataholder dh = mInternal.getProperty(ii_City,
jj_city,
Database.RET_OBJECT,
"City");
CacheObject cobj = dh.getCacheObject();
if (cobj == null)
return null;
return (User.Person)(cobj.newJavaInstance());
}
//
Modifica del valore di un attributo di un oggetto
public
void setCity(User.Person value) throws CacheException
{
Dataholder dh = new Dataholder (value);
mInternal.setProperty(ii_City, jj_City,kk_city,
Database.RET_OBJECT, "city", dh);
return;
}
Ridefinizione
di un metodo per l'interrogazione di un oggetto
Un'informazione
molto importante sta nel fatto che i metodi di
sistema per l' accesso ai dati dell'oggetto possono
essere utilizzati nel momento in cui gli attributi
dell'oggetto vengono definiti come Public.
Di seguito viene riportato un esempio di un metodo
utilizzato per il recupero dell'attributo City
della classe Person nel caso in cui venisse dichiarato
come Private.
Interrogazione
di un oggetto con un metodo ridefinito
public
java.lang.String myGetCity (java.lang.String id)
throws
CacheException {
Dataholder[] args = new Dataholder[1];
args[0] = new Dataholder(id);
Dataholder
res=mInternal.runInstanceMethod("myGetCity",args,Database.RET_PRIM);
return res.getString();
}
Esempi diversi
di interrogazione degli oggetti
Il
codice seguente mostra un esempio di connessione
al database via JDBC, e la creazione di nuovi
oggetti, istanziando le classi ottenute dalla
proiezione.
//
Connessione al database e creazione di una nuova
persona
try
{
// Connect to Cache in the USER namespace
dbconnection = CacheDatabase.getDatabase (url,
username, password);
InputStreamReader
isr = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(isr);
// *****************************
// CREATE NEW INSTANCE OF PERSON
// *****************************
person = new User.Person(dbconnection);
person.setName("new name");
person.setCity("new city");
// Save the new object to the database
person._save();
// For each object
dbconnection.closeObject(person.getOref());
person = null;
dbconnection.close();
}
catch (Exception ex) {
System.out.println("Obj not created: "
+ ex.getClass().getName()
+ ": " + ex.getMessage());
}
// Recupero e aggiornamento di un oggetto dal
database attraverso l'OID
try
{
System.out.print("\n\n\nEnter ID of Person
object to be
opened and updated:");
String strID = br.readLine();
person = (User.Person)User.Person._open(dbconnection,
new Id(strID));
// Get name and city values with native methods
System.out.println("Person name: " +
person.getName());
System.out.println("Person city: " +
person.getCity());
// Get city value with re-defined methods
System.out.println("Person city: " +
person.myGetCity(strID));
// **********************************
// GET NEW DATA AND UPDATE OBJ VALUES
// **********************************
System.out.print("\n\n\nEnter new PERSON
name:");
String newPersonName = br.readLine();
System.out.print("\n\n\nEnter new PERSON
city:");
String newPersonCity = br.readLine();
person.setName((newPersonName));
person.setCity((newPersonCity));
// Save the new object to the database
person._save();
//
For each object
dbconnection.closeObject(person.getOref());
person = null;
dbconnection.close();
} catch (Exception ex) {
System.out.println("Obj not updated: "
+ ": " + ex.getMessage());
}
Conclusioni
Naturalmente questo tipo di procedure richiede
particolare attenzione, soprattutto nel momento
della definizione delle classi. Un grosso vantaggio
che si ha lavorando con questi strumenti è
la velocità di sviluppo, in poco tempo
si ottiene subito del codice Java utilizzabile
per dei test di fattibilità o per la realizzazione
stessa di un'applicazione. Partendo da una definizione
logica statica di un diagramma delle classi si
arriva ad ottenere del codice java compilato con
pochissimi sforzi, e questo potrebbe tornare comodo
quando le applicazioni sono di medio piccole dimensioni.
Provando si verifica che l'intero processo non
provoca nessun tipo di problema, la comunicazione
fra Rational Rose e Caché avviene in modo
semplice e trasparente, niente di ciò che
non si è volutamente progettato viene incluso
all'interno delle definizioni delle classi. Al
contrario, servendosi della proiezione per arrivare
al linguaggio Java, vi sono alcuni aspetti da
considerare, in primo luogo la generazione del
package con al suo interno le classi proiettate.
Bibliografia
[1]
Object-Oriented Application Development Using
the Caché Postrelational Database
[2]
Corso di Gestione Avanzata dei Dati - Roberto
Mastropietro Corso di Basi di Dati Relazionali
e a Oggetti - Giancarlo Corti (Scuola Universitaria
Professionale della Svizzera Italiana)
[3]
http://www.intersystems.com/
|