Tesi di Laurea Accesso a basi di dati con una piattaforma ORM

Transcript

Tesi di Laurea Accesso a basi di dati con una piattaforma ORM
UNIVERSITÀ DEGLI STUDI DI PARMA
Facoltà di Scienze Matematiche, Fisiche e
Naturali
Corso di laurea triennale in Informatica
Tesi di Laurea
Accesso a basi di dati con una piattaforma
ORM
Candidato:
Simone Bianchi
Relatore:
Chiar.mo Prof. Giulio Destri
Co-relatore:
Ing. Alberto Picca
Anno accademico 2008—2009
Ringraziamenti
Desidero innanzitutto ringraziare il Prof. Giulio Destri e l’Ing. Alberto Picca per la disponibilità in modo più assoluto avuta e per avermi permesso lo
svolgimento del mio stage formativo presso la ditta Area Solutions Provider
ubicata a Casalmaggiore—Parma. Ho appresso le basi per lo sviluppo di software a livello professionale e impostando la stesura della mia tesi. Ringrazio
Carlo e Donatella in modo speciale per essermi stato molto vicini e per avermi
supportato materialmente e moralmente. Un altro grosso ringraziamento va
ad Adamo, Stina e Paolo per essermi stati vicini. Ringrazio i miei compagni di
Università Maria Chiara, Davide, Fede, Marina, Cecilia, Paolo, Fabio, Sparty,
Alessandro U., Alessandro T., Lucia, con cui ho passato questi anni di studio
insieme. Desidero ringraziare anche altri miei amici, in modo speciale Stefano
e Marcello, per tutta la disponibilità e l’amicizia avuta nei miei confronti.
Desidero concludere con un sentimento che offro a tutti quelli che come me,
cercano di raggiungere obiettivi che sono importantissimi nella vita. Lo studio
è uno strumento e la cultura ne fa padrona. I CARE, concludo con questa
semplice frase che significa Me ne importa. Ho voluto scrivere questa frase
perchè credo che per fare una cosa bisogna che te ne importi, se no diventa
una cosa fatta male e non ti servirà a niente nella crescita personale di ognuno
di noi.
i
Indice
Ringraziamenti
i
Indice
iii
Introduzione
Il contesto del problema . . . . . . . . . . . . . . . . . . . . . . . . .
1 Il problema della persistenza
dei dati
1.1 Programmazione orientata agli oggetti . . . . . .
1.2 Persistenza dei dati . . . . . . . . . . . . . . . . .
1.3 Survey sulle soluzioni esistenti per la persistenza
1.4 L’importanza dei RDBMS . . . . . . . . . . . . .
1.5 Modello a oggetti vs modello relazionale . . . . .
1.6 Il disaccoppiamento di impedenza . . . . . . . . .
1.7 Le soluzioni possibili . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2 Architetture software a oggetti
2.1 I Design Pattern . . . . . . . . . . . . . . . . . . . . .
2.2 Il pattern MVC nella progettazione software . . . . . .
2.3 Le architetture stratificate ed i loro vantaggi . . . . . .
2.4 Classi ed oggetti entità . . . . . . . . . . . . . . . . . .
2.5 Algoritmi e strutture dati . . . . . . . . . . . . . . . .
2.6 Classi ed oggetti contenitori . . . . . . . . . . . . . . .
2.7 Le soluzioni informatiche dato-centriche . . . . . . . .
2.8 Gli ORM ed il loro ruolo . . . . . . . . . . . . . . . . .
2.9 Implementazione di un ORM : Il pattern Data Mapper
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
v
v
.
.
.
.
.
.
.
1
1
4
6
8
12
13
14
.
.
.
.
.
.
.
.
.
18
18
22
23
26
29
39
44
49
50
3 Soluzioni nel mondo .NET: Nhibernate
51
3.1 Il mondo .NET . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
3.2 Le DataTable e le loro caratteristiche . . . . . . . . . . . . . . . 54
3.3 Rappresentazioni in memoria: DataTable vs ArrayList di oggetti entità . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
3.4 Introduzione a Nhibernate . . . . . . . . . . . . . . . . . . . . . 59
3.5 Scenari di applicazione . . . . . . . . . . . . . . . . . . . . . . . 70
iii
iv
4 Realizzazione del mapper automatico per Nhibernate
4.1 UML e metodologie per la progettazione del software . .
4.2 Obiettivi del progetto . . . . . . . . . . . . . . . . . . .
4.3 Analisi, progettazione ed implementazione . . . . . . . .
4.4 Il progetto compiuto . . . . . . . . . . . . . . . . . . . .
Indice
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
82
82
87
88
92
5 Realizzazione del prototipo operativo
109
5.1 Le entità e la base di dati utilizzata . . . . . . . . . . . . . . . 109
5.2 Versione con Query automatiche . . . . . . . . . . . . . . . . . 111
5.3 Versione con Nhibernate . . . . . . . . . . . . . . . . . . . . . . 123
6 Confronto di prestazioni
129
7 Conclusioni
150
7.1 Bilancio del lavoro svolto . . . . . . . . . . . . . . . . . . . . . 150
7.2 Esperienze e conoscenze acquisite . . . . . . . . . . . . . . . . . 150
7.3 Espansioni future . . . . . . . . . . . . . . . . . . . . . . . . . . 150
Bibliografia
152
Introduzione
La presente tesi di Laurea Triennale è stata sviluppata dal candidato dopo
un periodo di tirocinio svolto presso l’azienda informatica Area Solutions
Providers S.r.l., nella sede operativa Lombardia Est ed Emilia, sotto il coordinamento dell’Ing. Alberto Picca, e con la supervisione del Prof. Giulio
Destri.
Il contesto del problema
Nello sviluppo di sistemi informatici si sono affermate numerose tecnologie,
che vanno utilizzate in modo combinato e, possibilmente sinergico.
Da una parte, i sistemi di gestione di basi di dati relazionali consentono
una gestione efficiente ed efficace di dati persistenti, condivisi e transazionali [1]. Dall’altra, gli strumenti e i metodi orientati agli oggetti (linguaggi di
programmazione, ma anche metodologie di analisi e progettazione) consentono
una sviluppo efficace della logica applicativa delle applicazioni [2].
È utile in questo contesto spiegare che cosa s’intende per sistema informativo e sistema informatico.
• Sistema informativo: L’insieme di persone, risorse tecnologiche, procedure aziendali il cui compito è quello di produrre e conservare le informazioni che servono per operare nell’impresa e gestirla. (M. De Marco
in [3])
• Sistema informatico: L’insieme degli strumenti informatici utilizzati
per il trattamento automatico delle informazioni, al fine di agevolare le
funzioni del sistema informativo. Ovvero, il sistema informatico raccoglie, elabora, archivia, scambia informazione mediante l’uso delle tecnologie proprie dell’Informazione e della Comunicazione (ICT ): calcolatori, periferiche, mezzi di comunicazione, programmi. Il sistema
informatico è quindi un componente del sistema informativo.
La costruzione dell’informazione ed il suo uso entro l’azienda può avvenire
seguendo questi passi, in base alla classificazione di G. Bellinger, N. Shedroff
ed altri, definita in [4] e [5]:
• Dati: I dati sono materiale informativo grezzo, non (ancora) elaborato
da chi lo riceve, e possono essere scoperti, ricercati, raccolti e prodotti.
v
vi
INTRODUZIONE
Sono la materia prima che abbiamo a disposizione o produciamo per
costruire i nostri processi comunicativi. L’insieme dei dati è il tesoro di
un ’zazienda e ne rappresenta la storia evolutiva [6].
• Informazione: L’informazione viene costruita dai dati elaborati cognitivamente, cioè trasformati in un qualche schema concettuale successivamente manipolabile e usabile per altri usi cognitivi. L’informazione
conferisce un significato ai dati, grazie al fatto che li pone in una relazione
reciproca e li organizza secondo dei modelli. Trasformare dati in informazioni significa organizzarli in una forma comprensibile, presentarli in
modo appropriato e comunicare il contesto attorno ad essi.
• Conoscenza: La conoscenza è informazione applicata, come un senso
comune, o non comune, che sa quando e come usarla. È attraverso l’esperienza che gli esseri umani acquisiscono conoscenza. È grazie alle esperienze fatte, siano esse positive o negative, che gli esseri umani arrivano
a comprendere le cose. La conoscenza viene comunicata sviluppando
interazioni stimolanti, con gli altri o con le cose, che rivelano i percorsi nascosti e i significati dell’informazione in modo che possano essere
appresi dagli altri. La conoscenza è fondamentalmente un livello di comunicazione partecipatorio. Dovrebbe rappresentare sempre l’obiettivo
a cui tendere, poichè consente di veicolare i messaggi più significativi.
Le informazioni ottenute dall’elaborazione dei dati devono essere salvate
da qualche parte, in modo tale da durare nel tempo dopo l’elaborazione. Per
realizzare questo scopo viene in aiuto l’informatica.
Per informatica si intende il trattamento automatico dell’informazione
mediante calcolatore (naturale o artificiale). Philippe Dreyfus
All’inizio di questo capitolo è stato accennato che nello sviluppo dei sistemi
informatici si sono affermate diverse tecnologie e che, in particolare, l’uso di
sistemi di gestione di basi di dati relazionali comporta una gestione efficace
ed efficiente di dati persistenti.
Per persistenza di dati in informatica si intende la caratteristica dei dati
di sopravvivere all’esecuzione del programma che li ha creati. Se non fosse
cosi, i dati verrebbero salvati solo in memoria RAM e sarebbero persi allo
spegnimento del computer.
Nella programmazione informatica, per persistenza si intende la possibilità
di far sopravvivere strutture dati all’esecuzione di un programma singolo. Occorre il salvataggio in un dispositivo di memorizzazione non volatile, come per
esempio su un file system o su un database. Nel capitolo 1 si vedranno le problematiche e le possibili soluzioni che si hanno per persistere i dati, mostrando
l’importanza di database relazionali. Dal capitolo 2 in avanti si vedranno
IL CONTESTO DEL PROBLEMA
vii
delle caratteristiche di una tecnica di programmazione per convertire dati fra
RDBMS e linguaggi di programmazione orientati agli oggetti. Quindi per persistere i dati entro un database relazionale. Questa tecnica di programmazione
prende il nome di ORM, ovvero di Object-Relation mapping.
Capitolo 1
Il problema della persistenza
dei dati
In questo capitolo viene introdotto il problema della persistenza dei dati. Si
vedrà anche come lo sviluppo di applicazioni orientate agli oggetti richiede
quasi sempre di memorizzare lo stato degli oggetti in una base di dati. Sarà
dimostrata l’importanza dei RDBMS per la gestione della persistenza dei dati
e si vedrà qualche soluzione possibile.
1.1
Programmazione orientata agli oggetti
Il computer è una macchina capace di eseguire algoritmi generici descritti
attraverso un linguaggio noto come codice macchina. Purtroppo il linguaggio macchina è poco espressivo in quanto descrive, attraverso codici binari,
semplicemente istruzioni elementari aritmetiche e logiche o di input/output
direttamente comprensibili dalla CPU.
È nata l’esigenza di esprimere gli algoritmi attraverso concetti e simboli più
vicini alla mente umana. I linguaggi di programmazione servono a questo. Un
linguaggio di programmazione è un testo più facilmente leggibile da un essere
umano che sarà convertito cda un programma apposito (compilatore) in un
codice macchina dal medesimo significato. La programmazione procedurale
è stato il primo paradigma di programmazione: infatti descrive gli algoritmi
come una sequenza di operazioni da fare; a seconda del linguaggio le operazioni
erano le stesse del hardware (Assembly) oppure a più alto livello (Linguaggio
C).
Es. Apri il frigorifero, prendi l’uovo, rompi l’uovo nella padella, accendi il
fuoco, cuoci l’uovo e servi nel piatto.
La programmazione procedurale presenta dei limiti: i programmi nel tempo sono diventati sempre più complessi e programmando con questo stile non
si riusciva più a riutilizzare il codice e c’è stata la difficoltà di adattarlo ad
esigenze che possono mutare in corso d’opera.
Per ridurre questi problemi, è nato il paradigma di programmazione orientata agli oggetti: invece di semplicemente descrivere i passi per risolvere i
problemi, si cerca di spezzare la realtà in oggetti e programmare le operazioni
possibili di ogni oggetto indipendentemente. Gli oggetti sono poi messi in
1
2
CAPITOLO 1. IL PROBLEMA DELLA PERSISTENZA
DEI DATI
Figura 1.1: Un uovo nella realtà un’istanza della classe uova
relazione per la risoluzione del problema, ma gli algoritmi relativi ai singoli
oggetti ed implementati entro i metodi interni di ogni oggetto sono facilmente
riutilizzabili.
Tornando all’esempio dell’uovo, usando il paradigma OOP si potrebbe programmare le varie operazioni relative all’uovo in modo separato del resto del
programma. In questo modo l’attenzioe si focalizza sull’uovo come entità del
mondo reale e tutto il resto divemta solo l’insieme delle condizioni al contorno
(vedi fig. 1.1).
Negli ultimi anni sempre piùOB lo sviluppo del software si basa sul paradigma di programmazione orientata agli oggetti OOP. Esso introduce un modo
diverso e, se si vuole, più efficiente per strutturare cil codice e la logica applicativa in esso contenuta. I primi linguaggi di programmazione che supportano
questo paradigma sono stati progettati negli anni ’70 e ’80, come per esempio Simula1, Simula67 e SmallTalk. Ma è nella seconda metà degli anni’80
che si diffondono anche nuove metodologie di programmazione più adatte per
gestiree problemi reali più complessi. A fine anni’80 vede la luce un altro
linguaggio usatissimo, il C++, creato da Bjarne Stroustrup, in cui vengono
introdotti concetti Object-Oriented nel linguaggio di programmazione C.
Il grosso vantaggio dell’approccio Object-Oriented rispetto agli altri paradigmi di programmazione consiste nel fatto che, per strutturare le applicazioni,
lo sviluppatore si trova ad utilizzare una logica che è molto vicina a quella che
è la percezione comune del mondo reale.
Pensare ad oggetti significa infatti saper riconoscere gli aspetti che caratterizzano una particolare realtà e saper fornire di conseguenza una rappresentazione astratta in un’ottica OOP.
Semplicemente, definiamo i concetti di oggetto e classe. Una classe è un’astrazione di uno degli aspetti della realtà che ci interessa. Questa astrazione
avviene tramite la definizione di attributi, che rappresentano i possibili stati
dell’aspetto in questione, e funzioni, che rappresentano le azioni possibili da
parte di quell’aspetto e che, quindi, si possono fare con le istanze della classe
rappresentante tale aspetto. Un oggetto è una istanza di una classe, ovvero è
una n-upla di valori memorizzati negli attributi, che occupa uno spazio entro
1.1. PROGRAMMAZIONE ORIENTATA AGLI OGGETTI
3
la memoria di lavoro del programma, tipicamente la memoria RAM. La sua
classe definisce come sono organizzati i dati di questa memoria, possiede tutti gli attributi definiti nella classe, ed essi hanno un valore, che può mutare
durante l’esecuzione del programma, cosı̀ come avviene per le variabili di un
programma procedurale.
Gli oggetti e le classi di un sistema orientato agli oggetti si basano sui
principi di [7]:
• Information Hiding: La descrizione interna dei dati di un oggetto e
del suo funzionamento, non deve essere visibile all’esterno, ovvero all’utente, ma è resa accessibile soltanto definendone opportune interfacce
ben definite. Nei moderni linguaggi orientati agli oggetti vi sono diversi
livelli di protezione delle informazioni (private, protette, pubbliche). Inoltre è bene notare che esiste una differenza concettuale tra Information
Hiding e Incapsulamento. L’information hiding è il principio teorico su
cui si basa la tecnica dell’incapsulamento. Con la tecnica dell’incapsulamento si può vedere l’oggetto in esame come una black-box, cioè
una scatola nera di cui, attraverso l’interfaccia si sa cosa fa e come interagisce con l’esterno ma non come lo fa, ossia l’implementazione dei
comportamenti a livello di codice risulta totalmente nascosta all’esterno
dell’oggetto. L’incapsulamento contribuisce ai vantaggi della programmazione ad oggetti: (indipendenza), (robustezza) e (riusabilità degli
oggetti creati).
• Identità dell’oggetto: Ogni oggetto dispone di un’identità univoca
valida in tutto il sistema. L’uguaglianza di due oggetti implica che tutti
gli attributi hanno il medesimo valore, ma gli oggetti non hanno necessariamente la stessa identità. Due oggetti identici sono lo stesso oggetto. L’identità è spesso espressa attraverso il concetto di identificatore
univoco di un oggetto o Object IDentifier (OID).
• Ereditarietà: Gli oggetti con comportamenti simili e/o con strutture
dati simili possono ereditare le loro proprietà e funzioni. Pensiamo per
esempio ad una classe Cane che eredita da una classe Animale. Il Cane
è un animale. Questo tipo di relazione si chiama IS A è la più usata
nella programmazione orientata agli oggetti. Ne esistono delle altre, ma
un altra comunemente usata è la relazione per contenimento chiamata
HAS A. Una classe che contiene un’istanza di un altra classe.
• Polimorfismo: I metodi con gli stessi nomi, possono avere una semantica diversa, secondo il contesto in cui vengono richiamati (concetto di
overriding).
CAPITOLO 1. IL PROBLEMA DELLA PERSISTENZA
DEI DATI
4
1.2
Persistenza dei dati
Nell’introduzione abbiamo parlato di cosa significa persistere i dati. Adesso
prima di addentrarci sulle soluzioni esistenti per la persistenza, vediamo come
si rappresenta l’informazione in informatica.
Per far si che l’informazione esista nel mondo fisico, è necessario rappresentarla in modo fisico [6].
Può essere rappresentata come variazioni di grandezze fisiche entro opportuni supporti fisici. Per poterla immagazzinare e trasmetterla, sono necessari supporti fisici. L’immagazzinamento dell’informazione viene realizzato
attraverso archivi cartacei oppure attraverso archivi informatici. Anche la
trasmissione dell’informazione può aver luogo attraverso canali “tradizionali”
come la posta cartacea o il fax o attraverso canali digitali come Internet ed i
vari meccanismi di comunicazione in essa contenuti.
In informatica, l’informazione viene rappresentata e misurata come insiemi di byte. L’unità elementare di informazione è la quantità di informazione
ottenuta da un supporto che può contenere al più due configurazioni diverse.
Essa viene chiamata bit. Esistono codifiche associate agli standard che assegnano significati particolari a tali valori, per esempio il codice ASCII associa
ai valori da 0 a 255 rappresentati dai byte le lettere (maiuscole e minuscole)
dell’alfabeto latino internazionale, le lettere accentate, le cifre da 0 a 9, i segni
di interpunzione, parantesi, simboli matematici, caratteri di controllo tra quali
cito l’a capo e il fine riga.
Poiché sempre più spesso le applicazioni sono realizzate ad oggetti è necessario rendere persistenti alcune oggetti di alcune classi. Esistono diversi modi
per persistere gli oggetti:
• Base di dati a Oggetti
• Base di dati relazionale
• Insieme di file
Confrontando le metodologie, la migliore oggi è quella basata sulle basi di
dati relazionali Vedremo nella prossima sezione l’importanza di esso e che si
dimostrerà un eccelente immagazzinatore dell’informazione e un modo utile
per persistere gli oggetti nel mondo della programmazione OOP.
L’approccio convenzionale alla gestione dei dati e quindi alla loro persistenza sfrutta la presenza di archivi o file per memorizzare i dati in modo persistente sulla memoria di massa. Un file consente di memorizzare e ricercare
dati, ma fornisce solo semplici meccanismi di accesso e di condivisione.
Le procedure scritte in un linguaggio di programmazione sono completamente autonome; ciascuna di essa definisce e utilizza uno o più file privati.
Dati di interesse per più programmi sono replicati tante volte quanti sono i
1.2. PERSISTENZA DEI DATI
5
programmi che li utilizzano. Si noti che operando in questo modo inciampiamo
su ridondanza e possibilità di incoerenza.
Per superare questi problemi, sono state concepite le basi di dati, che non
sono altro una collezione organizzata di dati, di interesse per una qualche
applicazione.
Un sistema di gestione di basi di dati (DBMS) è un sistema software in
grado di gestire collezioni di dati che siano grandi, condivise, e persistenti,
assicurando la loro affidabilità e privatezza. Inoltre devono essere efficace e
efficienti.
Spieghiamo brevemente queste caratteristiche che deve avere un DBMS.
• Grandi: Le basi di dati possono avere dimensioni molto più grandi
della memoria centrale disponibile. Quindi i DBMS devono prevedere
una gestione dei dati in memoria secondaria.
• Condivise: Applicazioni e utenti diversi devono poter accedere ai dati
comuni. Cosi facendo si riduce la ridondanza dei dati e si riduce anche
possibilità di inconsistenze. I DBMS dispongono di un controllo di concorrenza che serve nel garantire l’accesso condiviso ai dati da parte di
molti utenti che operano contemporaneamente.
• Persistenti: Le basi di dati sono persistenti, ovverro hanno un tempo
di vita che non è limitato a quello delle singole esecuzioni dei programmi che le utilizzano. Ricordo che, i dati gestiti da un programma in
memoria centrale hanno una vita che inizia e termina con l’esecuzione
del programma. Tali dati non sono persistenti.
• Affidabilità: Capacità del sistema di conservare sostanzialmente intatto il contenuto della base di dati in caso di malfunzionamenti hardware o
software. I DBMS devono offrire politiche di backup e disaster recovery.
• Privatezza: Attraverso meccanismi di autorizzazione, l’utente, riconosciuto in base ad un nome utente, viene abilitato a svolgere determinate
azioni sui dati.
• Efficienza: Capacità di svolgere le operazioni utilizzando un insieme di
risorse (spazio e tempo) che sia accettabile per gli utenti.
• Efficacia: Capacità della base di dati di rendere produttive, in ogni
senso, le attività dei suoi utenti.
Alcune delle caratteristiche dei DBMS sono già garantite, per esempio,
dai file. Possiamo quindi vedere un DBMS come concepito e realizzato per
estendere le funzioni dei file systems.
CAPITOLO 1. IL PROBLEMA DELLA PERSISTENZA
DEI DATI
6
1.3
Survey sulle soluzioni esistenti per la
persistenza
In questa sezione vengono illustrate alcune soluzioni esistenti per gestire la
persistenza dei dati. Esistono diversi meccanismi o, per meglio dire, implementazioni della persistenza. Il primo meccanismo è il concetto di serializzazione
e deserializzazione.
La serializzazione è un meccanismo che permette la trasformazione automatica di oggetti (di qualunque complessità) in una sequenza di byte. In altri
termini, è un meccanismo che permette di salvare un oggetto in un supporto
di memorizzazione lineare, per esempio in un’area di memoria o in un file,
o per trasmetterlo su una connessione di rete, per esempio in un socket. In
che forma può essere la serializzazione? Una forma è quella binaria, abbiamo
detton che essa trasforma automaticamente gli oggetti in sequenza di byte
oppure in un altra forma più leggibile ad un essere umano, per esempio in
formato XML.
Il processo inverso, detto di deserializzazione consiste nel riportare gli
oggetti negli stati in cui si trovavano prima di effettuare la serializzazione.
Prima di vedere un esempio, è meglio chiarire perchè si utilizza questo
approccio per persistere gli oggetti.
Esistono sostanzialmente due motivi principali: quello di rendere persistente lo stato di un oggetto su un supporto di archiviazione in modo da poter
ricreare una copia esatta in una fase successiva e di inviare l’oggetto per valore
da un dominio dell’applicazione ad un altro.
Per chiarire meglio il concetto di serializzazione ecco un esempio in codice
Java:
public class Serializza { public static void main(String[] args)
throws IOException { Film film = new Film("Fantozzi alla riscossa",
"Paolo Villaggio", "1994");
//Apriamo lo stream di output (supporto di persistenza) OutputStream
fos = new FileOutputStream("C:\\Film.ser");
//Apertura dello stream di serializzazione ObjectOutputStream oos =
new ObjectOutputStream(fos);
//Scrittura su file della sequenza di byte rappresentativa dello
//stato dell’oggetto. oos.writeObject(film); oos.close();
fos.close(); } }
Come si può notare nel main(), vien creato un oggetto film ed aperto uno
stream in output in modo tale che l’oggetto, binarizzato, vien scritto nel file
C:
1.3. SURVEY SULLE SOLUZIONI ESISTENTI PER LA PERSISTENZA7
Film.ser. Lo stream di output funge come supporto alla serializzazione dell’oggetto e una volta aperto viene scritto sul file la sequenza di byte rappresentativa dell’oggetto film.
Ed ecco l’esempio di deserilizzazione corrispondente:
public class Deserializza { public static void main(String[] args) {
//Apertura dello stream di input InputStream fis = new
FileInputStream("C:\\Film.ser");
//Apertura stream di deserializzazione ObjectInputStream ois = new
ObjectInputStream(fis);
//Lettura e assegnazione dell’oggetto Film film =
(Film)ois.readObject();
ois.close(); fis.close(); System.out.println(film.toString()); } }
In questo caso viene aperto lo stream di input dell’oggetto film, viene letto
byte per byte dal file Film.ser e viene ricostruita una copia esatta dell’oggetto
Film come era prima di effettuare la serializzazione.
Il meccanismo di serializzazione quindi, permette di salvare lo stato dell’oggetto nel suo complesso. Ciò significa due cose: se in tale oggetto è presente un riferimento ad un altro oggetto viene salvato anche lo stato di tale
oggetto, in questo caso si parla di Copia in profondità e non semplicemente il
riferimento ad esso nel qual caso si sarebbe parlato di Copia superficiale.
Questo è il motivo per cui il network di oggetti deve essere costituita
da istanze derivanti da classi serializzabili. Nel file binario, questa rete viene
riprodotta esattamente con la stessa configurazione esistente in memoria prima
della serializzazione. Gli indirizzi di memoria che sono privi di significato fuori
dal contesto dell’esecuzione del programma, sono sostituiti da numeri seriali.
Da qui prende il nome di serializzazione.
Chiaramente questo modo di procedere crea sia vantaggi sia svantaggi.
Un grosso vantaggio della serializzazione è quello di essere semplice da implementare ed è intrinsicamente Object-Oriented. Uno svantaggio è che non
dispone di supporto per transazioni e non consente il recupero selettivo dei
dati (se non dopo aver effettuato la deserializzazione). Un altro svantaggio
è quello della gestione della sicurezza e delle versioni dei singoli oggetti, che
risulta molto onerosa. Quindi, questo metodo si può impiegare per progetti
che non presentano particolare criticità in termini di sicurezza e non gestiscano
notevoli quantità di dati.
Esistono diversi altri meccanismi per persistere i dati, per esempio:
• EJB (Enterprise Java Beans)
CAPITOLO 1. IL PROBLEMA DELLA PERSISTENZA
DEI DATI
8
• JDBC (Driver per la connessione alla sorgente di base di dati
per Java)
• XMI
• JDO
• HIBERNATE (ORM nel mondo Java)
• NHIBERNATE (ORM nel mondo C#)
Molti dei metodi citati usano al loro interno il metodo oggi più diffuso per
immagazzinare (persistere) i dati, il RDBMS.
1.4
L’importanza dei RDBMS
Prima di dire il perchè ancor’oggi i DBMS più diffusi e più utilizzati sono
quelli relazionali è bene spiegare i diversi modelli che esistono.
Un DBMS è un potente strumento per creare e amministrare grosse moli di
dati in modo efficiente e permettendo di persistere per un lungo periodo di
tempo questi dati. [8].
Le potenzialità che un DBMS offre agli utenti sono:
• Persistenza: Come un file system, un DBMS supporta la memorizzazione di grosse mole di dati che esistono indipendentemente dai molti
processi che li utilizzano.
• Programmazione di interfacce: Un DBMS permette agli utenti o
ad un programma applicativo di accedere o modificare dati offrendo un
potente linguaggio per creare delle Query.
• Gestione delle transazioni: Un DBMS supporta accessi ai dati in
modo concorrente. In altri termini, possiamo dire che accessi simultanei
corrispondono a molti processi distinti(transazioni).
Un modello di dati è un insieme di concetti utilizzati per organizzare i dati
di interesse e descriverne la struttura in modo che essa risulti comprensibile a
un elaboratore [1].
Questo modello si basa sullo standard ANSI/X3/SPARC (vedi Fig. 1.2).
Lo standard definisce in pratica tre livelli [9] e [10]:
• Schema esterno: Rappresenta la visione che l’utente e le applicazioni
utente devono avere del sistema; si opera per mezzo di SQL creando
delle viste.
1.4. L’IMPORTANZA DEI RDBMS
9
Figura 1.2: Architettura ANSI/SPARC delle moderne basi di dati
• Schema concettuale: Vengono definiti gli elementi costitutivi del database,
talora chiamati anche oggetti del database, come le tabelle, le viste,
gli indici; si opera anche qui per mezzo di SQL creando le tabelle e
definendone gli attributi.
• Schema interno: Rappresenta il layout fisico dei record e dei campi; si
opera per mezzo di un linguaggio di programmazione come il C definendo
a basso livello la struttura della tabella.
Questa struttura a livelli consente di avere indipendenza di dati tra un
livello e l’altro. All’interno dello schema concettuale viene definito il modello
dei dati ed è qui che esistono diversi modelli.
• Modello gerarchico
• Modello reticolare
• Modello relazionale
• Modello ad oggetti
• Modelli ibridi relazionali e orientati agli oggetti
Il modello gerarchico è stato storicamente il primo modello ad affermarsi.
Fù definito negli anni sessanta. In questo modello i dati sono organizzati
secondo strutture ad albero, che si suppone che riflettano in una gerarchia
esistente le entità che appartengono al database e le relazioni che le connettono.
Un esempio di questo modello sono i file system che usiamo oggi. Essi sono
organizzati secondo una struttura ad albero, in uso ormai da decenni.
10
CAPITOLO 1. IL PROBLEMA DELLA PERSISTENZA
DEI DATI
Il modello reticolare fù definito negli anni settanta. Esso è basato sull’uso
dei grafi.
I modelli ibridi sono invece quelli che all’interno del singolo record posso
avere degli oggetti, ovvero elementi complessi come altri record o intere tabelle.
Il modello relazionale si basa su due concetti fondamentali, relazione e
tabella. Il termine relazione proviene dalla matematica, in particolare dalla teoria degli insiemi. Infatti una relazione è un sottoinsieme del prodotto
cartesiano. Invece il termine tabella è un concetto semplice e molto intuitivo.
Esso risponde al requisito dell’indipendenza dei dati, che come abbiamo
visto nell’introduzione, è uno dei requisiti per poter parlare di DBMS.
Il modello a oggetti è stato introdotto negli anni ottanta come evoluzione
del modello relazionale. Qui troviamo concetti Object-Oriented.
I Database relazionali furono proposti da Edgar F. Codd nel 1970 per semplificare la scrittura di interrogazioni SQL e per favorire l’indipendenza dei
dati. Diciamo che un database è relazionale se asserisce alle 12 regole di
Codd.
Richiamo qui di seguito le 12 regole:
• Regola 0 : Il sistema deve potersi definire come relazionale, base di dati,
e sistema di gestione. Affinché un sistema possa definirsi sistema relazionale per la gestione di basi di dati (RDBMS), tale sistema deve
usare le proprie funzionalità relazionali (e solo quelle) per la gestire la
base di dati.
• Regola 1 : L’informazione deve essere rappresentata sotto forma di tabelle;
L informazioni nel database devono essere rappresentate in maniera univoca, e precisamente attraverso valori in colonne che costituiscano, nel
loro insieme righe di tabelle.
• Regola 2 : La regola dell’accesso garantito: tutti i dati devono essere
accessibili senza ambiguità (questa regola è in sostanza una riformulazione de requisito per le chiavi primarie). Ogni singolo valore scalare
nel database deve essere logicamente indirizzabile specificando il nome
della tabella che lo contiene, il nome della colonna in cui si trova e il
valore della chiave primaria della riga in cui si trova.
• Regola 3 : Trattamento sistematico del valore NULL; il DBMS deve
consentire all’utente di lasciare un campo vuoto, o con valore NULL. In
particolare, deve gestire la rappresentazione di informazioni mancanti e
quello di informazioni inadatte in maniera predeterminata, distinta da
ogni valore consentito (per esempio, diverso da zero o qualunque altro
numero per valori numerici), e indipendente dal tipo di dato. È chiaro inoltre che queste rappresentazioni devono essere gestite dal DBMS sempre
nella stessa maniera.
1.4. L’IMPORTANZA DEI RDBMS
11
• Regola 4 : La descrizione del database deve avvenire ad alto livello logico
tramite i metadati.
• Regola 5 : Deve esistere un linguaggio che permetta la gestione dei dati
(come SQL).
• Regola 6 : Si possono creare delle viste per vedere una parte dei dati.
Queste viste devono essere aggiornabili.
• Regola 7 : Le operazioni che avvengono sul database devono avvenire
anche sulle tabelle.
• Regola 8 : I dati memorizzati nel database devono essere indipendenti
dalle strutture di memorizzazione fisiche.
• Regola 9 : I dati devono essere indipendenti dalla struttura logica del
database per garantire la crescita naturale e la manutenzione del database.
• Regola 10 : Le restrizioni sui dati devono essere memorizzate nel database.
• Regola 11 : L’accesso ai dati è indipendente dal tipo di supporto per la
lettura o memorizzazione degli stessi.
• Regola 12 : L’accesso ai dati non deve annullare le restrizioni o i vincoli
di integrità del linguaggio principale.
Vediamo adesso gli elementi principali che costituiscono un modello relazionale.
Come abbiamo detto in precedenza il tutto si basa sul concetto di relazione.
Dati n > 0 insiemi A1 , ..., An , non necessariamente distinti, il prodotto cartesiano di A1 , ..., An , indicato con A1 × A2 × ... × An , è costituito
dall’insieme delle n-uple (v1 , ..., vn ) tali che vi ∈ Ai , per1 ≤ i ≤ n.
Una relazione matematica sui domini A1 , ..., An è un sottoinsieme del
prodotto cartesiano A1 × A2 × ... × An . Se vogliamo essere meno matematici,
una relazione è un insieme di record omogenei, cioè definiti sugli stessi campi.
Ad ogni campo è associato un nome, quindi associamo a ciascuna occorrenza
di dominio (A1 , ..., An ) nella relazione un nome, detto attributo, che descrive
il ruolo giocato dal dominio stesso. Per essere più formali, esiste una corrispondenza tra attributi e domini per mezzo di una funzione dom : X −→ A,
che associa a ciascun attributo att ∈ X un dominio dom(att) ∈ A che ne
definisce valori discreti o intervalli di valori continui possibili. Prende il nome
di tupla un insieme non ordinato di valori degli attributi.
Le relazioni nei database relazionali vengono rappresentate graficamente
tramite le tabelle. Ogni colonna di tale tabella costituisce un attributo. Le
tuple sono rappresentate dalle righe delle tabelle. Nella fig. 1.3 mostra un
esempio di tabella nel mondo dei database relazionali.
12
CAPITOLO 1. IL PROBLEMA DELLA PERSISTENZA
DEI DATI
Figura 1.3: Una tabella
In altri termini, un database relazionale è un insieme di tabelle, collegate
tramite determinati attributi, che hanno la caratteristica di avere diversi valori
associati per ogni tupla della tabella e detti chiave primaria. Quando questi
vengono utilizzati per relazionare (ossia collegare, ponendole in relazione fra
loro) due tabelle vengono dette chiavi esterne.
I sistemi informatici richiedono solitamente di gestire alcuni dati in modo
persistente e abbiamo visto fino adesso dei modi per persistere (serializzazione,
EJB, XML, ecc.). In un’applicazione a oggetti, è necessario rendere persistenti
alcuni oggetti di alcune classi (vedremo in seguito che queste classi sono spesso
indicate col nome di classi entità). Queste classi prendono anche il nome di
classi persistenti. Sono classi che fanno parte della logica applicativa ispirate
alle classi concettuali.
Però, esistono alcuni problemi legati al voler gestire oggetti persistenti
mediante una base di dati relazionale. Questo problema è noto in letteratura
come Impedance Mismatch o disaccopiamento di impedenza.
Vedremo in seguito di che cosa si tratta e di come poterlo risolvere. Per
arrivare a spiegare l’importanza dei RDBMS è utile soffermarci un’attimo a
confrontare il modello a oggetti vs il modello relazionale. Da qui si evincerà
l’importanza dei database relazionali.
1.5
Modello a oggetti vs modello relazionale
I modelli a oggetti costituiscono una promettente evoluzione delle basi di dati.
I sistemi a oggetti integrano la tecnologia della base di dati con il paradigma
ad oggetti sviluppato nell’ambito dei linguaggi di programmazione.
1.6. IL DISACCOPPIAMENTO DI IMPEDENZA
13
Nelle basi di dati a oggetti, ogni entità del mondo reale è rappresentata da
un oggetto. Per esempio: dati multimediali, cartine geografiche, ecc [11]. Da
notare che è difficile pensare ad una struttura relazionale per queste entità. I
programmatori che utilizzano il paradigma OOP e hanno necessità di salvare
i propri oggetti, possono scegliere di salvarli su un database relazionale; in
questo caso però bisogna convertire gli oggetti in tabelle con righe e colonne,
nel fare questo bisogna mantenere anche la descrizione e le relazioni delle varie
classi nonchè lo schema relazionale e il mapping delle classi nel db relazionale.
Se al contrario scegliamo una base di dati a oggetti, i programmatori possono salvare gli oggetti cosi come sono e dovranno solamente preoccuparsi di
modellare una base di dati a oggetti che descriva correttamente la realtà fisica
che deve immagazzinare nel database. La differenza tra questi due modelli
sta nella mancanza di interoperabilità. Ciò significa che si può accedere ad
un ODBMS solo per mezzo del DBMS che lo gestisce. Per esempio, ad un database
Goods si può accedere solo per mezzo di un programma scritto per Goods e non
qualcos’altro.
1.6
Il disaccoppiamento di impedenza
Le informazioni memorizzate in un database hanno una natura persistente: ogni
tabella sul disco conserva il suo stato (ossia il valore degli attributi nelle tuple) tra
sessioni di lavoro successive. Pertanto un programmatore che sfrutta un linguaggio di
programmazione ad oggetti e che costruisce la classe tabella dovrà fare in modo che
tale classe sia persistente. Non ci deve essere nessuna differenza tra il trattamento
fra oggetti transitori e oggetti persistenti. Cosi facendo, la persistenza è una proprietà che non dipende dal particolare tipo di oggetto, ma è una proprietà attribuile
ad ogni classe di oggetti. Bisogna quindi che la classe che deve essere persistente
deve prevedere opportuni metodi per gestire la persistenza. L’oggetto persistente
a differenza di un oggetto non persistente, deve essere in grado automaticamente
di rileggere da una memoria di massa le informazioni sul suo stato. dei problemi
nel persistere dei dati entro un database. Questo comporta la necessità di decidere
in generale come implementare politiche di persistenza e in particolare quali scelte
progettuali fare nel programma specifico che si sta realizzando.
Un programmatore che utilizza un linguaggio di programmazione orientato agli
oggetti (C++, Java, C#, J#, Vb.net, Eiffell, ecc.) gradisce nella maggior parte dei
casi utilizzare lo stesso approccio anche quando salva i dati sul database. Cioè rende
le classi persistenti. Il problema che è noto in letteratura sotto il nome di impedance
mismatch [12], sta a significare il problema dell’integrazione tra SQL (linguaggio
dichiarativo, in cui non si pensa all’algoritmo che implementa la ricerca, ma solo al
risultato della ricerca) e i normali linguaggi di programmazione OOP che non sono
dichiarativi. Esiste anche il problema legato alla conversione dei dati come tornati
dall’SQL in dati che possono essere interpretati dall’ambiente OOP, come Java o
C++. Quindi questo dissaccoppiamento di impedenza porta a differenze sui tipi di
dato primitivi (anche se molte differenze sono risolte dai driver di comunicazione tra
programma e RDBMS) e, soprattutto, differenze tra i riferimenti interni al programma
ad oggetti e gli insiemi di chiavi esterne che collegano i dati nel RDBMS.
CAPITOLO 1. IL PROBLEMA DELLA PERSISTENZA
DEI DATI
14
Figura 1.4: Un esempio di interazione sql immerso nelle classi
1.7
Le soluzioni possibili
Esistono diverse soluzioni per impedire il verificarsi del problema del disaccopiamento
d’impedenza.
Una prima possibilità è quella di rendere persistente una classe scrivendo direttamente il codice SQL dentro la classe stessa (vedi fig. 1.4).
Questo approccio presenta alcuni ostacoli. Il primo problema deriva dal fatto
che SQL è un linguaggio ricco, con una propria sintassi. Sono state proposte due
soluzioni:
1. SQL Embedded
2. CLI (Call Level Interface)
SQL Embedded prevede di introdurre direttamente nel programma sorgente scritto nel linguaggio ad alto livello le istruzioni SQL, distinguendole dalle normali istruzioni
tramite opportuni separatori e/o sintassi appropriate. Di norma in questo approccio la
compilazione è preceduta dall’esecuzione di un apposito preprocessore che riconoscerà
le istruzioni SQL è sostituirà a esse un insieme opportuno di chiamate ai servizi dei
RDBMS, tramite una libreria specifica per ogni RDBMS.
Le CLI sono un insiemi di funzioni messe a disposizione per il programmatore
che permettano di interagire con il DBMS. Rispetto al SQL Embedded con questa
libreria di funzioni si dispone di uno strumento più flessibile, meglio integrato con
il linguaggio di programmazione, con l’incoveniente di dover gestire esplicitamente
aspetti che in SQL Embedded erano risolti dal preprocessore. Le CLI si usano in
questo modo:
1. Si utilizza un servizio della CLI per creare una connessione con il DBMS.
2. Si invia sulla connessione un comando SQL, in forma di stringa, che rappresenta
la richiesta.
1.7. LE SOLUZIONI POSSIBILI
15
Figura 1.5: Un esempio di utilizzo di data classes [ERRORE: 2 FIGURE
UGUALI]
3. Si riceve come risposta del comando una struttura relazionale in un opportuno
formato (col linguaggio C# usato nella parte sperimentale della corrente tesi è
una DataTable); la CLI dispone di un certo insieme di primitive che permettono
di analizzare e descrivere la struttura del risultato del comando.
4. Al termine della sessione di lavoro, si chiude la connessione e si rilasciano le
strutture dati utilizzare per la gestione del dialogo.
Una seconda soluzione è quella di scrivere il codice SQL in appossite classi di
supporto (detti data classes) alle classi persistenti (vedi fig. 1.5).
Una terza soluzione è quella di delegare la gestione della persistenza degli oggetti
ad un modulo apposito, detto PL - Persistence Layer (vedi fig. 1.6).
Un PL nasconde i dettagli della persistenza (nonché della condivisione e della
transazionalità) al programmatore. Gli Object-Relational Mapper (ORM) sono una
categoria molto diffusa di PL e nel corso di questa tesi viene usato un ORM open
source chiamato NHibernate.
Qui di seguito ecco un esempio di codice che fa uso di un PL:
PersistenceManager pm = new PersistenceManager(...); Transaction tx
= pm.currentTransaction(); PersonaID pID = new PersonaID("1");
Persona p = (Persona) pm.getObjectByID(pID); p.setStipendio(1000);
tx.commit();
Persona è una classe entità, che fornisce soltanto metodi get e set, cioè che consentono
accesso pubblico in lettura e scrittura sui suoi attributi privati. Si può dire che in
questo esempio esiste un isomorfismo tra tabella nel mondo dei database relazionale
e la classe nel mondo OOP. Questo importante concetto verrà dettagliato in seguito.
CAPITOLO 1. IL PROBLEMA DELLA PERSISTENZA
DEI DATI
16
Figura 1.6: Un esempio di utilizzo del PL
Un PL nasconde al programmatore i dettagli di come gli oggetti vengono resi
persistenti. Esistono tre approcci principali per la realizzazione di un PL:
1. O/R Mapping
2. R/O Mapping
3. Incontro al centro
Nel primo approccio, il programmatore indica in un file di configurazione quali
sono le classi che vanno rese persistenti e quindi a partire da questi file viene generata
la base di dati e le data classes per le classi persistenti.
Nel secondo approccio, il programmatore indica in un file di configurazione la
base di dati relazionale di interesse e quindi, a partire da questi file, vengono generate
le data classes per accedere e modificare le tuple delle relazioni delle base di dati.
Nell’ultimo approccio, le classi persistenti e la base di dati vengono progettate e
realizzate in modo indipendente. Il programmatore indica in un file di configurazione
le corrispondenze tra classi persistenti e base di dati e quindi a partire da questi file
vengono generate le data classes per le classi persistenti.
Esistono altre soluzioni a questo problema:
1. Cursori
2. Uso di una struttura del tipo insieme di righe
Un cursore è uno strumento che permette a un programma di accedere alle righe
di una tabella una alla volta; esso viene definito su una generica query. La sintassi
SQL per la definizione di un cursore è la seguente:
declare NomeCursore [scroll] cursor for SelectSQL
only|update [of Attributo{, Attributo}]>]
[for <read
1.7. LE SOLUZIONI POSSIBILI
17
É importante analizzare la semantica dell’istruzione:
1. declare cursor : Definisce un cursore, associato ad una particolare query sulla
base di dati.
2. scroll : Opzionale, se si vuole permettere al programma di muoversi “liberamente” sul risultato della query.
3. for update: Opzionale, specifica se il cursore deve essere utilizzato nell’ambito di
un comando di modifica, permettendo di specificare eventualmente gli attributi
che saranno oggetto del comando di update.
La seconda soluzione consiste nell’utilizzare un linguaggio di programmazione che
abbia a disposizione dei costruttori di dati più potenti e in particolare riesce a gestire
in modo naturale una struttura del tipo insieme di righe. Esempi che adottano questa
soluzione sono: ADO, ADO.net, JDBC.
Per meglio chiarire gli aspetti pratici risultanti per i programmatori è bene illustrare brevemente le caratteristiche di alcune tra le soluzioni oggi maggiormente
diffuse:
1. ODBC : Interfaccia standard che permette di accedere a basi di dati in qualunque
contesto, realizzando interoperabilità con diverse combinazioni DBMS - Sistemi
Operativi - Reti.
2. OLEDB: Soluzione proprietaria Microsoft, basata sul metodo COM, che permette ad applicazioni Windows di accedere a sorgenti dati generiche, ovvero non
solo DBMS. Vedremo che questa soluzione sarà usata largamente nel progetto
di generazione di strati software che sarà descritta nel capitolo 5.
3. ADO: Soluzione proprietaria Microsoft che permette di sfruttare i servizi OLEDB,
utilizzando un’interfaccia record-oriented.
4. ADO.NET: Soluzione proprietaria Microsoft che adatta ADO alla piattaforma
.NET; offre un’interfaccia set-oriented e introduce i DataAdapter.
5. JDBC: Soluzione per l’accesso ai dati in Java sviluppata da Sun Microsystems;
offre in quel contesto un servizio simile a ODBC.
In particolare vediamo ora ADO.NET. In ADO.NET i dati vengono gestiti tramite
DataSet i quali costituiscono dei contenitori di oggetti (in particolare entro i DataSet
si trovano le DataTable, che rappresentano in memoria le normali tabelle del RDBMS
con tutte le loro caratteristiche). Parleremo più in dettaglio di come sono rappresentate in memoria le DataTable e il loro utilizzo entro il contesto degli ORM e del
progetto creato. Un DataSet permette anche la gestione di relazioni e vincoli di integrità tra gli oggetti al suo interno. Questa flessibilità è resa possibile dal fatto che
i DataSet rappresentano strutture che risiedono pienamente nell’ambiente del programma. Per concludere questa breve descrizione, aggiungo che il coordinamento tra
i DataSet e le sorgenti dati avviene tramite dei componenti specifici, che prendono il
nome di DataAdapter.
Capitolo 2
Architetture software a oggetti
In questo capitolo vengono introdotte le architetture ad oggetti e, in particolare, i
Design Pattern e il loro utilizzo. Si vedranno come le architetture stratificate sono
vantaggiose nella progettazione e realizzazione di progetti di medie-grandi dimensioni.
Inoltre si vedranno i primi ingredienti nella realizzazione di un ORM: le classi e gli
oggetti entità. Sarà presentato infine il ruolo degli ORM come strumento dato al
programmatore per legare il mondo della programmazione a oggetti con il mondo dei
database.
2.1
I Design Pattern
Progettare sistemi secondo il paradigma Object-Oriented non è facile e creare software Object-Oriented che sia anche riutilizzabile è ancora più difficile: una soluzione
dovrebbe essere specifica al problema ma abbastanza generale da poter essere riutilizzata. Di solito è molto difficile creare del software che risponda alle esigenze
funzionali alla prima scrittura e il programmatore in generale impiega molto tempo
ad imparare a creare del buon codice Object-Oriented. I programmatori esperti solitamente non risolvono i loro problemi partendo da zero ma riusano soluzioni o parte
di esse, sulle quali hanno lavorato in passato: in questo modo riutilizzano pezzi di
design e di codice che sono consolidati e testati. Il riutilizzo di elementi della fase di
progettazione è l’obiettivo dei design pattern: il semplice riutilizzo del codice porta
a degli indiscutibili vantaggi in termini di risparmio di tempo ma il poter usare dei
pezzi di design si pone ad un livello superiore. Come è noto, la fase di design è una
delle fasi più impegnative nel ciclo di sviluppo di un’applicazione software mentre la
scrittura del codice non è cosı̀ critica: il riutilizzo di micro-architetture nella fase di
progettazione ha dei vantaggi notevoli sia nella fase stessa che nella fase di programmazione, senza contare i riflessi sulla manutenzione del sistema. Alcune soluzioni
per di risolvere dei problemi a livello di progettazione si sono rivelate una costante
di molti progetti, anche in contesti diversi. Da qui l’idea di collezionare e documentare queste soluzioni sicure a dei problemi ricorrenti per migliorare lo sviluppo
di un sistema software. Per facilitare il riutilizzo di alcuni elementi di progettazione
che sono già stati sviluppati si ricorre ai design pattern, che hanno lo scopo di dare
un nome, spiegare e valutare pezzi importanti e ricorrenti nella progettazione del
software Object-Oriented. I design pattern aiutano a costruire del software che sia
riusabile ed evitare scelte che compromettano il riutilizzo dello stesso, inoltre possono migliorare la documentazione e la manutenzione di sistemi esistenti, in poche
parole aiutano a progettare in modo giusto in minor tempo. La prima e la più famosa
collezione di design pattern è contenuta nel libro di Gamma [13], più noto come libro della “Gang of 4”. In questo libro sono contenuti e documentati 23 design pattern.
18
2.1. I DESIGN PATTERN
19
Un design pattern sistematicamente dà un nome, motiva e spiega un concetto
generale che indirizza un problema di progettazione ricorrente nei sistemi Object
Oriented. Esso descrive il problema, la soluzione, quando applicare la soluzione e
le sue conseguenze. Inoltre dà suggerimenti sull’implementazione ed esempi. La
soluzione è personalizzata e implementata per risolvere il problema in un particolare
contesto.
Il problema dell’impedance mismatch che abbiamo visto nel capitolo precedente,
può essere trattato anche per mezzo di pattern. I pattern quindi sono degli schemi
di idee [14], ossia soluzioni preconfezionate, provate in diverse situazioni e standardizzate [15]. Essi si concentrano maggiormente sui concetti e meno sull’aspetto
implementativo. I pattern nella programmazione Object-Oriented sono usatissimi.
Vedremo che il progetto Generatore Strati Software si basa su un pattern chiamato
MVC. Ma prima di addentrarci in alcuni pattern fondamentali che ricorrono nello
studio degli ORM e nel mio progetto, vediamo alcune caratteristiche dei pattern e
vediamo come si classificano.
Nel libro dei Gang of four vengono identificati 23 tipi di design pattern, suddivisi
in 3 categorie:
1. Pattern strutturali
2. Pattern creazionali
3. Pattern comportamentali
I pattern strutturali consentono di riutilizzare degli oggetti esistenti fornendo
agli utilizzatori un’interfaccia più adatta alle loro esigenze. Qui troviamo:
• Adapter: Converte l’interfaccia di una classe in un’altra permettendo a due
classi di lavorare assieme anche se hanno interfacce diverse.
• Bridge: Disaccoppia un’astrazione dalla sua implementazione in modo che
possano variare in modo indipendente.
• Composite: Compone oggetti in strutture ad albero per implementare delle
composizioni ricorsive.
• Decorator: Aggiunge nuove responsabilità ad un oggetto in modo dinamico,
è un’alternativa alle sottoclassi per estendere le funzionalità.
• Facade: Provvede un’interfaccia unificata per le interfacce di un sottosistema
in modo da rendere più facile il loro utilizzo.
• Flyweigth: Usa la condivisione per supportare in modo efficiente un gran
numero di oggetti con fine granularità.
• Proxy: Provvede un surrogato di un oggetto per controllarne gli accessi.
• Private class data
• Extensibility
I pattern creazionali nascondono i costruttori delle classi e mettono dei metodi
al loro posto creando un’interfaccia. In questo modo si possono utilizzare oggetti
senza sapere come sono implementati.
20
CAPITOLO 2. ARCHITETTURE SOFTWARE A OGGETTI
• Abstract Factory: Provvede un’interfaccia per creare famiglie di oggetti in
relazione senza specificare le loro classi concrete.
• Builder: Separa la costruzione di un oggetto complesso dalla sua rappresentazione in modo da poter usare lo stesso processo di costruzione per altre
rappresentazioni.
• Factory method: Definisce un’interfaccia per creare un oggetto ma lascia
decidere alle sottoclassi quale classe istanziare.
• Prototype: Specifica il tipo di oggetti da creare usando un’istanza prototipo
e crea nuovi oggetti copiando questo prototipo.
• Singleton: Assicura che la classe abbia una sola istanza e provvede un modo
di accesso.
• Lazy initialization: É la tattica di instanziare un oggetto solo nel momento in
cui deve essere usato per la prima volta. É utilizzato spesso insieme al pattern
factory method.
I pattern comportamentali forniscono soluzione alle più comuni tipologie di
interazione tra gli oggetti.
• Chain of responsibility: Evita l’accoppiamento di chi manda una richiesta
con chi la riceve dando a più oggetti la possibilità di maneggiare la richiesta.
• Command: Incapsula una richiesta in un oggetto in modo da poter eseguire
operazioni che non si potrebbero eseguire.
• Iterator: Provvede un modo di accesso agli elementi di un oggetto aggregato
in modo sequenziale senza esporre la sua rappresentazione sottostante.
• Mediator: Definisce un oggetto che incapsula il modo in cui un insieme di
oggetti interagisce in modo da permettere la loro indipendenza.
• Memento: Cattura e porta all’esterno lo stato interno di un oggetto senza
violare l’incapsulazione in modo da ripristinare il suo stato più tardi.
• Observer: Definisce una dipendenza 1:N tra oggetti in modo che se uno cambia
stato gli altri siano aggiornati automaticamente.
• State: Permette ad un oggetto di cambiare il proprio comportamento a seconda
del suo stato interno, come se cambiasse classe di appartenenza.
• Strategy: Definisce una famiglia di algoritmi, li incapsula ognuno e li rende
intercambiabili in modo da cambiare in modo indipendente dagli utilizzatori.
• Interpreter: Dato un linguaggio, definisce una rappresentazione per la sua
grammatica ed un interprete per le frasi del linguaggio.
• Template method: Permette di definire la struttura di un algoritmo lasciando
alle sottoclassi il compito di implementarne alcuni passi come preferiscono.
• Visitor: Permette di separare un algoritmo dalla struttura di oggetti composti
a cui è applicato, in modo da poter aggiungere nuovi comportamenti senza dover
modificare la struttura stessa.
• Single-serving Visitor
2.1. I DESIGN PATTERN
21
• Hierarchical Visitor
• Event Listener
Esistono anche altri tipi di design pattern, ma questi non operano al livello di
progettazione del sistema. Essi sono suddivisi in:
1. Pattern architetturali
2. Pattern di metodologia
3. Pattern di concorrenza
I pattern architetturali operano ad un livello diverso (e più ampio) rispetto ai
design pattern, ed esprimono schemi di base per impostare l’organizzazione strutturale
di un sistema software. In questi schemi si descrivono sottosistemi predefiniti insieme
con i ruoli che essi assumono e le relazioni reciproche.
Qui si trovano i pattern: Broker, MVC, Repository, Client-Server, Reflection, Presentation Abstraction Control, Microkernel, Layers, Pipes and
Filters e Blackboard.
Nei pattern di metodologia si trovano: Responsibility e Make it run, make
it right, make it fast, make it small.
Nel caso di processi che eseguono contemporaneamente delle attività su dati condivisi si parla di concorrenza. Alcuni design pattern sono stati sviluppati per mantenere sincronizzato lo stato dei dati in tali situazioni. Si parla in tal caso di pattern di concorrenza. Essi sono suddivisi in: Active object, Balking, Double
checked locking, Guarded suspension, Leaders/followers, Monitor object,
Read-Write lock, Scheduler, Thread pool, Thread-specific storage, Token
passing synchronization e Reactor.
Al suo interno, un pattern è formato da questi quattro elementi:
1. Il nome, che è utile per descrivere la sua funzionalità.
2. Il problema nel quale il pattern è applicabile. Esso spiega il problema e il
contesto, a volte descrive strutture di classi o a volte il design di sistema. Può
includere anche una lista di condizioni.
3. La soluzione, che descrive in maniera astratta come il pattern risolve il problema. Descrive anche la responsabilità e le collaborazioni che compongono il
progetto.
4. Le conseguenze portate dall’applicazione del pattern. Servono per valutare i
costi-benefici dell’utilizzo del pattern.
Il nome del pattern Il problema La soluzione Infine le conseguenze
Vediamo adesso il pattern architetturale MVC che è stato usato nella progettazione del prototipo Generatore Strati Software (GSS 1.0) realizzato durante la
presente tesi.
22
CAPITOLO 2. ARCHITETTURE SOFTWARE A OGGETTI
Figura 2.1: Schema funzionale MVC
2.2
Il pattern MVC nella progettazione software
Questo modello prevede una netta separazione tra i dati, la rappresentazioni dei dati
e la logica di funzionamento del sistema [16] e [17].
Questo approccio porta a diversi vantaggi:
1. Separazione dei ruoli e delle relative interfacce;
2. Indipendenza tra business data (model), logica di presentazione (view) e logica
di controllo (controller);
3. Viste diverse per il medesimo model;
4. Maggior semplicità per il supporto a nuove tipologie di client: basta scrivere la
vista ed il controller appropriati riutilizzando il model esistente;
Nella figura (fig. 2.1) viene mostrata lo schema funzionale del pattern MVC
(Model-View-Controller).
Nella figura si vedono i tre componenti funzionali del MVC: il model, view e
controller.
2.3. LE ARCHITETTURE STRATIFICATE ED I LORO VANTAGGI
23
• Model: Esso individua la rappresentazione dei dati dell’applicazione e le regole
di business con cui viene effettuato l’accesso e la modifica a tali dati. Il modello
non e a conoscenza dei suoi controller e tanto meno delle sue view; non contiene
riferimenti ad essi, ma è il sistema che si prende la responsabilità di mantenere
i link tra il modello e le sue view e di notificare quest’ultime le variazioni nei
dati del modello;
• View: È la presentazione visuale dei dati all’utente e interagisce con il modello attraverso un riferimento ad esso. Uno stesso modello può quindi essere
presentato secondo diverse viste (Form, WPF, Web, Console, ecc);
• Controller:È colui che interpreta le richieste della view in azioni che vanno ad interagire con il model (di cui possiede un riferimento), aggiornando
conseguentemente la view stessa.
La suddivisione in livelli permette di gestire la progettazione e la programmazione
dei vari componenti in maniera indipendente tra loro, collegandoli solamente a runtime.
2.3
Le architetture stratificate ed i loro vantaggi
I sistemi software stanno diventando sempre più complessi e più grandi. Nell’ambito
della progettazione cresce sempre di più la necessità della configurazione strutturale
del sistema. In tal modo nasce il concetto di Archittetura Software. Essa viene
definita come [18]:
L’Archittetura software comprende la descrizione degli elementi partendo dai
quali vengono creati i sistemi, le interazioni tra essi, i modelli che ne gestiscono
la composizione e i limiti rispetto a questi modelli. Un determinato sistema viene
descritto attraverso un insieme di componenti e le interazioni di questi.
Un altra definizione, estremamente pragmatica, di architettura software è la
seguente:
L’Architettura software è l’insieme di decisioni progettuali le quali se non sonfatte
in modo coretto, causeranno il fallimento del progetto.
Prima d’arrivare a definire il concetto di archittetura stratificata e fornirne i
vantaggi, è bene capire come si è arrivati alla necessità di ricorrere ad architetture
nella strutturazione di un progetto software.
Il problema principale per chi si occupa di software è la manutenzione [6]. Essa
è definita come la fase che segue l’entrata in servizio di un software.
Il termine manutenzione viene usato tipicamente per descrivere due attività distinte:
1. Manutenzione evolutiva
2. Manutenzione ordinaria
24
CAPITOLO 2. ARCHITETTURE SOFTWARE A OGGETTI
Figura 2.2: Il Mainframe
La prima rappresenta i cambiamenti che il software deve subire per adattarsi
alle nuove specifiche dovute a mutamenti dei requisiti funzionali cui il software deve
rispondere. Il secondo è la rimozione di errori che in realtà non avrebbero dovuto
essere presenti, ma che sono sfuggiti alle fasi di test prima della messa in produzione
del software stesso. La maggior parte del costo del software sta nella manutenzione.
Per ridurre questi costi, si sono sviluppate architetture software via via sempre più
stabili.
Inizialmente ci fù il Mainframe. Esso prevedeva che tutto debba essere centralizzato e il Mainframe ne costituiva l’unità centrale, spesso chiamato il cervellone.
Ancor’oggi questa tipo di archittetura la troviamo nel settore bancario. A questo
Mainframe sono collegati un numero abbastanza elevato di terminali, per esempio i
diffusissimi IBM 3270. Essi sono privi di sistema operativo, dotati di poca memoria
e indipendenti da ciò che accade all’interno dei Mainframe. Nella figura 2.2. viene
rappresentato un Mainframe.
La manutenzione di questi sistemi con il passare del tempo è diventata sempre
più complessa, conducendo al concetto di legacy.
Con questo termine intendiamo un oggetto di cui non si può fare a meno, ma che
nessuno riesce più a dominare [6] e [19]. Molto spesso, associata alla legacy c’è poi
una situazione di software ormai inadeguato, che, più che subire degli aggiornamenti,
è soggetto a continue correzioni, spesso non documentate, che rendono le ulteriori
manutenzioni più difficili di giorno in giorno.
Il mainframe monolitico era destinato a scomparire sia a causa del legacy sia a
causa dei problemi di costo di manutenzione e scalabilità. La soluzione di questi
problemi venne trovata ribaltando la situazione ovvero nel diminuire il carico computazionale del server e dotare invece il terminale di funzionalità diciamo più intelligenti.
Per essere più formali, possiamo dire che questa architettura è la logica estensione
2.3. LE ARCHITETTURE STRATIFICATE ED I LORO VANTAGGI
25
Figura 2.3: Possibili configurazioni di un sistema Client-Server basato sul
modello 2-Tier
alla programmazione modulare il cui pressuposto base è la separazione di grossi stralci
di codice in tanti parti detti moduli che consentono uno sviluppo più facile e una
migliore manutenzione; inoltre non è necessario che questi moduli debbano essere
eseguiti all’interno dello stesso spazio di memoria.
Questa archittetura va sotto il nome di Client-Server.
Il Client è il modulo che esegue le richieste dei servizi e il Server quello che mette
a disposizione i servizi.
In quasi tutte le applicazioni interattive, ossia destinate ad essere utilizzate da un
operatore umano, si possono riconoscere le tre differenti componenti:
1. L’interfaccia utente
2. Il Business-Logic
3. Dati
Nel momento in cui i 3 elementi logici vengono “spalmati” su 2 elementi fisici,
l’archittetura prende il nome di 2-Tier. Se consideriamo un ambiente tipicamente
orientato alle basi di dati, possiamo schematizzare i tre livelli appena indicati come
in figura 2.3.
Nelle applicazioni 2-Tier i primi due componenti (interfaccia e business-logic) sono
unificati e il terzo è separato e rappresenta i servizi a cui accedere (es. base di dati).
Il problema di questa architettura è la scalabilità.
Nella figura possiamo vedere tre casi (A, B e C) che rappresentano come una applicazione 2-Tier può essere scritta. Purtroppo gran parti dell’applicazioni rientrano
26
CAPITOLO 2. ARCHITETTURE SOFTWARE A OGGETTI
Figura 2.4: I componenti logici di una architettura 3-Tier
nel caso C, dove non c’è una netta separazione fra server e client e ogni tier fa una
parte di quello che dovrebbe fare e una parte di quello che non dovrebbe fare.
Le architetture a 2-Tier col passare degli anni hanno cominciato a dimostrare i
loro limiti e gli sviluppatori si sono accorti che non erano più adatte a supportare il
peso delle nuove funzionalità proposte dai manager.
Per questi motivi è nata l’architettura 3-Tier, in cui i 3 elementi logici vengono
distribuiti su 3 elementi fisici. Scrivere applicazioni in questa nuova architettura
presenta una complessità decisamente maggiore ma che viene ripagata nel tempo,
migliorando molto la manutenzione.
Nella figura 2.4. vengono mostrati i componenti logici di un architettura 3-Tier.
Il presentation service gestisce l’interfaccia utente verso il sistema. Il process
service agisce come sorta di buffer tra il livello superiore e quello inferiore. Il data
service rappresenta di solito il database vero e proprio, quindi sia i dati propriamente
detti sia la logica che consente di aggiornarli, cancellarli e modificarli.
Nella figura 2.5. vengono mostrati invece i componenti fisici di un architettura
3-Tier.
A questo punto è possibile descrivere l’architettura stratificata. Come dice il
nome, quest’architettura è suddivisa in livelli. Ogni livello fornisce fornisce un servizio
più astratto ai livelli superiori, rispetto a quanto ad esso fornito da quelli inferiori.
La stratificazione favorisce lo sviluppo incrementale ed evoluzione.
2.4
Classi ed oggetti entità
Uno degli ingredienti fondamentali quando si parla di ORM sono le classi entità.
Prima avevamo parlato di achitetture software, più precisamente di architettura a
3 livelli o 3-Tier. In questa architettura i tre livelli sono: presentazione, dominio e sorgente dati. Tutta la logica business si trova nel livello dominio e qui che collocheremo
gli oggetti entità.
É chiarificante esaminare le caratteristiche di uno degli standard maggiormente
2.4. CLASSI ED OGGETTI ENTITÀ
27
Figura 2.5: I componenti logici di una architettura 3-Tier
usati per questi oggetti entità, usato nel linguaggio di programmazione Java, che
viene chiamato POJO(Plain Old Java Object). I POJO sono uno standard basato
sui Bean entità, simile agli EJB.
Questi file POJO devono seguire la seguente semantica:
1. Il costruttore deve essere privo di argomenti;
2. Tutti gli attributi privati che si vuole considerare devono avere dei metodi
accessori ( chiamati getter e setter essendo basati sullo standard getAttributo
per la lettura e setAttributo per la scrittura);
Gli attributi privati sono resi pubblici grazie ai metodi accessori. Risulta possibile
anche definire degli attributi virtuali allorchè i metodi accessori collegano il valore
ritornato a quello di un attributo diverso da quello corrispondente al loro nome.
Questo standard è stato esteso anche al linguaggio di programmazione C#, dove
gli oggetti entità corrispondenti vengono chiamati POCO.
Per meglio chiarire come è fatta una classe entità ecco un esempio, relativo al
modello entità-relazione della figura 2.6.
La classe entità relativa alla tabella Dipartimento può essere ottenuta con le
seguenti caratteristiche:
public class Dipartimento {
// ATTRIBUTI PRIVATI private Long
private String sede;
id; private String nome;
//COSTRUTTORE DI DEFAULT public Dipartimento() { }
public Dipartimento(String nome,String sede) { this.nome=nome;
this.sede=sede; }
28
CAPITOLO 2. ARCHITETTURE SOFTWARE A OGGETTI
Figura 2.6: Un agenzia
//METODI ACCESSORI (getAttributo E setAttributo)
public Long getId() { return id; }
private void setId(Long id) { this.id = id; }
public String getNome() { return nome; }
public void setNome(String nome) { this.nome = nome; }
public String getSede() { return sede; }
public void setSede(String sede) { this.sede = sede; }
//METODI PER LA CONVERSIONE public String toString() { ....
} }
L’equivalente nel mondo C#, il POCO, è il seguente, in cui i metodi accessori
sono sostituiti dalle proprietà che il C# eredita dal Visual Basic:
public class Dipartimento { // ATTRIBUTI PRIVATI private long
private string nome; private string sede;
//COSTRUTTORE DI DEFAULT public Dipartimento() {}
id;
2.5. ALGORITMI E STRUTTURE DATI
29
public Dipartimento(string nome, string sede) { this.nome =
nome; this.sede = sede; }
//METODI ACCESSORI (GET e SET)
public long Id { get { return id;
} set { id = value; } }
public string Nome { get { return nome;
}
} set { nome = value; }
public string Sede { get { return sede;
} }
} set { sede = value; }
Come si può notare non c’è niente di complesso, e si crea un isomorfismo tra
la tabella Dipartimento del database e la classe entità Dipartimento. Per essere
maggiormente chiari, si parla di isomorfismo fra classi entità e tabelle, quando i
nomi dei campi sono gli stessi dei nomi degli attributi della classe e i tipi dati sono
corrispondenti (ad esempio, String e Varchar, double e numeric).
Dal punto di vista esterno, la classe entità viene trattata esattamente come se
fosse una struttura (es. struct in C). Come si vedrà meglio nei capitoli seguenti, gli
elementi interni della libreria ORM NHibernate provvedono al caricamento di questa
struttura in presenza di un’operazione di lettura del database e alla lettura della stessa struttura per scrivere nel database nel caso opposto, il tutto in modo trasparente
per il programmatore. Nel caso di NHibernate, oltre alla classe entità serve anche il
file mapper, che stabilisce una corrispondenza tra campi ed attributi, per mappare
gli oggetti nel mondo Object-oriented e le tabelle nel mondo RDBMS.
Formalmente, indicando con:
• O: L’oggetto entità;
• T: Tabella o vista nel database;
• M: Il mapper in formato XML;
esiste la condizione:
∀O, ∃ < T, M, O >
2.5
Algoritmi e strutture dati
Le strutture dati sono una delle caratteristiche fondamentali per una piattaforma di
sviluppo. La necessità di mantenere in memoria un insieme di informazioni prima
della loro scrittura o dopo la loro lettura è infatti un’esigenza che si presenta in ogni
programma, dal più semplice al più complesso.
30
CAPITOLO 2. ARCHITETTURE SOFTWARE A OGGETTI
In un programma a oggetti, l’importanza delle strutture dati è ridimensionata
rispetto a un programma procedurale, in quanto sono gli oggetti stessi a mantenere le
informazioni. Nonostante questo, non potrebbe esserci cardinalità (es. più oggetti figli
per un oggetto padre) se non ci fossero strutture dati pronte a ospitare gli oggetti di
applicazione. Infatti, come si vedrà anche nel prossimo capitolo, senza una struttura
dati come la DataTable si sarebbe in grado di importare in memoria tutta una tabella
di un database, per esempio.
Diamo una definizione più precisa di struttura dati, presa da [20].
Una struttura dati è un particolar tipo di dato, caratterizzata più dall’organizzazione imposta agli elementi che la compongono, che dal tipo degli elementi
stessi.
Per essere più precisi, una struttura dati consiste di:
1. Un modo sistematico di organizzare i dati;
2. Un insieme di operatori che permettono di manipolare elementi della struttura
o di aggregare elementi per costruire altri agglomerati;
Quello che segue è una classificazione delle strutture di dati in base alle caratteristiche presentate dalla disposizione dei dati, dal loro numero e dal loro tipo.
• Lineari: Gli agglomerati sono formati da dati disposti in sequenza, tra i quali
si individua un primo elemento, un secondo, ecc.;
• Non lineari: In cui non si individua una sequenza;
• A dimensione fissa: Il numero di elementi dell’agglomerato rimane sempre
costante nel tempo;
• A dimensione variabile: Il numero di elementi può aumentare o diminuire
nel tempo;
• Omogenee: I dati sono tutti dello stesso tipo;
• Non omogenee: I dati non sono tutti dello stesso tipo;
Vedremo in seguito, che l’utilizzo di strutture di dati sono fondamentali nella buon
riuscita di un progetto software. Nel progetto GSS 1.0. o Generatore Strati Software
vengono utilizzate tante strutture dati: HashTable, ArrayList, DataTable, SortedList,
ecc. Ovviamente queste strutture sono definite nel framework .NET versione 3.5.
Prima di vedere un quadro generale delle strutture dati esistenti e la focalizzazione
delle stesse usate nel corso del progetto, è bene parlare di come descriverle e dare
qualche accenno sulla teoria della complessità di un algoritmo.
Nel descrivere una struttura dati, è opportuno distinguere, come per tutti i tipi
di dato, tra la specifica astratta della proprietà della struttura e i possibili modi con
i quali si può memorizzare la struttura ed eseguire le operazioni.
La specifica è divisa in due aspetti fondamentali:
1. Specifica sintattica;
2. Specifica semantica;
2.5. ALGORITMI E STRUTTURE DATI
31
Figura 2.7: Un vettore
La specifica sintattica, cioè la notazione con cui sono indicati sia i tipi di dato
che le operazioni, fornisce l’elenco dei nomi dei tipi di dato utilizzati per definirne la
struttura, delle operazioni specifiche della struttura stessa, e delle costanti.
La specifica astratta, cioè il significato che diamo ai tipi di dato e le loro operazioni, associa un insieme matematico ad ogni nome di tipo introdotto nella specifica
sintattica, un valore ad ogni costante, e una funzione ad ogni nome di operatore.
Questa funzione è definita in modo matematico, esplicitando una coppia di condizioni:
< P recondizione, P ostcondizione > sui domini di partenza e di arrivo.
La Precondizione stabilisce quando l’operatore è applicabile.
IF Precondizione is null THEN l’operatore è sempre applicabile;
ELSE l’operatore non è applicabile;
La Postcondizione indica come il risultato sia vincolato agli argomenti dell’operatore.
Vediamo un esempio di Per meglio chiarire il concetto ecco un esempio di specifica sintattica e astratta con le relative precondizioni e postcondizioni attuato sulla
struttura dati vettore.
Il vettore è una struttura lineare omogenea a dimensione fissa. (Vedi figura 2.8.)
Specifica sintattica:
Nomi dei tipi:
VETTORE, INTERO, TIPO ELEMENTO
Nomi degli operatori:
CREA VETTORE, LEGGI VETTORE, SCRIVI VETTORE
dove:
CREA VETTORE: <>−→ VETTORE
LEGGI VETTORE: < V ET T ORE, IN T ERO >−→ TIPO ELEMENTO
SCRIVI VETTORE: < V ET T ORE, IN T ERO, T IP O ELEM EN T O >−→
VETTORE
Specifica semantica:
32
CAPITOLO 2. ARCHITETTURE SOFTWARE A OGGETTI
VETTORE: L’insieme di tutte le sequenze di n elementi di tipo
TIPO ELEMENTO (grazie a questo TIPO ELEMENTO possiamo dichiarare
qualsiasi tipo, per esempio, reali, interi, ecc.);
v: Valore generico del tipo VETTORE;
i: Valore generico del tipo INTERO;
e: Valore generico del tipo TIPO ELEMENTO;
CREA VETTORE = v
Postcondizione : ∀i, 1 ≤ i ≤ n, l’i-esimo elemento v(i) del vettore è uguale ad un
prefissato elemento di tipo TIPO ELEMENTO;
LEGGI VETTORE(v, i) = e
Precondizione : 1 ≤ i ≤ n;
Postcondizione : e = v(i);
SCRIVI VETTORE(v, i, e) = v’
Precondizione : 1 ≤ i ≤ n;
Postcondizione : v 0 (i) = e, v 0 (j) = v(j), ∀j : 1 ≤ j ≤ n e j 6= i;
Il CREA VETTORE specifica come debba essere inizializzato il vettore. LEGGI VETTORE restituisce il valore contenuto nell’i-esima posizione del vettore. SCRIVI VETTORE
restituisce un nuovo vettore v’ con tutti i valori degli elementi uguali a quelli di v,
tranne il valore dell’i-esima posizione che è posto al valore e.
La scelta nell’utilizzare una struttura dati piuttosto che un’altra non è sempre
facile. Bisogna analizzare le varie operazioni (inserimento, cancellazione, ricerca,
copia, ecc.) in termini di tempo di calcolo. Per risolvere un problema spesso sono
disponibili molti algoritmi diversi. Per scegliere un algoritmo invece che un altro un
criterio potrebbe essere quello di valutare la sua bontà in base alla quantità di risorsa
utilizzata per il calcolo. Quindi si considera:
1. Spazio: necessario per memorizzare e manipolare i dati;
2. Tempo: il tempo richiesto per eseguire le azioni elementari;
Queste azioni elementari sono: operazioni aritmetiche, logiche, di confronto e
di assegnamento. Di solito il costo delle operazioni è valutato nel caso pessimo,
cioè sul dato d’ingresso più sfavorevole, tra tutti quelli di dimensione n. A volte è
valutato anche nel caso medio, cioè mediando su tutti i possibili dati di dimensione
n, tenendo conto della probabilità con cui ciascun dato può occorrere.
Nella valutazione del tempo di calcolo di una procedura T(n) si ricorre alla complessità computazionale asintotica in ordine di grandezza. Usiamo le seguenti notazioni:
O, Ω e θ definite nel seguente modo:
2.5. ALGORITMI E STRUTTURE DATI
33
O(f(n)), l’insieme di tutte le funzioni g(n) tali che esistono due costanti positive
c ed m per cui g(n) ≤ c ∗ f (n), ∀n ≥ m.
Ω(f (n)), l’insieme di tutte le funzioni g(n) tali che esistono due costanti positive
c ed m per cui c ∗ f (n) ≤ g(n), ∀n ≥ m.
θ(f (n)), l’insieme di tutte le funzioni g(n) tali che esistono tre costanti positive
c,d ed m per cui c ∗ f (n) ≤ g(n) ≤ d ∗ f (n), ∀n ≥ m.
Una classificazione degli ordini di grandezza è la seguente:
• θ(1) : ordine costante
• θ(log(n)): ordine logaritmico
• θ(n): ordine lineare
• θ(n ∗ log(n)): ordine pseudolineare
• θ(n2 ): ordine quadratico
• θ(n3 ): ordine cubico
• θ(2n ): ordine esponenziale in base 2
• θ(n!): ordine fattoriale
• θ(nn )): ordine esponenziale in base n
Vediamo adesso le strutture dati esistenti.
Quella che segue è una classificazione delle strutture dati che vengono utilizzate
quotidianamente durante la fase di implementazione di un progetto software.
1. Vettori
2. Liste
3. Pile
4. Code
5. Alberi
6. Insiemi
7. Dizionari
8. Code con priorità
9. Alberi bilanciati di ricerca
10. Grafi
34
CAPITOLO 2. ARCHITETTURE SOFTWARE A OGGETTI
Figura 2.8: Alcune realizzazioni di una lista con puntatori
Le liste
Una Lista è una sequenza di elementi di un certo tipo, in cui è possibile aggiungere
o togliere elementi. Per far questo, occorre specificare la posizione relativa all’interno della sequenza nella quale il nuovo elemento va aggiunto o dalla quale il vecchio
elemento va tolto. La lista è una struttura a dimensione variabile è si può accedere
direttamente solo ad un ristretto sottoinsieme di elementi (di solito al primo a all’ultimo). Per accedere ad un generico elemento, occorre scandire sequenzialmente gli
elementi della lista: partendo da un elemento accessibile direttamente, ci si sposta
via via da un elemento ad uno adiacente nella sequenza, fino a raggiungere l’elemento
desiderato.
Le liste si possono realizzare in tanti modi: Puntatori o cursori.
Nella figura 2.9. vengono mostrati alcuni realizzazioni possibili per una lista con
l’uso di puntatori.
Le pile
Una Pila o Stack è una sequenza di elementi di un certo tipo in cui è possibile
aggiungere o togliere soltanto ad un estremo (la testa) della sequenza. Viene chiamata
struttura LIFO (Last In First Out), ovvero l’ultimo da arrivare e il primo ad essere
servito (es. pila di piatti). Una pila può essere anche vista come una particolare lista
2.5. ALGORITMI E STRUTTURE DATI
35
Figura 2.9: Realizzazione di una pila con un vettore
Figura 2.10: Realizzazione di una coda con un vettore circolare
in cui l’ultimo elemento inserito è anche il primo ad essere rimosso e non è possibile
accedere ad alcun elemento che non sia quello in testa.
Una tipica realizzazione di tale struttura è con l’utilizzo di un vettore (come
mostra la figura 2.10.).
Le code
La Coda o Queue è una sequenza di elementi di un certo tipo, in cui è possibile
aggiungere elementi ad un estremo (il fondo) e togliere elementi dall’altro estremo
(la testa). Viene chiamata struttura FIFO (First In First Out), ovvero il primo ad
arrivare è anche il primo ad essere servito (es. coda ad uno sportello in banca). Una
coda può essere anche vista come una particolare lista in cui il primo elemento inserito
è anche il primo ad essere rimosso è non è possibile accedere ad alcun elemento che
non sia quello in testa o quello in fondo.
Una tipica realizzazione di tale struttura è con l’utilizzo di un vettore circolare
(come mostra la figura 2.11.).
36
CAPITOLO 2. ARCHITETTURE SOFTWARE A OGGETTI
Figura 2.11: Un albero binario di decisione per l’ordinamento di tre numeri
Gli alberi
Un Albero ordinato è formato da un insieme finito di elementi, detti nodi. Se
tale insieme non è vuoto, allora un particolare nodo è designato come radice, ed i
rimanenti nodi, se esistono, sono a loro volta partizionati in insiemi ordinati disgiunti,
ciascuno dei quali è un albero ordinato.
L’utilizzo degli alberi ricorre in tante situazioni (es. albero di derivazione di una
grammatica context free, organizzazione di un file system, albero geneaologico, ecc.).
Esistono particolari alberi chiamati alberi binari. Essi sono un particolare albero
ordinato in cui ogni nodo ha al più due figli e si fa distinzione tra il figlio sinistro e il
figlio destro di un nodo (si veda la figura 2.12.).
Gli insiemi
Un insieme o SET è una collezione (o famiglia) di elementi distinti (detti membri
o componenti) dello stesso tipo. L’insieme è la struttura matematica fondamentale, e può essere descritto o elencadone tutti gli elementi (insiemi definiti per elencazione) o stabilendo una proprietà che ne caratterizzi gli elementi (insiemi definiti
per proprietà).
Esistono diversi modi nella realizzazioni di insiemi: liste non ordinate, vettore
booleano, liste ordinate e mfset.
I Dizionari
Un Dizionario è un caso particolare di insieme, in cui è possibile verificare l’appartenenza di un elemento, cancellare un elemento e inserire un nuovo elemento.
Esistono anche qui diverse possibili realizzazioni di tale strutture: vettore ordinato, tabella hash, ecc.
Ci soffermiamo sulle HashTable o tabelle hash, poichè sono utilizzate nel progetto di generazioni strati software nella creazione, come vedremo, di codice automatico
nel realizzare un piccolo ORM.
Gli elementi di un dizionario vengono chiamati chiavi. La realizzazione di un
dizionario con queste tabelle hash si basa sul concetto di ricavare direttamente dal
valore della chiave la posizione che la chiave stessa dovrebbe occupare in un vettore.
2.5. ALGORITMI E STRUTTURE DATI
37
Formalmente:
K: L’insieme di tutte le possibili chiavi distinte;
V: Vettore dove viene memorizzato il dizionario;
m: La dimensione del vettore V;
L’ideale sarebbe avere una funzione d’accesso H:K −→ {1, ..., m} che permetta di
ricavare la posizione H(k) della chiave k nel vettore V in modo che ∀k1 , k2 ∈ K, con
k1 6= k2 , risulti H(k1 ) 6= H(k2 ).
Per realizzare una tabella hash in maniera efficiente, occorre:
1. Una funzione H, detta funzione hash, che sia calcolabile velocemente (in
tempo O(1)) e che distribuisca le chiavi uniformemente nel vettore V, al fine di
ridurre il numero di collisioni tra le chiavi diverse che hanno lo stesso indirizzo
hash;
2. Un metodo di scansione, per reperire chiavi che hanno trovato la loro posizione occupata, che permetta di esplorare tutto il vettore V e non provochi
la formazione di agglomerati di chiavi;
3. La dimensione m del vettore V deve essere una sovrastima (di solito il doppio)
del numero di chiavi attese, onde evitare di riempire V completamente;
Spendiamo ancora qualche parola sulla generazione di indirizzi hash (funzioni
hash) e sui diversi metodi di scansione.
Con la funzione hash, si vuole ricavare l’output (cioè un numero intero) che sia
scorrelato dalla struttura dell’input (la chiave stessa). L’algoritmo che calcola la
funzione hash non è casuale, in quanto se si ricalcola più volte H(k) per la stessa
chiave k si ottiene sempre lo stesso valore, ma l’indirizzo hash si comporta da un
punto di vista statico come se fosse stato davvero prodotto con uno o più lanci casuali
di una moneta.
Per definire funzioni hash, è conveniente considerare la rappresentazione binaria
bin(k) della chiave k. Se la chiave k non è numerica, bin(k) è data dalla concatenazione
della rappresentazione binaria di ciascun carattere che la compone.
Denotiamo con int(b) il numero intero rappresentato da una stringa binaria b.
Esistono quattro buoni metodi di generazione di indirizzi hash:
1. H(k) = int(b), dove b è un sottoinsieme di p bit di bin(k), solitamente estratti
nella posizioni centrali;
2. H(k) = int(b), dove b è dato dalla somma modulo 2, effettuata bit a bit, di
diversi sottoinsiemi di p bit di bin(k);
3. H(k) = int(b), dove b è un sottoinsieme di p bit estratti dalla posizioni centrali
di bin(int(bin(k))2 );
4. H(k) è uguale al resto della divisione di int(bin(k)) ∗ m;
I metodi di scansione si suddividono in esterni e interni a seconda che le chiavi
siano memorizzate all’esterno o all’interno del vettore V.
Metodi esterni:
38
CAPITOLO 2. ARCHITETTURE SOFTWARE A OGGETTI
Figura 2.12: Una rappresentazione di una tabella hash
• Liste di trabocco: Il vettore V contiene in ogni posizione, l’identificatore di
una lista (lista di trabocco) e tutte le chiavi che collidono sullo stesso indirizzo
hash vengono inserite nella stessa lista. Un vantaggio di queste liste è il non
formarsi di agglomerati e di non imporre alcun limite di capacità del dizionario.
Metodi interni:
Sia fi la funzione che viene utilizzata nei diversi metodi di scansione interna.
• Scansione lineare: fi = (H(k) + h ∗ i)modm, dove h è un intero primo con
m. Lo svantaggio è la non riduzione di agglomerati di chiavi;
• Scansione quadratica: fi = (H(k) + h ∗ i + i ∗ (i − 1)/2)modm, dove m è
un numero primo. Si riduce l’effetto di agglomerazione dovuto alla scansione
lineare. Uno svantaggio è la non riuscita di non includere tutte le posizioni di
V;
• Scansione pseudocasuale: fi = (H(k)+ri )modm, dove ri è l’i-esimo numero
generato da un generatore di numeri pseudo-casuali. Elimina gli svantaggi delle
due scansioni precedenti;
• Hashing doppio: fi = (H(k) + i ∗ F (k))modm, dove F è un’altra funzione
hash diversa da H. Evitiamo la formazione di agglomerati;
Una rappresentazione possibile di una tabella hash in figura 2.13.
Le code con priorità
Una coda con priorità è un particolare insieme, sugli elementi del quale è definita
una relazione ≤ di ordinamento totale, in cui è possibile inserire un nuovo elemento
o estrarre l’elemento minimo.
Si possono realizzare queste code con alberi binari con gli elementi disposti in uno
heap (vettore).
Gli alberi bilanciati di ricerca
Gli elementi di un insieme A possono essere contenuti nei nodi di un albero binario
B, detto albero binario di ricerca, che verifica le seguenti proprietà:
2.6. CLASSI ED OGGETTI CONTENITORI
39
Figura 2.13: Un albero binario di ricerca
Figura 2.14: Un grafo orientato
1. Per ogni nodo u, tutti gli elementi contenuti nel sottoalbero radicato nel figlio
sinistro di u sono minori dell’elemento contenuto in u;
2. Per ogni nodo u, tutti gli elementi contenuti nel sottoalbero radicato nel figlio
destro di u sono maggiori dell’elemento contenuto in u;
Un esempio di tale albero in figura 2.14.
I grafi
Un grafo orientato è una coppia G =< N, A >, con N insieme finito di elementi, detti
nodi (vertici), ed A insieme finito di coppie ordinate di nodi, detti archi (linee). Un
esempio in figura 2.15.
Una possibile realizzazione di tale struttura può essere: uso di matrici di adiacenza, insiemi di adiacenza.
2.6
Classi ed oggetti contenitori
Nella programmazione Object-Oriented, un oggetto contenitore è una classe di
oggetti che è preposta al contenimento di altri oggetti. Questi oggetti usualmente
possono essere di qualsiasi classe, e possono anche essere a loro volta dei contenitori.
40
CAPITOLO 2. ARCHITETTURE SOFTWARE A OGGETTI
Esempi di queste classi contenitori sono: insiemi, stack, pila, mappe, code, ecc...,
ossia gli elementi visti nel paragrafo sulle strutture dati.
La progettazione orientata agli oggetti utilizza e può modificare i risultati dell’analisi per questioni implementative. Queste modifiche però devono essere ridotte
al minimo per mantenere coerenza con l’analisi e con le specifiche.
Molto spesso tra le classi intercorrono delle relazioni o associazioni. Possiamo
avere:
• Associazioni 0-1 o 1-1: bisogna aggiungere alla classe del cliente un attributo
che riferisce all’oggetto della classe fornitore, e il valore dell’oggetto della classe
fornitore (siamo nel caso di composizione);
• Associazioni 0-N o N-N: si utilizza una classe contenitore in cui le istanze
sono collezioni a oggetti della classe fornitore, inoltre viene comunque aggiunto
al cliente un attributo che rappresenta per valore o per riferimento l’istanza
della classe contenitore;
Da ciò, possiamo definire meglio che cosa si intende per classe contenitore.
Un classe contenitore è una classe le cui istanze appartengono ad altre classi.
Se gli oggetti contenuti sono in numero fisso e senza ordine, allora si può utilizzare
un vettore; viceversa, se gli oggetti sono in numero variabile, o è chiesto che abbiano
un ordine, allora occorre una classe contenitore. Queste classi contenitore hanno funzionalità minime (memorizzare, aggiungere, togliere, trovare un oggetto, enumerare
gli oggetti) e possono essere classificate in base al modo in cui contengono gli oggetti,
o all’omogeneità/eterogeneità degli oggetti contenuti.
Esistono quattro tipi di contenimento:
• Contenimento per valore: L’oggetto contenuto è memorizzato nella struttura dati del contenitore, ed esiste proprio perchè è contenuto fisicamente in
un altro oggetto. Quando un oggetto viene inserito nel contenitore, viene duplicato, e la distruzione del contenitore implica la distruzione degli oggetti
contenuti;
• Contenimento per riferimento: L’oggetto contenuto ha una propria esistenza e può essere contenuto in diversi contenitori, infatti quando viene inserito
nel contenitore non viene copiato, ma ne viene memorizzato il riferimento. Per
questo motivo, alla distruzione del contenitore, l’oggetto contenuto non viene
eliminato;
• Contenimento di oggetti omogenei (valore o riferimento):È sufficiente
l’uso di template (classi generiche o parametriche). Il tipo degli oggetti contenuti viene lasciato generico e si pensa soprattutto agli algoritmi di gestione
della collezione. Quando serve una classe contenitore di oggetti di una classe
specifica, basta istanziare una classe generica specificando il tipo dell’oggetto;
• Contenimento di oggetti eterogenei (riferimento): È necessario utilizzare l’ereditarietà e sfruttare la proprietà che un puntatore alla superclasse può
puntare alle istanze di qualunque sottoclasse. La classe contenitore può essere
generica, ma solo per la gestione dei riferimenti agli oggetti contenuti.
Ma perchè uno sviluppatore può avere bisogno di implementare una sua collezione
(contenitore)?
Sostanzialmente ci sono due ragioni:
2.6. CLASSI ED OGGETTI CONTENITORI
41
• Per implementare un oggetto di Business contenitore: Nella implementazione di oggetti che appartengono al livello di astrazione applicativo spesso
è necessario implementare un oggetto che è anche un contenitore; in concreto
si può pensare alle relazioni tra Ordini e Ordine, tra Ordine e RigaOrdine e
tra Cliente e Condizioni di Pagamento. Dall’oggetto contenitore si vuole poter
accedere ad un elemento, ciclare tutti gli elementi ed aggiungere o rimuovere
elementi, cioè ciò che fa una collezione, ma una collezione che deve operare
esclusivamente con elementi di un determinato tipo (per esempio RigaOrdine)
e allo stesso tempo fornire altri servizi (per esempio la persistenza, la ricerca
con condizioni di filtro o specifiche regole di Business).
• Per implementare una collezione di Value-Type efficiente: Le collezioni
fornite dal .Net Framework possono essere utilizzate per contenere elementi di
qualsiasi tipo (come si vedrà nel prossimo capitolo). A questo scopo il tipo utilizzato per rappresentare un elemento nei metodi delle collezioni è Object, che è
la radice di tutti i tipi dato. Tuttavia Object è anche un Reference-Type (quei
tipi dato che vengono passati come parametro o assegnati per riferimento),
quindi se gli elementi inseriti nella collezione sono Value-Type (quei tipi dato
che se passati come parametro o assegnati vengono copiati per valore) avverranno continue operazioni di Boxing e Unboxing (cioè le conversioni necessarie per
poter trattare i Value-Type come oggetti) con sensibile peggioramento dei tempi di elaborazione. Quindi le collezioni fornite dal .Net Framework potrebbero
non essere indicate.
Vediamo adesso, un esempio di utilizzo di un contenitore nel mondo .NET :
l’HashTable. L’esempio è scritto nel linguaggio C#:
using System; using System.Collections;
namespace EsempioHashTable { public class myClasse { //MAIN static
void Main(string[] args) { //Creazione di un oggetto HashTable
(contenitore) HashTable myHashTable = new HashTable();
//Inserisco delle coppie di chiave e valore
myHashTable.Add("TAG + SelectQuery + TAG", "select * from
nomeTabella"); myHashTable.Add("chiavePrimaria","id");
myHashTable.Add("tipoChiavePrimaria","int");
//Utilizziamo IDictionaryEnumerator che è un’interfaccia //per
ricavare l’elenco di tutte le chiavi //e dei rispettivi valori
dell’hashtable Console.WriteLine("Coppie presenti
nell’HashTable"); IDictionaryEnumerator myEnumerator =
myHashTable.GetEnumerator();
while (myEnumerator.MoveNext()) Console.WriteLine(" {0} : {1}",
myEnumerator.Key, myEnumerator.Value);
//Controlliamo se l’hashtable contiene una certa chiave //con
il metodo ContainKey() Console.WriteLine("Contiene la chiave
42
CAPITOLO 2. ARCHITETTURE SOFTWARE A OGGETTI
’TAG + SelectQuery + TAG’ ?"); Console.WriteLine("
myHashTable.ContainsKey("TAG + SelectQuery + TAG").ToString());
//Controlliamo se l’hashtable contiene una certa chiave //con
il metodo ContainValue() Console.WriteLine("Contiene il valore
’id’ ?");
Console.WriteLine("myHashTable.ContainsValue("id").ToString());
//Stampo il numero totale di coppie presenti nell’hashtable
Console.WriteLine("Elementi totali: " +
myHashTable.Count.ToString());
//Rimuovo una coppia di chiave ed elemento tramite //il nome
della chiave Console.WriteLine("Rimuovo la chiave
’tipoChiavePrimaria’ ...");
myHashTable.Remove("tipoChiavePrimaria");
//Ristampo il numero totale di coppie per verificare l’avvenuta
//rimozione Console.WriteLine("Elementi totali : " +
myHashTable.Count.ToString());
}//end metodo main }//end classe }//end namespace
Schematizzando ogni contenitore offre alcuni servizi generici, comuni a tutti i
contenitori (vedi figura 2.7.).
Un concetto importantissimo quando parliamo di contenitori sono gli iteratori.
Gli iteratori derivano dai design pattern. Il pattern Iterator risolve diversi problemi connessi all’accesso e alla navigazione attraverso gli elementi, in particolare, di
una struttura dati contenitrice, senza esporre i dettagli dell’implementazione e della
struttura interna del contenitore. L’oggetto principale su cui si basa questo design
pattern è l’ iteratore.
Una classe contenitrice dovrebbe consentire l’accesso e la navigazione attraverso
l’insieme degli elementi che contiene. Nella programmazione a oggetti, un’alternativa
semplice e preferibile all’uso di indici (come accade ad esempio per gli array) consiste
nell’aggiungere operazioni all’interfaccia del contenitore. Questa soluzione ha il grosso
vantaggio che, se l’interfaccia è ben definita, consente di annullare la dipendenza da
dettagli interni del contenitore, ma ciò presenta alcuni inconvenienti:
• Sovraccarico dell’interfaccia del contenitore: Le operazioni aggiunte sovraccaricano l’interfaccia preesistente della classe contenitore;
• Mancanza di punti di accesso multipli: Le operazioni sono centralizzate
nella classe contenitore. Questo non consente di effettuare contemporaneamente più visite indipendenti agli elementi dello stesso contenitore.
• Supporto carente per metodi di navigazione speciali: Quando i contenitori possiedono una struttura complessa, non di rado vi sono diversi e ugual-
2.6. CLASSI ED OGGETTI CONTENITORI
43
Figura 2.15: Un contenitore in astratto
mente utili modi di attraversarne l’insieme degli elementi contenuti. Un’interfaccia centralizzata si adatta male a questa situazione, perché richiede l’aggiunta di più operazioni specializzate, esacerbando il problema del sovraccarico.
Abbiamo detto che l’elemento principale del pattern Iterator è l’iteratore. Esso
fornisce un metodo generale per accedere in successione a ciascun elemento di uno
qualsiasi dei tipi di contenitore (sequenziali o associativi). Un esempio chiarificatore di
classificazione è la suddivisione degli iteratori nella STL (Standard Template Library)
del linguaggio di programmazione C++.
Possiamo classificare gli iteratori in 5 categorie [21]:
• Input Iterator: Legge gli elementi di un contenitore ma non supporta la
scrittura;
• Output Iterator: Scrive gli elementi di un contenitore ma non supporta la
lettura;
• Forward Iterator: Usato per leggere da e scrivere in un contenitore in una
sola direzione;
• Bidirectional Iterator: Usato per leggere da e scrivere in un contenitore in
entrambe le direzioni;
• Random Access Iterator: Fornisce accesso a ogni posizione nel contenitore
con un costo costante in termini di tempo;
44
CAPITOLO 2. ARCHITETTURE SOFTWARE A OGGETTI
2.7
Le soluzioni informatiche dato-centriche
Abbiamo parlato nell’introduzione di sistemi informativi, precisamente dell’importanza del dato come tesoro di ogni azienda. È meglio soffermarci per un momento
sulla struttura di organizzazione di un azienda.
Possiamo vedere l’organizzazione di un azienda in due modi:
1. Strutturata per funzioni;
2. Strutturata per processi;
Storicamente l’aziende erano strutturate per funzioni. Le funzioni sono aggregazioni
di uomini e mezzi necessari per lo svolgimento di attività della stessa natura. Quindi
in un azienda che è organizzata per funzioni le attività simili, che assolvono cioè la
stessa funzione, che richiedono le stesse competenze e che utilizzano lo stesso tipo di
risorse e di tecnologie, vengono raggrupate in un’unità organizzativa sotto un’unica
responsabilità. [6].
Queste aziende che adottavano questo modo di struttura, erano meno interessate
ad utilizzare le tecnologie informatiche come supporto alle attività produttive; questo
perchè ogni reparto procede all’inserimento semplice delle tecnologie IT entro funzioni
di lavoro per proprio conto e le applicazioni specialistiche non sempre prevedono una
facile integrazione tra di loro, creando il cosiddetto problema delle isole informatiche,
cioè sistemi informatici diversi in ogni reparto, che usano formati di rappresentazione
dei dati diversi, costringendo i responsabili IT alla creazione di apposite interfacce di
comunicazione, che effettuino la traduzione dei dati tra i vari formati [22].
Gran parte delle aziende moderne sono strutturate per processi.
Un processo aziendale è un insieme organizzato di attività e decisioni, finalizzato alla creazione di un output effettivamente domandato dal cliente, e al quale
questi attribuisce un valore ben definito. [23].
E bene tenere presente due concetti fondamentali quando trattiamo questi argomenti. Il primo concetto è la Catena del valore di Porter in [24] e l’altro concetto
è la Piramide di Anthony in [25].
Nella figura 2.16. viene mostrato il primo concetto nel caso di un azienda di
produzione.
In questa figura si può notare che i processi sono suddivisi in:
• Buy side: processi il cui input proviene dai fornitori;
• Inside: processi aventi sia input sia output interni all’azienda, che possono essere suddivisi tra processi primari, che sono direttamente legati alla produzione
del valore del core business dell’azienda e processi secondari, che non generano
direttamente un valore, ma producono quei servizi senza i quali l’organizzazione
non potrebbe operare;
• Sell side: processi il cui output è rivolto direttamente ai clienti esterni all’azienda;
Invece nella figura 2.17. viene mostrata la piramide di Anthony.
Nella figura i processi sono suddivisi:
2.7. LE SOLUZIONI INFORMATICHE DATO-CENTRICHE
45
Figura 2.16: La catena del valore di Porter nel caso di un azienda di produzione
Figura 2.17: La piramide di Anthony
46
CAPITOLO 2. ARCHITETTURE SOFTWARE A OGGETTI
• Processi direzionali: Sono anche chiamati strategici. Essi concorrono alla
definizione degli obiettivi strategici;
• Processi gestionali: Sono anche chiamati manageriali. Essi traducono gli
obiettivi strategici in obiettivi economici e ne controllano il raggiungimento;
• Processi operativi: Concorrono alla attuazione degli obiettivi;
I sistemi informatici collegati ai processi devono essere flessibili e riprogrammabili
rapidamente seguendo l’evoluzione dei processi business. In queste situazioni, è bene
disporre di una base di dati unica, centralizzata e condivisa dalle varie aree funzionali
cosicchè i dati siano resi disponibili a tutti. Il dato viene messo al centro di tutto.
Parliamo di struttura database-centrica.
Parlare di database come strumento informatico unico per la gestione delle informazioni è molto riduttivo, in quanto esistono diversi sistemi applicativi adatti alla
gestione di ogni tipo di informazione. Tali sistemi si possono classificare come [6]:
• Sistemi a livello operativo: È il componente più presente del sistema informatico. Supportano la registrazione delle attività elementari e delle transizioni che si svolgono nell’azienda (es. depositi, paghe,ecc.). Il loro scopo
principale è quindi supportare le attività routinarie e registrare il flusso delle
transizioni entro l’azienda, al livello operativo. Il loro componente fondamentale sono i TPS(Transaction Processing Systems). Questi vengono chiamati
anche OLTP(Online Transaction Processing), che svolgono e registrano le transizioni di routine necessarie per le attività quotidiane dell’azienda (es. Calcolo
stipendi, registrazione ordini, ecc.);
• Sistemi di gestione della conoscenza: Qui troviamo due componenti: i sistemi per l’ufficio che aumentano la produttività dei lavoratori (es. OpenOffice, MS Power Point, ecc.) e i KWS meglio conosciuti come Knowledge Working Systems, che supportano i lavoratori della conoscenza nella creazione di
nuova conoscenza;
• Sistemi di supporto dell’attività manageriale: Questi sistemi favoriscono
le attività di controllo e monitoraggio e le attività decisionali e amministrative.
Forniscono report periodici e sono composti dai MIS(Management Information
Systems), che servono principalmente le funzioni di pianificazione e controllo,
con supporto alle decisioni manageriali. In mezzo al livello strategico e quello
menageriale troviamo i DSS(Decision Support Systems);
• Sistemi di supporto delle attività strategiche: Questi sistemi aiutano i
senior manager ad affrontare i problemi strategici e a valutare le tendenze a
lungo termine. Sono formati dagli ESS(Executive-Support Systems);
Nella figura 2.18. sono rappresentate le relazioni ed i flussi informativi che
intercorrono fra i sistemi che ho elencato sopra.
Per arrivare a capire come è fatta la struttura database-centrica nelle grosse
aziende che sono strutturate secondo una visione a processi, è meglio comprendere i
concetti di OLAP e Datawarehouse.
A seconda del sistema dove ci troviamo, la struttura interna della base di dati
cambia. Le basi di dati per le attività quotidiana di OLTP sono caratterizzate da:
2.7. LE SOLUZIONI INFORMATICHE DATO-CENTRICHE
47
Figura 2.18: Relazioni e flussi informativi che intercorrono fra i vari
componenti dello strato applicativo del sistema informatico (fonte in [26]).
1. Normalizzazione completa delle tabelle (per normalizzazione si intende in questo
contesto il processo volto all’eliminazione della ridondanza e del rischio di
inconsistenza del database);
2. Dati memorizzati al minimo livello di granularità;
3. Ottimizzazione per l’inserimento dei dati e lettura di un piccolo numero di
record alla volta;
4. Frequente uso di interrogazioni che richiedono join (operatore dell’algebra relazionale che unisce due tuple in relazione tra loro) di molte tabelle;
5. La struttura dei dati non varia di frequente;
6. Alto numero di tabelle e di associazioni;
La fase estrazione dei dati dalla base dati di produzione, di trasformazione dei
dati nella rappresentazione più adatta all’analisi da effettuare e di caricamento dei
dati nel programma di analisi viene detta ETL( Extract Trasform an Load), ed è
effettuata ogni qual volta si debbano eseguire analisi strategiche.
Il processo di analisi completa dei dati prende il nome di OLAP. Esso viene
eseguito utilizzando un database che ha le seguenti caratteristiche:
1. Le entità sono denormalizzate;
2. I dati memorizzati possono essere aggregati o riassuntivi;
3. Le interrogazioni richiedono pochi join;
4. Lo schema del database è semplice (con meno tabelle e meno relazioni) per una
comprensione più facile da parte dell’utente;
5. È ottimizzato per la consultazione di grandi moli di dati e solitamente è in sola
lettura
48
CAPITOLO 2. ARCHITETTURE SOFTWARE A OGGETTI
Figura 2.19: Struttura database-centrica
Esistono dei modelli di database generici pensati per queste esigenze come lo Star
Schema e il Snowflake Schema vedi in [1].
Nel contesto aziendale, in cui vi è un unico database centrale dove vengono memorizzate le informazioni, i client, che condividono i dati, di solito inviano delle richieste
che sono per lo più messaggi in formato SQL, al server. Il server elabora questi messaggi e produce come risultato un set di dati che spedisce come risposta ai client,
oppure, nei casi di istruzioni di modifica dei dati (inserimento, aggiornamento, cancellazione) il server effettua la modifica dei dati, notificando poi al client l’azione
svolta.
Il database centrale definisce le diverse entità in maniera univoca e omogenea per
tutti i client al quale fanno riferimento. Tale database garantisce la mancanza di
ridondanza tra i dati e l’integrità del sistema.
Un data warehouse rappresenta il magazzino di dati a livello di impresa, ossia un
insieme di strumenti per convertire un vasto insieme di dati in informazioni utilizzabili
dall’utente, con la possibilità di accedere a tutti i dati dell’impresa centralizzati in un
solo database che garantisce coerenza e consolidamento dei dati e velocità di accesso
alle informazioni e supporto all’analisi dei dati. Il data warehouse a volte può essere
segmentato per questioni di praticità o per dividere i dati in base ai dipartimenti
aziendali; si parla allora di data mart (l’insieme di data mart di tutti i dipartimenti
aziendali forma il data warehouse).
Nella figura 2.19. vediamo come è fatta una struttura database-centrica.
Da questo contesto si evincè l’importanza che copre una soluzione databasecentrica. Quindi l’idea è quella di mettere al centro del proprio lavoro il database
come principe della persistenza dei dati. Infatti ancor’oggi troviamo database vecchissimi ma ancora utilizzati, nonostante i cambiamenti del modo di programmare
ecc.
2.8. GLI ORM ED IL LORO RUOLO
2.8
49
Gli ORM ed il loro ruolo
Arriviamo finalmente a parlare di una tecnica di programmazione per convertire dati
fra RDBMS e linguaggi di programmazione orientati agli oggetti, gli ORM (ObjectRelational Mapping).
Il ruolo che svolge un ORM è quello di associare a ogni operazione e elemento usato
nella gestione del database degli oggetti con adeguate proprietà e metodi, astraendo
l’utilizzo del database dal DBMS specifico.
Prima di approfondire l’argomento ORM e bene sottolineare i principali vantaggi
che porta usando questa tecnica di programmazione.
Il primo vantaggio è il superamento dell’impedance mismatch.
Il secondo vantaggio è l’elevata portabilità rispetto alla tecnologia DBMS utilizzata; i.e se noi cambiassimo DBMS (es. da Postgres vado a usare Oracle o meglio
SqlServer), non ci dobbiamo preoccupare a riscrivere tutte le routine che implementino lo strato di persistenza. Come vedremo nei successivi capitoli, il cuore centrale
che fa funzionare il tutto è il mapper. Grazie a lui sappiamo come sono relazionati
le classi entità (i cosidetti POJO che abbiamo visto prima) e le tabelle o meglio lo
schema del mondo dei RDBMS.
Un altro grossisimo vantaggio è la facilità d’uso. Come vedremo nel seguito
non si è tenuti a scrivere query SQL. Le librerie ORM fra i quali cito Nhibernate,
che vedremo, hanno il supporto al SQL, ma vengono utilizzate solo per interrogazioni
complesse e quindi che richiedono un elevata efficienza.
Nhibernate che come vedremo nel prossimo capitolo è una libreria ORM. Esso
offre tante funzionalità:
1. Caricamento automatico dei grafi degli oggetti;
2. Gestione della concorrenza nell’accesso ai dati;
3. Meccanismo di caching dei dati (aumento delle prestazioni e riduzione del carico
di lavoro nei confronti del RDBMS;
La necessità di un ORM nasce dall’intrinseca differenza tra il modello relazionale
è quello ad oggetti; quest’ultimo infatti ha concetti come ereditarietà, polimorfismo,
relazioni bidirezionali ed altre che non hanno una controparte nel mondo relazionale
dei database. Per questa ragione, se si ha la necessità di gestire la persistenza su
database relazionali, è consigliabile appoggiarsi ad una libreria che si occupi di gestire
nella maniera più trasparente possibile le trasformazioni necessarie tra questi due
mondi.
Gli ORM in generale sono indispensabili quando l’architettura della propria applicazione è fortemente basata sul Domain Model e quindi si modella la logica di
business con tutti i paradigmi dell’Object Orientation. Questo processo è il più adatto per un ORM, si parte infatti dal modello ad oggetti ed in base ad esso si crea una
struttura di Database dedicata per gestirne la persistenza. Il processo inverso, partire
da uno schema di database preesistente e da questo arrivare al modello ad oggetti
e meno ideale, ma anche in questo caso un ORM mostra la sua potenza, dato che
permette di evitare la dicotomia un oggetto una tabella, che chiaramente finisce per
creare un insieme di oggetti strutturati secondo il modello relazionale, andando cosi
a perdere la flessibilità di una struttura pienamente OO.
50
CAPITOLO 2. ARCHITETTURE SOFTWARE A OGGETTI
Figura 2.20: Un esempio di utilizzo del Data Mapper
Come abbiamo visto all’inizio del capitolo, i design pattern sono utilizzatissimi
nella progettazione del software in sistemi complessi.
Un ORM implementa il pattern Data Mapper.
2.9
Implementazione di un ORM : Il pattern Data
Mapper
Nel paragrafo precedente si è visto che cos’è un ORM e che ruolo investe nella progettazione e realizzazione di software di medie-grandi dimensioni. In questo paragrafo
viene invece descritta l’implementazione di un ORM mediante un pattern chiamato
Data Mapper. Esso si colloca nel terzo livello di un architettura a 3-Tier.
Il Data Mapper è un livello di software che sepera gli oggetti che risiedono in
memoria dal databse. Esso è responsabili nel trasferire i dati tra i due mondi e ha
anche la capicità di isolare i dati da ogni altro. Con questo pattern gli oggetti in
memoria non hanno bisogno di conoscere il database sottostante. Questi oggetti non
hanno bisogno di interfacciarsi con codice SQL e certamente non conoscono nulla
dello schema del database. Lo schema del database è sempre ignorato dagli oggetti
che la utilizzano.
Nella figura 2.20. viene fatto un esempio di utilizzo del pattern Data Mapper.
Come si può notare nel mondo Object-Oriented c’è la classe Persona e si può
supporre di avere la tabella Persona nel database relazionale. In mezzo si noti che
esiste una classe chiamata Persona Mapper che offre le seguenti tre funzionalità:
Inserimento, Cancellazione e Modifica.
Come si può notare il cuore dei dati è il mapper. Nella realizzazione del progetto
sviluppato durante la tesi, è stati scritto un generatore che origina in maniera automatica il file mapper per collegare il mondo relazionale con il mondo Object-Oriented.
Nel capitolo 4 verranno presentati tutti i passi di tale realizzazione.
Capitolo 3
Soluzioni nel mondo .NET: Nhibernate
In questo capitolo vengono introdotte le soluzioni che offre il mondo .NET sul tema
degli ORM. Dopo una breve spiegazione del mondo .NET si introduranno strutture
dati proprie di questo mondo (DataTable e ArrayList) e si introdurrà una libreria open
source per la gestione di un ORM: Nhibernate. Infine si fornirà qualche scenario di
applicazione di questa libreria.
3.1
Il mondo .NET
Chiediamoci come mai uno sviluppatore dovrebbe scegliere il mondo .NET. Il mondo
.NET offre le seguenti cose [27]:
1. Runtime comune per il software (una specia di macchina virtuale);
2. Stessi oggetti indipendentemente dal linguaggio e dall’ambiente di sviluppo
(una specie di libreria di classi);
3. Indipendenza dal linguaggio (una specie di bytecode);
4. Linguaggio OOP flessibile e umano(una specie di Java);
Microsoft ha sviluppato .NET come contrapposizione proprietaria al linguaggio
Java (che è open source) e attribuisce un ruolo strategico al lancio di .NET come
piattaforma di sviluppo per applicazioni desktop e server nel prossimo decennio per
le architetture client/server, internet ed intranet. Rispetto a Java, .Net (e il suo
linguaggio principe, cioè C#) sono standard ISO riconosciuti e quindi non è possibile,
da parte della casa madre, modificarne la sintassi (a meno di discostarsi dal suo stesso
standard).
Quindi possiamo definire .NET nel seguente modo:
.NET non è un linguaggio (è Runtime e una libreria) per eseguire e scrivere
programmi scritti in ogni linguaggio compatibile (es. C#, J#, Vb.net). Quindi
.NET è un nuovo framework per lo sviluppo di applicazioni web-based e windows
all’interno di un ambiente Microsoft.
Il framework offre un fondamentale spostamento verso la strategia Microsoft,
quindi muove lo sviluppo di applicazioni client-centric ad una server-centric.
Nelle seguenti due figure (figura 3.1 e figura 3.2) mostrano rispettivamente .NET
e il framework e linguaggi associati a .NET.
Bisogna però confrontare il mondo .NET con un altra tecnologia di software a
componenti su cui Microsoft ci aveva puntato un sacco il COM(Component Object
Model). Questa tecnologia poi si evolse in COM+, detto anche MTS. Per consentire
51
52
CAPITOLO 3. SOLUZIONI NEL MONDO .NET: NHIBERNATE
Figura 3.1: .NET
Figura 3.2: Framework e linguaggi
una migrazione graduale verso .NET dei progetti esistenti, .NET è stato progettato
per interagire con oggetti COM, facendo da wrapper (involucro), cioè da strato esterno
che accede alle funzione dello strato interno.
La Common Language Runtime (CLR), il Common Intermediate Language (CIL)
ed il .NET (C#) sono simili rispettivamente alla Java Virtual Machine, al bytecode
e al linguaggio Java della Sun Microsystems, con cui sono in forte concorrenza. Entrambi utilizzano un proprio bytecode intermedio. Il bytecode di .NET è progettato
per essere compilato al momento dell’esecuzione (just in time compilation detta anche
JITtin come il bytecode di Java, che invece inizialmente era interpretato (ovvero non
compilato al momento). A momento .NET è compatibile soltanto con le piattaforme
Windows, mentre Java è disponibile per tutte le piattaforme. La Java EE (Java Platform, Enterprise Edition) di Sun fornisce funzionalità leggermente superiori ad altre
tecnologie Microsoft, come COM+ e MSMQ, che lavorano peraltro in modo integrato
con i sistemi operativi Windows. .NET fa un uso estensivo ed astratto di tutte queste
tecnologie ormai consolidate. Si deve altresı̀ rilevare che .NET offre vantaggi di tipo
prestazionale delle applicazioni quando sono in esecuzione nonché costi e tempi di
3.1. IL MONDO .NET
53
Figura 3.3: La compilazione
sviluppo applicativo inferiori rispetto alla piattaforma Java EE. Inoltre il progetto
Mono sta portando alla piena portabilità di .Net su sistemi operativi non windows.
Già attualmente un applicativo compilato con il .Net framework può funzionare sotto
altri sistemi (per esempio Linux) con l’installazione del framework Mono.
La CLR funziona come una virtual machine eseguendo tutti i linguaggi compatibili. Tutti i linguaggi del mondo .NET devono obbedire alle regole e gli standard
imposti dalla CLR. Per esempio, gestione degli errori, dichiarazione, creazione ed uso
di oggetti ecc.
La Common Type Systems CTS è una ricca collezione di tipi dati all’interno di
CLR. Esso implementa vari tipi (double, int, ecc.) e le operazioni su di esse.
La Common Language Specification CLS è un set di specifiche che il linguaggio
e le librerie hanno bisogno. Questo assicura la interoperabilità tra i linguaggi.
Nella figura seguente viene mostra come avviene la compilazione in .NET (vedi
figura 3.3).
I linguaggi .NET non sono compilati in codice macchina ma sono compilati in un
linguaggio intermedio chiamato IL(Intermediate Language).
Il CLR accetta il codice IL e lo ricompila in codice macchina. La ricompilazione
è Just-in-time, JIT. Questo codice resta in memoria per le seguenti chiamate. Nei
casi non c’è abbastanza memoria il codice JIT viene scartato e il codice Il viene
interpretato.
Nella produzione del software, il framework è una struttura di supporto su cui
un software può essere organizzato e progettato.
Un framework è definito da un insieme di classi astratte e dalle relazioni tra
di esse. Istanziare un framework significa fornire un’implementazione delle classi
54
CAPITOLO 3. SOLUZIONI NEL MONDO .NET: NHIBERNATE
Figura 3.4: Framework .NET
astratte. L’insieme delle classi concrete, definite ereditando il framework, eredita le
relazioni tra classi; si ottiene in questo modo un insieme di classi concrete con un
insieme di relazioni tra classi.
La vera utilità di un framework è quello di rispiarmare allo sviluppatore la
riscrittura di codice già steso in precedenza per compiti simili.
Nella figura seguente viene mostrato il framework .NET (figura 3.4).
3.2
Le DataTable e le loro caratteristiche
La tecnologia per accedere ad una sorgente di basi di dati è ADO.net.
Essa è definita come:
Un insieme di librerie di classi che consentono l’accesso non solo ai Database, ma
a diversi tipi di sorgenti di dati.
Distinguiamo tra ambiente connesso e ambiente disconesso. Il primo significa che i programmi applicativi sono costantemente in contatto con la base di dati. Lo
svantaggio è la non tanto scalabilità. Gli elementi infrastrutturali di questa modalità
sono:
1. Connection;
2. Command;
3. DataAdapter;
4. DataReader;
3.2. LE DATATABLE E LE LORO CARATTERISTICHE
55
Figura 3.5: DataSet
Il secondo invece significa che in un sottoinsieme di dati può essere estratto (copiato) da una base di dati, e riemesso nella base di dati stessa. Il vantaggio rispetto
alla modalità connessa è la maggior scalabilità.
Gli elementi che manipolano i dati di questa modalità sono:
1. DataSet;
2. DataTable;
3. DataRow;
4. DataColumn;
5. Relation;
Parleremo in dettaglio della modalità disconessa.
Il DataSet è stato progettato come un contenitore di dati. Esso consiste in un
insieme di DataTable, ognuna dei quali avranno un insieme di data columns e di data
rows (vedi figura 3.5).
La DataTable è molto simile ad una tabella di un database, i.e. rappresenta
una tabella di dati relazionali in memoria. Tali dati sono locali rispetto all’applicazione basata su .NET in cui risiedono, ma possono provenire da un’origine dati
quale Microsoft SQL Server tramite DataAdapter. La DataTable consiste come abbiamo detto, di un insieme di colonne con particolari proprietà è hanno zero o più righe
di dati. Una data table deve anche definire una chiave primaria, una o più colonne e
devono anche contenere dei vincoli sulle colonne (vedi figura 3.6).
Lo schema o struttura di una tabella è rappresentato da colonne e vincoli. Per
definire lo schema di una DataTable, è possibile utilizzare gli oggetti DataColumn o
gli oggetti ForeignKeyConstraint e UniqueConstraint. Le colonne di una tabella
possono essere associate a colonne di un’origine dati, contenere valori calcolati da
espressioni, incrementare automaticamente i propri valori o contenere valori di chiavi
primarie.
Oltre a uno schema, è necessario che DataTable disponga anche di righe per
contenere e ordinare i dati. La classe DataRow rappresenta i dati effettivi contenuti
56
CAPITOLO 3. SOLUZIONI NEL MONDO .NET: NHIBERNATE
Figura 3.6: DataTable
Figura 3.7: Costruttori della classe DataTable
in una tabella. La classe DataRow e i relativi metodi e proprietà consentono di
recuperare, valutare e modificare i dati di una tabella. Quando si accede ai dati di
una riga e li si modifica, l’oggetto DataRow conserva sia lo stato corrente che lo stato
originale.
L’utilizzo di una o più colonne correlate delle tabelle consente di creare relazioni
padre-figlio tra tabelle. È possibile creare una relazione tra oggetti DataTable tramite
un tipo DataRelation. Gli oggetti DataRelation possono quindi essere utilizzati per
restituire le righe padre o figlio correlate di una particolare riga.
La figura 3.7 mostra i costruttori della classe DataTable nel framework .Net.
La figura 3.8 mostra le proprietà della classe DataTable.
La figura 3.9 mostra alcuni metodi che fornisce la classe DataTable.
3.3
Rappresentazioni in memoria: DataTable vs
ArrayList di oggetti entità
Prima di vedere i pro e contro delle DataTable rispetto agli ArrayList, è bene vedere
cosa sono gli ArrayList e come vengono rappresentati.
Gli ArrayList implementano una lista di dimensione dinamica che può contenere
3.3. RAPPRESENTAZIONI IN MEMORIA: DATATABLE VS
ARRAYLIST DI OGGETTI ENTITÀ
57
Figura 3.8: Proprietà della classe DataTable
qualsiasi tipo di oggetto. 1 Il numero di elementi che possono essere contenuti nella
lista è detto capacità. Il numero di elementi presenti effettivamente nella lista può
essere inferiore alla capacità e si ottiene utilizzando la proprietà Count.
Per aggiungere elementi alla lista si utilizza il metodo Add(). Qui di seguito
riporto un esempio nel frammento C#:
ArrayList myList = new ArrayList();
myList.Add("Uno");
myList.Add("Due");
myList.Add("Tre");
myList.Count; //vale 3
È da notare che le strutture dati come ArrayList non sono tipizzate, quindi è
necessario eseguire una conversione di tipo quando si estrae un elemento dalla stessa.
Un altra cosa importante è che queste liste dinamiche possono anche contenere valori
1
questi sono simili ai vector della STL nel linguaggio C++
58
CAPITOLO 3. SOLUZIONI NEL MONDO .NET: NHIBERNATE
Figura 3.9: Alcuni metodi della classe DataTable
3.4. INTRODUZIONE A NHIBERNATE
59
htbp
Metodo
Add()
AddRange()
Clear()
Contains()
GetEnumerator()
Insert()
Remove()
Descrizione
Aggiunge un elemento alla lista
Aggiunge gli elementi di una collezione alla fine della lista
Rimuove tutti gli elementi della lista
Determina se un elemento è presente o meno nella lista
Ritorna un enumeratore (iteratore) che può essere utilizzato per scorrere la lista
Inserisce un oggetto nella lista in una posizione particolare
Rimuove uno specifico oggetto della lista
Tabella 3.1: Elenco di alcuni metodi della classe ArrayList
nulli.
Nella tabella 3.1 che segue vengono riportati alcuni metodi della classe ArrayList.
3.4
Introduzione a Nhibernate
Nhibernate è una soluzione Object-relational Mapping (ORM) per il linguaggio di
programmazione C#. È un software free, open source, e distribuito sotto licenza
LGPL. Nhibernate fornisce un framework semplice da usare, che mappa un modello
di dominio orientato agli oggetti in un classico database relazionale. È importante
notare che Nhibernate non si occupa solo di mappare dalle classi C# in tabelle del
database (e da tipi .NET ai tipi SQL), ma fornisce anche degli aiuti per le query di
dati, il recupero di informazioni e riduce significativamente il tempo che altrimenti
sarebbe speso lavorando manualmente in SQL e con OLEDB. Se noi ci ponessimo
questa semplice domanda: Ma dobbiamo usarlo sempre Nhibernate?. Ovviamente
no. Vanno valutate una serie di considerazioni quali:
1. Possibilità di cambiare DBMS;
2. L’uso API standard;
3. Aspetti specifici dei DBMS;
Per lavorare con Nhibernate abbiamo bisogno di:
• Entità o POCO: La classe che mappa le entità in oggetti;
• Mapping: Il file di mapping che esprime la corrispondenza tra oggetti e le
entità;
• File di configurazione: File di configurazione specifico per Nhibernate;
Il primo di dei file richiesti non è altro che un POCO (lo abbiamo visto nel capitolo
precedente). È conveniente specificare:
60
CAPITOLO 3. SOLUZIONI NEL MONDO .NET: NHIBERNATE
• un campo id: rappresenta il valore del campo id corrispondente nel database
quando l’oggetto viene caricato;
• un costruttore di default: Nhibernate lo sfrutterà tramite reflection (vedremo) per caricare le entità del database;
Il secondo file, come detto, rappresenta il mapping tra Entità ed Oggetti e verrà
trattato ed affrontato in seguito (scritto in XML).
Il terzo file serve a configurare Nhibernate. Esso permette di definire quale DBMS
si sta usando, quale dialetto (per sfruttare a fondo le capacità dei diversi DBMS),
l’indirizzo IP del server e la porta, il nome utente e la password ed infine permette di
specificare quali file devono essere usati per il mapping (possono essere più d’uno).
Viene riportato qui sotto un esempio di file di configurazione relativo al DBMS
MySQL.
<?xml version=’1.0’ encoding=’utf-8’?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="connection.driver_class">
com.mysql.jdbc.Driver
</property>
<property name="connection.url">
jdbc:mysql://IP:PORTA/DB
</property>
<property name="connection.username">USER</property>
<property name="connection.password">PASSWORD</property>
<!-- JDBC connection pool (use the built-in) -->
<property name="connection.pool_size">2</property>
<!-- SQL dialect -->
<property name="dialect">
org.hibernate.dialect.MySQLInnoDBDialect
</property>
<!-- Enable Hibernate’s automatic session context management -->
<property name="current_session_context_class">
thread
</property>
<!-- Disable the second-level cache -->
<property name="cache.provider_class">
org.hibernate.cache.NoCacheProvider
</property>
<!-- Echo all executed SQL to stdout -->
<property name="show_sql">false</property>
<mapping resource="it/demo/dominio/Dipartimento.hbm.xml"/>
</session-factory>
</hibernate-configuration>
3.4. INTRODUZIONE A NHIBERNATE
61
Figura 3.10: Un documento XML rappresentante una canzone ed una sua
visione grafica ad albero
Prima di vedere il funzionamento di Nhibernate, parliamo del linguaggio XML.
XML il futuro
Abbiamo visto nell’introduzione il problema della rappresentazione elettronica dell’informazione. Questa è la parte più complessa e critica per ogni organizzazione
aziendale. Per ovviare a questi problemi sono state proposte diverse soluzioni, ma
negli ultimi anni sta prendendo piede il metalinguaggio XML(eXtended Markup
Language).
Esso è un metalinguaggio, ovvero un linguaggio per definire altri linguaggi, basato
sui tag, ossia particolari parola chiave racchiuse fra i caratteri ASCII ’¡’ e ’¿’, con
rilevanza semantica. L’XML consente di definire una struttura avente contenuto
semantico implicito entro i documenti.
XML è un file di testo (in formato ASCII o Unicode) dove, accanto alle informazioni, sono presenti anche metainformazioni (tag) che ne definiscono un significato.
Normalmente distinguiamo due categorie di documenti:
1. Documenti di definizione, definiscono il formato di un documento XML
vero e proprio, formati dai DTD.
2. Documenti veri e propri con un contenuto di informazione che, non avendolo
di solito incluso, hanno il riferimento, di solito in forma di indirizzo Web, dello
schema o del DTD che li definisce.
Faccio notare che l’XML non rappresenta la rappresentazione visiva di un documento, ma bensı̀ solo il suo contenuto semantico.
Un esempio riguardante una canzone in figura 3.10.
Vediamo come si utilizza Nhibernate in un progetto scritto nel linguaggio C#,
precisamente usando un IDE (Visual Studio 2008).
Nhibernate deve referenziare quattro assembly:
62
CAPITOLO 3. SOLUZIONI NEL MONDO .NET: NHIBERNATE
1. Castle.DynamicProxy
2. Iesi.Collections
3. log4net
4. Nhibernate
È anche possibile decorare gli oggetti con attributi specifici di NHibernate, invece
di usare file XML esterni, ma questo secondo approccio è meno adottato per il principio di persistence ignorance, ovvero gli oggetti non debbono contenere nulla correlato
alla persistenza.
Per prima cosa vediamo come è fatto un file di mapping scritto in XML.
Esempio: Classe Impiegato (POCO).
<?xml version="1.0" encoding="utf-8" ?>
<nhibernate-mapping xlmns = "urn:nhibernate-mapping-2.2"
assembly = "..."
namespace = "...">
<class name="Impiegato" table="impiegato" lazy="false">
<id name="id" unsaved-value=0 access="field" type="System.Int32">
<generator class="native" />
</id>
<property name="Nome" column="nome" type="System.String" />
<property name="Cognome" column="cognome" type="System.String" />
</class>
</nhibernate-mapping>
Nel tag radice viene indicato l’assembly dove si trova la classe mappata e il namespace di appartenenza. L’elemento class dichiara la classe mappata e grazie all’attributo table, si può specificare il nome della tabella del database su cui verrà salvato
l’oggetto. A questo punto è necessario specificare uno ad uno tutti i campi o proprietà
della classe che si vuole salvare nel DB. NHibernate analizza la classe tramite reflection ed è quindi in grado di accedere anche ai campi privati, questa caratteristica, che
ad un primo esame sembra violare il principio di incapsulamento, è invece veramente
utile, ad esempio per il campo id. Quest’ultimo infatti è molto particolare e lo si
vede anche dal fatto che nel mapping viene identificato come id, ad indicare che è il
campo dell’oggetto che ne defisce l’identità. Nel mapping viene poi indicato che il
membro mappato è un campo e non una proprietà (access=Field mentre per default
si ha access=property), ne viene indicato il tipo e si avverte Nhibernate che per gli
oggetti mai salvati nel DB il valore del campo è zero (unsaved-value=0). Quest’ultimo attributo è fondamentale perché permette a NHibernate di distinguere tra insert
ed update. Nell’elemento id è necessario inserire un tag generator che specifica
l’algoritmo di generazione dei valori di identità, dato che per esempio in SQL server
viene utilizzata una colonna identity viene specificato come generatore il tipo native,
ovvero è il DB che si occuperà di generare un nuovo valore per ogni oggetto inserito.
3.4. INTRODUZIONE A NHIBERNATE
63
Come abbiamo detto in precedenza, un altro ingrediente fondamentale per utilizzare Nhibernate è il file di configurazione.
Esistono tanti modi per configurare Nhibernate, il più usato è quello di inserire nel
file di configurazione principale del progetto (App.config o Web.config) una sezione
apposita.
<configSections>
<section name="NHibernate" type="
System.Configuration.NameValueSectionHand"
</configSections>
<!--Sql server connection-->
<NHibernate>
<add key="hibernate.connection.driver_class"
value="NHibernate.Driver.SqlClientDriver" />
<add key="hibernate.dialect"
value="NHibernate.Dialect.MsSql2005Dialect" />
<add key="hibernate.connection.provider"
value="NHibernate.Connection.DriverConnectionProvider" />
<add key="hibernate.connection.connection_string"
value="Server=localhost\sql2005; Integrated Security=SSPI;
<add key="hibernate.show_sql" value="true" />
</NHibernate>
Le informazioni minime che si debbono fornire sono: il driver fisico da utilizzare per accedere al DB (SqlClientDriver), il dialetto (che specifica il tipo esatto di
database usato Es. MsSql2005Dialect), il provider (DriverConnectionProvider) ed
infine la stringa di connessione.
È stato aggiunto come si può vedere show.sql2 che permette di visualizzare nella
console tutto il codice SQL Server che viene inviato al database. Questa funzionalità
è particolarmente utile perché permette di visualizzare esattamente le operazioni che
vengono fatte sul DB.
Nella figura 3.11 che segue viene mostrata l’architettura ad alto livello delle API
di Nhibernate [28].
Come si può vedere dalla figura, l’interfaccia di Nhibernate viene classificata nel
seguente modo:
• Interfaccie per operazioni CRUD: Usati per performance di query e operazioni CRUD (Create, Retrieve, Update e Delete). Queste interfaccie sono il
punto principale di dipendenza delle applicazioni della logica business. Troviamo: ISession, ITransaction, IQuery e ICriteria.
• Interfaccie per l’infrastruttura: Per configurare Nhibernate. Troviamo:
Classe Configuration.
2
Attenzione!! Quando si eseguono dei test togliere questo valore aggiuntivo
64
CAPITOLO 3. SOLUZIONI NEL MONDO .NET: NHIBERNATE
Figura 3.11: Architettura ad alto livello delle API di Nhibernate
• Interfaccie per il Callback: Gestione degli eventi. Troviamo: IInterceptor,
ILifeCycle e IValidatable.
• Interfaccie per estensioni mapping: IUserType, ICompositeUserType
e IIdentifierGenerator.
Dopo aver visto com’è composta l’architettura di Nhibernate, il punto da focalizzarci è l’oggetto ISession. Esso è l’oggetto principale con cui si interfaccia a
Nhibernate. Esso corrisponde al cuore del intero processo.
Data l’importanza, di solito si costruisce una classe SessionManager in grado
di gestire la centralizzazione delle sessioni. Il SessionManager si occupa di gestire
la creazione delle sessioni , il cui costruttore statico non fa altro che inizializzare
un’oggetto chiamato SessionFactory.
Configuration cfg = new Configuration();
cfg.AddAssembly(Assembly.GetExecutingAssembly());
factory = cfg.BuildSessionFactory();
Per caricare la configurazione basta creare un oggetto di tipo NHibernate.Cfg.Configuration,
e poi si procede semplicemente aggiungendo gli assembly che contengono i file di
mapping. Se si commettono errori nei mapping, la classe SessionManager genererà
un’eccezione nella chiamata al metodo Configuration.AddAssembly, è in questo pun-
3.4. INTRODUZIONE A NHIBERNATE
65
to infatti che NHibernate esamina le risorse dell’assembly, individua i mapping e li
analizza per creare dinamicamente le classi che gestiranno la persistenza.
Adesso vediamo un esempio di utilizzo delle sessioni.
private static void Inserimento()
{
Impiegato imp = new Impiegato("Simone", "Bianchi");
using (ISession session = NHSessionManager.GetSession() )
{
session.SaveOrUpdate(customer);
session.Flush();
}
}//end metodo Inserimento()
Il primo fatto importante da notare è che l’oggetto ISession di NHibernate è incluso in un blocco using, questo è fondamentale perché la sessione utilizza al suo
interno oggetti come Connessioni e Transazioni, per cui è necessario che tali risorse
vengano rilasciate correttamente quando si termina di utilizzare la sessione stessa,
pena un possibile connection leak. Dimenticare di chiamare il Dispose() significa infatti delegare il rilascio delle risorse al garbage collector e quindi in un tempo
indefinito nel futuro. Il secondo fatto importante è che è stato chiamato il metodo
SaveOrUpdate() che internamente capisce se l’oggetto deve essere salvato o aggiornato. Questa decisione viene infatti presa in base al valore della chiave primaria, che
nel caso di oggetti nuovi è pari a zero (ricordiamo l’attributo unsaved-value), mentre nel caso di oggetti già salvati è pari al valore di identità restituito dal database.
Infine, grazie all’impostazione show sql è possibile vedere nella console il codice SQL
che NHibernate ha generato per inserire l’oggetto.
L’ultima nota riguarda la chiamata al metodo ISession.Flush() che è necessario
invocare per informare la sessione che vogliamo propagare al database tutti gli eventuali cambiamenti degli oggetti. La sessione si comporta come uno stream, ovvero non
propaga immediatamente al database i cambiamenti degli oggetti, ma solo quando lo
reputa necessario. Chiudere o chiamare il Dispose() su una sessione, senza chiamare
il Flush(), non propaga al DB tutte i nostri cambiamenti, per cui si deve fare molta
attenzione.
Un altra cosa interessante, e che se noi volessimo cambiare database lo sforzo è
minimo.
Per cui una delle particolarità più interessanti degli ORM è che essi sono in grado
di accedere a database differenti in maniera praticamente trasparente. Dato che le
query SQL e gli oggetti di accesso al database sono creati dinamicamente dalla libreria
e soprattutto visto che il dominio degli oggetti segue la persistence ignorance, è spesso
possibile cambiare tipologia di database con sforzo veramente minimo. Per esempio
se volessimo modificare l’esempio precedente per accedere ad un database Access,
la prima operazione da fare è creare un database con lo stesso schema utilizzato in
SQL, naturalmente con le differenze del caso. Quello che segue va aggiunto al file di
configurazione principale.
66
CAPITOLO 3. SOLUZIONI NEL MONDO .NET: NHIBERNATE
<NHibernate>
<add key="hibernate.connection.driver_class"
value="NHibernate.JetDriver.JetDriver, NHibernate.JetDriver" />
<add key="hibernate.dialect"
value="NHibernate.JetDriver.JetDialect, NHibernate.JetDriver" />
<add key="hibernate.connection.provider"
value="NHibernate.Connection.DriverConnectionProvider" />
<add key="hibernate.connection.connection_string"
value="Provider=Microsoft.Jet.OLEDB.4.0;
Data Source=Databases\NHSamp
<add key="hibernate.show_sql" value="true" />
</NHibernate>
Dopo aver parlato di come si utilizza Nhibernate, dell’importanza del file di
mapping e del file di configurazioni, vediamo come Nhibernate gestisce le relazioni.
Partiremo sempre da un esempio, per capire il funzionamento. L’esempio che
segue riguarda: Impiegati e ordini. La relazione che intercorre tra i due è la molti a
uno.
<class name="Order" table="Orders" lazy="false">
<id name="id" unsaved-value="0" access="field" type="System.Int32">
<generator class="native" />
</id>
<property name="Date" column="Date" type="System.DateTime"/>
<many-to-one name="Impiegato"
class="Impiegato"
column="ImpiegatoId"
not-found="exception"
not-null="true" />
</class>
La particolarità è che la proprietà Impiegato viene mappata come many-to-one,
ma d’altra parte questa non è una sorpresa, perché la relazione tra Impiegato e Ordini
è di tipo molti a uno in cui l’ordine è nella parte molti. Questa associazione, come
una normale proprietà, possiede un set di attributi che permettono di specificarne il
funzionamento. L’attributo class serve ad indicare a NHibernate il tipo di oggetto
usato nella relazione e tramite column si indica la colonna usata per memorizzare la
foreign-key. NHibernate controlla se la classe usata per la relazione (ovvero Impiegato) ha un id compatibile con il campo del db per vedere se la relazione è possibile. In
questo caso Impiegato ha una chiave di tipo Int32, la colonna ImpiegatoId è intera
per cui il mapping è compatibile con la struttura di database. Gli attributi not-found
e not-null servono invece per specificare rispettivamente come comportarsi nel caso
3.4. INTRODUZIONE A NHIBERNATE
67
Figura 3.12: Ciclo di vita della persistenza in Nhibernate
di una foreign-key orfana (id che non è presente in impiegati) e se possono esistere
ordini orfani con la foreign-key pari a null.
Nella figura 3.12 viene mostrato il ciclo di vita della persistenza in Nhibernate.
Dopo aver spiegato il significato della figura vedremo com’è questo ciclo di vita viene
utilizzato quando si ha che fare con l’utilizzo delle API di Nhibernate.
Un entità che non è mai stata salvata con un oggetto Session prende il nome di
oggetto transiente. Il suo stato non verrà mai propagato al Database e Nhibernate lo
ignora. Quando utilizziamo i metodi Save() o SaveOrUpdate(), l’entità diventa persistente. Tutte le modifiche che vengono fatte alle sue proprietà mappate verranno
automaticamente propagate al database. Quando utilizziamo i metodi Dispose(),
Close(), Evict(), Clear(), l’entità assume lo stato Detached. Esso indica che un
oggetto che è stato precedentemente persistente, ma che ora è scollegato da qualsiasi
sessione attiva. L’oggetto può essere riportato nello stato persistente semplicemente
tramite i metodi Lock(), Update(), SaveOrUpdate().
Adesso creiamo per esempio, un inserimento di un ordine:
using (ISession session = NHSessionManager.GetSession() )
{
Impiegato simone = new Impiegato("Simone", "Bianchi")
Ordine ord = new Ordine(DateTime.Now, simone);
session.Save(ord);
session.Flush();
}
68
CAPITOLO 3. SOLUZIONI NEL MONDO .NET: NHIBERNATE
Nel momento in cui l’oggetto ord deve essere reso persistente grazie al metodo
Save(), Nhibernate prepara la INSERT per la tabella ordini (per il calcolo dell’id
identity autogenerato dal DBMS), e per conoscere il valore del campo ImpiegatoId,
che rappresenta la foreign-key con la tabella Impiegato, esamina l’oggetto impiegato
associato all’ordine. A questo punto si verifica il problema, perché l’oggetto impiegato è ancora in stato transiente, dato che non è stato mai salvato con NHibernate.
Questa situazione presenta due anomalie, in primo luogo NHibernate non conosce
l’id dell ’oggetto impiegato e non può quindi sapere cosa inserire nella colonna della
foreign-key, in secondo luogo non è lecito salvare un entità quando esistono relazioni
con altre entità che sono in stato transiente. Le soluzioni possono essere due, la prima
è chiamare Session.Save() anche sull’oggetto impiegato prima di salvare l’oggetto ordine, effettuandone quindi il passaggio nello stato persistente, la seconda è modificare
il mapping di ordini in questo modo:
...
<many-to-one name="Customer" class="CustomerOne" column="CustomerId"
not-found="exception" not-null="true"
cascade="save-update"/>
L’unico cambiamento è l’attributo cascade, che indica a NHibernate di percorrere
la relazione propagando la persistenza agli oggetti correlati. Questa caratteristica
nel mondo degli ORM e della programmazione con Domain Model si chiama: persistence by reachability, ovvero persistenza per raggiungimento. In pratica le
operazioni che cambiano lo stato di persistenza di un oggetto vengono propagate percorrendo il grafo degli oggetti. In questo modo è possibile salvare un oggetto ed essere
certi che anche tutti gli oggetti correlati vengano salvati in maniera automatica.
Parliamo adesso di strategie di fetching (ovvero strategie che permettono di
rendere ottimizate le operazioni di query).
La modalità LAZY
Se vado a mettere nel file mapping l’attributo lazy = true, indico a Nhibernate che
l’entità può essere usata in modalità lazy (pigra). L’implementazione del lazy avviene
nel seguente modo:
Il termine Lazy indica un’operazione che viene effettuata solo quando strettamente necessaria. Nel caso di lazy load (caricamento pigro), si effettua la query per
recuperare i dati solamente quando si accede ad una proprietà e non prima. Chiaramente NHibernate deve avere un modo per intercettare quando si utilizza per la prima
volta la proprietà di un oggetto e per questa ragione, al momento del caricamento
dell’oggetto Ordini, il sistema non assegna un vero oggetto Impiegati alla sua proprietà impiegato, ma un istanza di una classe creata dinamicamente, che eredita da
Impiegati ed implementa il pattern proxy. Questo pattern significa che:
data una classe X, un suo proxy non è altro che un’istanza di una classe Y che
3.4. INTRODUZIONE A NHIBERNATE
69
eredità da X e che al suo interno mantiene una istanza di X a cui delega tutte le
chiamate fatte dall’esterno. Grazie a questo pattern è possibile associare in maniera
trasparente funzionalità aggiuntive ad un oggetto esistente.
Nhibernate, all’atto del caricamento dell’oggetto Ordini, effettua una SELECT
sulla sola tabella ordini, da questa tabella recupera l’id dell’oggetto impiegato correlato (dalla colonna con la foreign-key), istanza un proxy di Impiegato.
Questa tecnica, è anche conosciuta come Transparent Lazy Load, è una delle
strategie di fetch possibile e probabilmente la più utile. Grazie ad essa si può senza
problemi navigare un grafo di oggetti in maniera completamente naturale, lasciando
a NHIbernate il compito di caricare i dati quando necessario.
La modalità EAGER
Proviamo ad osservare il seguente listato:
IList<Ordini> ordini =
session.CreateQuery("from Ordini").List<Ordini>();
foreach(Ordini o in ordini)
Console.WriteLine("Ordini Id {0} nome impiegato {1}",
o.Id, o.Impiegato.Nome
In questo esempio abbiamo usato una query HQL, uno dei metodi che offre
NHibernate per effettuare query sul Domain Model. La convenienza di usare HQL è
che ha una sintassi molto simile a SQL, ma si usano i nomi di classi e proprietà, in
questo modo si è completamente scollegati dallo schema del database, che è invece
espresso solamente tramite i mapping.
In questa situazione il programmatore sa che le operazioni da effettuare prevedono
l’accesso alla proprietà Impiegato per tutti gli ordini, per cui la strategia di Lazy
Load è controproducente in termini di prestazioni, dato che viene eseguito un numero
elevato di interrogazioni al database. Anche i questo caso NHibernate ha la soluzione,
basta cambiare la query in questo modo:
"from Ordini o inner join fetch o.Impiegato"
Grazie alla clausola inner join fetch si chiede a NHibernate di recuperare tutti i
dati con una join, proprio come se il lazy load fosse stato disattivato. Questa strategia
di fetch, in cui si recuperano tutti i dati con una singola interrogazione al db, viene
chiamata Eager Load, ovvero caricamento anticipato, ed è utile in tutti quei casi in
cui si sa già in anticipo come verrà usato il grafo di oggetti.
È conveniente che gli oggetti siano tutti mappati come lazy, in questo modo si
può poi scegliere al momento del la query se usare un caricamento lazy oppure eager
per i vari rami del grafo di oggetti che si vuole usare.
L’oggetto Nhibernate.ISession presenta due metodi distinti per recuperare entità
70
CAPITOLO 3. SOLUZIONI NEL MONDO .NET: NHIBERNATE
Figura 3.13: Un agenzia
dal loro id: Get() e Load(). Il metodo Get(), recupera l’oggetto dal database
e restituisce null se non esiste nessun record con l’id specificato, il metodo Load()
ritorna invece un proxy senza eseguire nessuna query; in questo caso se non è presente
un record con l’id specificato viene generata eccezione al momento del primo accesso
ad una proprietà. Il metodo Load() permette quindi la creazione di un proxy ed è
utile per creare associazioni; supponiamo di voler associare l’impiegato con id 12 ad
un ordine, in questo caso utilizzando la chiamata Session.Load¡Impiegati¿(12) si crea
un proxy che può essere assegnato al la proprietà Impiegto dell’ordine in questione. Il
vantaggio di questo approccio è che il proxy permette di impostare la relazione senza
dover veramente caricare l’oggetto dal database.
Vedremo nella prossima sezione alcuni scenari di applicazione nell’uso di Nhibernate.
3.5
Scenari di applicazione
Dopo aver introdotto Nhibernate, forniamo qualche scenario di utilizzo di tale libreria.
Ricordiamo lo schema entità-relazione del capitolo 2 relativo ad un’agenzia (vedi
figura 3.10).
Ipotizziamo di realizzare un applicazione che abbia bisogno d’interagire con una
base di dati che rappresenta in modo banalissimo un’agenzia che ha un certo numero
di Dipartimenti in cui lavorano certe Persone che hanno determinati compiti.
Ci focalizzeremo solo è soltanto sulla persistenza degli oggetti entità.
Scenario num. 1
In questo primo scenario cercheremo di fare l’operazione più semplice ovvero ricevere
dal database tutte le Entità memorizzate (ottenere una lista di oggetti corrispondenti
ad una ’select * from ..’); per fare questa operazione dobbiamo definire i due file di cui
3.5. SCENARI DI APPLICAZIONE
71
abbiamo parlato in precedenza. Partiamo dal POCO dell’entità (qui il Dipartimento
ma il procedimento è lo stesso) che è sicuramente più familiare:
public class Dipartimento
{
//ATTRIBUTI
private long _id;
private string _nome;
private string _sede;
//COSTRUTTORE DI DEFAULT
public Dipartimento()
{}
//COSTRUTTORE
public Dipartimento(string pnome, string psede)
{
this._nome = pnome;
this._sede = psede;
}
//PROPRIETA’
public Id()
{
get { return _id; }
set { _id = value; }
}
public Nome()
{
get { return _nome; }
set { _nome = value; }
}
public Sede()
{
get { return _sede; }
set { _sede = value; }
}
}//end classe Dipartimento
Adesso ci servirà il relativo file di mapping Dipartimento.hbm.xml:
72
CAPITOLO 3. SOLUZIONI NEL MONDO .NET: NHIBERNATE
<?xml version="1.0"?>
<!DOCTYPE nhibernate-mapping PUBLIC
"-//NHibernate/NHibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<nhibernate-mapping>
<class name="Dipartimento" table="dipartimento" lazy="false">
<id name="Id" column="id_dipartimento">
<generator class="native"/>
</id>
<property name="Nome"/>
<property name="Sede"/>
</class>
</nhibernate-mapping>
Escluse le righe di intestazione il resto del file è fondamentale ai fini della persistenza. Come possiamo vedere definiamo un nodo nhibernate-mapping. All’interno di
un mapping è possibile definire varie classi; per la classe Dipartimento specifichiamo
che essa dovrà corrispondere ad una tupla della tabella ’dipartimento’ e che vogliamo
che gli oggetti siano caricati subito (lazy=false). All’interno del nodo di una classe
possiamo specificare il mapping tra le colonne del database e gli attributi di istanza della classe (qui in realtà non dobbiamo definire niente se non le proprietà che
vogliamo siano settate nell’oggetto caricato dal database perché i nomi degli attributi
di istanza sono uguali ai nomi delle colonne del database).
L’ultima cosa interessante è il campo id, che deve derivare dal valore della colonna
id dipartimento (qui infatti i nomi dell’attributo e della colonna sono diversi e quindi
bisogna specificare questa corrispondenza) e che per generare id univoci nel database
si sfrutta il generatore native (esistono diversi tipi di generatori, dai nativi a quelli
corrispondenti a tecniche di High-Low).
Ora sfruttiamo la libreria Nhibernate:
public static List<Dipartimento> listaTuttiDipartimenti()
{
List<Dipartimento> result = null;
Session session = NhibernateUtil.getSessionFactory().openSession();
Transaction tx = null;
try
{
tx = session.beginTransaction();
Query q = session.createQuery("from Dipartimento");
result = q.list();
tx.commit();
}
catch (NhibernateException e)
{
3.5. SCENARI DI APPLICAZIONE
73
if (tx!=null)
tx.rollback();
throw e;
}
finally
{
session.close();
}
return result;
}
//Crea un Dipartimento dati nome e sede e lo salva nel database
public static Dipartimento creaDipartimento(string pnome, string psede)
{
Dipartimento dip = new Dipartimento(pnome, psede);
Session session = NhibernateUtil.getSessionFactory().openSession();
Transaction tx = null;
try
{
tx = session.beginTransaction();
session.persist(d);
tx.commit();
}
catch (NhibernateException e)
{
if (tx!=null)
tx.rollback();
throw e;
}
finally
{
session.close();
}
return dip;
}
In questi due metodi vengono aperte le sessioni (abbiamo visto cosa sono nell’introduzione a Nhibernate) tramite le quali si possono eseguire una serie di azioni: il
primo metodo crea una Query che riporterà una lista di tutti i Dipartimenti presenti
nel database; il secondo metodo invece salva un oggetto del database delegando a
Nhibernate la creazione di un id univoco, la esecuzione delle varie insert necessarie e
la gestione degli errori.
74
CAPITOLO 3. SOLUZIONI NEL MONDO .NET: NHIBERNATE
Scenario num. 2
Nel primo esempio abbiamo visto alcune operazioni molto semplici e che tutto sommato si ottengono semplicemente anche con OLEDB; come sappiamo il vero problema
tra database e linguaggio di programmazione ad oggetti è il cosiddetto problema del
impedance mismatch visto nel primo capitolo. Vediamo allora di aggiungere la navigabilità tramite riferimenti; vogliamo che una volta che sia caricato un Dipartimento
dal database esso contenga una lista delle persone che vi lavorano. Questa operazione
in OLEDB richiederebbe di eseguire una query e analizzare il risultato per caricare
ogni Persona in una lista. In Nhibernate questo si fa molto più semplicemente; innanzitutto si deve aggiungere un attributo di istanza al Dipartimento, ovvero una
lista di Persone che vi lavorano.
Relazione UNO — MOLTI
public class Dipartimento
{
//ATTRIBUTI
private long _id;
private string _nome;
private string _sede;
private Set _persone;
//COSTRUTTORE DI DEFAULT
public Dipartimento()
{
_persone = new HashSet();
}
//COSTRUTTORE
public Dipartimento(string pnome, string psede)
{
this._nome = pnome;
this._sede = psede;
_persone = new HashSet();
}
...
...
}//end classe Dipartimento
Bisogna creare anche il file POCO per Persona e aggiungere il mapping tra la
tabella Persone e la classe. La classe Persona è uguale alla classe Dipartimento. Il
file mapping invece bisogna modificarlo.
3.5. SCENARI DI APPLICAZIONE
75
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Nhibernate/Nhibernate Mapping DTD 3.0//EN"
"http://nhibernate.sourceforge.net/nhibernate-mapping-3.0.dtd">
<nhibernate-mapping>
<class name="Dipartimento" table="dipartimento" lazy="false">
<id name="Id" column="id_dipartimento">
<generator class="native"/>
</id>
<property name="Nome"/>
<property name="Sede"/>
<set name="persone" lazy="false">
<key column="fk_dipartimento" not-null="true"/>
<one-to-many class="Persona"/>
</set>
</class>
</nhibernate-mapping>
Come è possibile vedere abbiamo aggiunto un set al mapping del Dipartimento
specificando che le Persone vengano caricate subito(lazy=false), quale è la chiave
esterna e che si tratta di una relazione uno a molti con la classe Persona unidirezionale.
Il mapping delle Persone:
<?xml version="1.0"?>
<!DOCTYPE nhibernate-mapping PUBLIC
"-//nhibernate/nhibernate Mapping DTD 3.0//EN"
"http://nhibernate.sourceforge.net/nhibernate-mapping-3.0.dtd">
<nhibernate-mapping>
<class name="Persona" table="persona">
<id name="Id" column="id_persona">
<generator class="native"/>
</id>
<property name="Nome"/>
</class>
</hibernate-mapping>
Il mapping tra la classe Persona e la tabella persone è molto banale e richiama il
mapping originale che avevamo definito per il Dipartimento. Ora possiamo sfruttare
questi nuovi mapping per ottenere un livello di interazione maggiore; sfruttando infatti
lo stesso codice che abbiamo visto prima per caricare la lista dei Dipartimenti e le
altre opzioni, questa volta dal database verranno caricate automaticamente le Persone
del Dipartimento, verranno salvate se si dovesse creare un nuovo Dipartimento con
altre Persone; il tutto modificando solo i file di configurazione del mapping.
76
CAPITOLO 3. SOLUZIONI NEL MONDO .NET: NHIBERNATE
Scenario num. 3
Nello scenario 2 per le Persone abbiamo definito un mapping semplice; abbiamo cioè
caricato solo l’id e il nome. Possiamo però aggiungere la possibilità di sfruttare
la relazione di appartenenza ad un Dipartimento anche nel verso opposto, ovvero da
Persona al Dipartimento dove lavora(supposto unico per semplicità). Andremo quindi
a definire un mapping bidirezionale tra Dipartimento e Persona. Per farlo innanzitutto
dobbiamo aggiungere un attributo di istanza per il Dipartimento all’interno della
classe Persona:
public class Persona
{
..
private Dipartimento _dipartimento;
...
//PROPRIETA’
public Dipartimento()
{
get { return _dipartimento;}
set { _dipartimento = value;}
}
}//end classe Persona
Dobbiamo inoltre aggiungere il mapping bidirezionale tra Persona e Dipartimento; per farlo modifichiamo il file Persona.hbm.xml:
Relazioni MOLTI—UNO
<?xml version="1.0"?>
<!DOCTYPE nhibernate-mapping PUBLIC
"-//nhibernate/nhibernate Mapping DTD 3.0//EN"
"http://nhibernate.sourceforge.net/nhibernate-mapping-3.0.dtd">
<nhibernate-mapping>
<class name="Persona" table="persona">
<id name="Id" column = "id_persona">
<generator class="native"/>
</id>
<property name="Nome"/>
<many-to-one name="dipartimento" column="fk_dipartimento"
not-null="true"/>
</class>
</nhibernate-mapping>
3.5. SCENARI DI APPLICAZIONE
77
Come si può vedere è bastato mettere un elemento che rappresenta l’opposto dell’elemento che abbiamo nel mapping per il Dipartimento. Il programma di esempio
dello scenario 3 serve a verificare la bidirezionalità della relazione; carica cioè il primo
Dipartimento, prende il primo elemento della lista delle Persone che lavorano presso
quel Dipartimento e verifica che quella Persona abbia il riferimento corretto al Dipartimento precedente. Nello scenario 3 è inoltre considerato un altro problema: le
sessioni divise. Ipotizziamo di dover caricare un oggetto dal database e di dover usare
questo oggetto in strati più alti della nostra applicazione. Usando OLEDB una volta
caricato l’oggetto dovremmo valutare attentamente come agire in una situazione del
genere, con nhibernate invece possiamo semplicemente caricare l’oggetto, chiudere
la sessione in cui abbiamo eseguito queste azioni, lavorare per un certo tempo con
l’oggetto e poi aggiornare lo stato nel database in maniera che rimanga consistente.
Come si può constatare in nhibernate tutto questo è spaventosamente semplice:
Dipartimento dip = creaDipartimento(".....",".....");
//crea un nuovo Dipartimento
//ed esegue il codice per salvarlo nel database
//ipotizziamo che l’oggetto venga passato allo strato superiore
//e che la sesssione sia chiusa
try
{
thread.sleep(2000);
}
catch (System.Exception e)
{}
dip.Nome = "Ingegneria del Software";
Session session = NhibernateUtil.getSessionFactory().openSession();
Transaction tx = null;
try
{
tx = session.beginTransaction();
session.saveOrUpdate(d);
tx.commit();
}
catch (nhibernateException e)
{
if (tx!=null)
tx.rollback();
throw e;
}
78
CAPITOLO 3. SOLUZIONI NEL MONDO .NET: NHIBERNATE
finally
{
session.close();
}
Scenario num. 4
Nello scenario 4 sfruttiamo nhibernate per creare un Dipartimento a patto che non
ne esista già uno con gli stessi parametri, che altrimenti sarà semplicemente caricato
dal database.
Dipartimento dip = null;
Session session = NhibernateUtil.getSessionFactory().openSession();
Transaction tx = null;
try
{
tx = session.beginTransaction();
List<Dipartimento> l =
session.createQuery("from Dipartimento where
nome=:nome and sede=:sede");
setString("nome", nome).setString("sede", sede).list();
if (l.size() == 0)
{
dip = new Dipartimento(nome, sede);
session.save(d);
}
else
dip = l.iterator().next();
tx.commit();
}
catch (NhibernateException e)
{
if (tx!=null)
tx.rollback();
throw e;
}
finally
{
session.close();
}
return dip;
3.5. SCENARI DI APPLICAZIONE
79
Scenario num. 5
In questo ultimo scenario cercheremo di vedere come la configurazione di Nhibernate
possa portare ad una differenza sostanziale nelle prestazioni e nell’uso di questo strumento. Ipotizziamo di voler ottenere una lista delle Persone con i rispettivi lavori;
dopo quanto visto sembrerebbe una operazione facile, quasi banale; e invece, proprio
qui stanno le insidie. Cominciamo innanzitutto definendo il mapping per i Job:
Relazioni MOLTI-MOLTI
<?xml version="1.0"?>
<!DOCTYPE nhibernate-mapping PUBLIC
"-//nhibernate/nhibernate Mapping DTD 3.0//EN"
"http://nhibernate.sourceforge.net/nhibernate-mapping-3.0.dtd">
<nhibernate-mapping>
<class name="Job" table="job">
<id name="Id" column="id_job">
<generator class="native"/>
</id>
<property name="Lavoro" column="nome"/>
<set name="persone" table="assegnazione_compiti" inverse="true">
<key column="fk_job"/>
<many-to-many column="fk_persona" class="Persona"/>
</set>
</class>
</nhibernate-mapping>
Modifichiamo anche il mapping per Persona in modo da aver per ogni Persona la
lista dei suoi lavori(ovviamente dobbiamo modificare anche la classe Persona, ma in
modo del tutto uguale a quanto fatto negli altri scenari:
<?xml version="1.0"?>
<!DOCTYPE nhibernate-mapping PUBLIC
"-//nhibernate/nhibernate Mapping DTD 3.0//EN"
"http://nhibernate.sourceforge.net/nhibernate-mapping-3.0.dtd">
<nhibernate-mapping>
<class name="Persona" table="persona">
<id name="Id" column="id_persona">
<generator class="native"/>
80
CAPITOLO 3. SOLUZIONI NEL MONDO .NET: NHIBERNATE
</id>
<property name="Nome"/>
<set name="job" table="assegnazione_compiti" lazy="false">
<key column="fk_persona"/>
<many-to-many column="fk_job" class="Job"
fetch="join"/>
</set>
<many-to-one name="dipartimento" column="fk_dipartimento"
not-null="true" fetch="join" lazy="false"/>
</class>
</nhibernate-mapping>
Per caricare una lista delle Persone con i rispettivi Job potremmo pensare di usare
un codice del genere:
Query q = session.createQuery("from Persona");
Una sintassi del genere chiede a Nhibernate di caricare tutte le Persone memorizzate nel database e visto che il mapping richiede che i job siano caricati insieme che
le liste siano popolate con i rispettivi Job. Di seguito possiamo vedere l’output con
abilitata le visualizzazione delle query necessarie per ricavare le informazioni.
Stampo tutte le persone con i rispettivi job
Nhibernate: select persona0_.id_persona ....
Nhibernate: select job0_.fk_persona as fk1_1_ ....
Nhibernate: select job0_.fk_persona as fk1_1_ ....
Nhibernate: select job0_.fk_persona as fk1_1_ ....
Nhibernate: select job0_.fk_persona as fk1_1_ ....
Nhibernate: select job0_.fk_persona as fk1_1_ ....
Nhibernate: select job0_.fk_persona as fk1_1_ ....
leonardo
Job: [scrivere documentazione, scrivere codice]
ale
Job: [rispondere alle mail, dialogare con i clienti]
Come possiamo facilmente vedere dai log, Nhibernate esegue una query per ottenere tutte le Persone e successivamente una per ogni Persona per ricercare i suoi
3.5. SCENARI DI APPLICAZIONE
81
Job. È inutile dire che tale comportamento è inutile quanto dannoso, in quanto non
sfrutta le caratteristiche dei DBMS attuali ed esegue un numero di query che è lineare
con il numero di Persone. È possibile, ragionando un secondo, trovare la soluzione
e riuscire ad applicarla; si tratta semplicemente di eseguire una query unica in cui
ottengo sia la lista delle Persone che dei Job relativi. Nhibernate si occuperà per me
di ritornare una lista di Persone correttemente inizializzate con i relativi Job senza
dover analizzare autonomamente il risultato della query. La query da eseguire è la
seguente:
Query q = session.createQuery("from Persona as p
left outer join fetch p.job");
Possiamo verificare come questa volta non ci sia bisogno di eseguire un numero
molto alto di query, ma come ne basti semplicemente una.
Stampo tutte le persone con i rispettivi job
Nhibernate: select persona0_.id_persona as ....
leonardo
Job: [scrivere codice, scrivere documentazione]
Capitolo 4
Realizzazione del mapper automatico per Nhibernate
In questo capitolo si vedrà la parte operativa della presente tesi. Dopo aver introdotto
una tecnologia per la progettazione del software a livello molto professionale e di altà
qualità, presenteremo il progetto Generatore di Strati Software (GSS 1.0) sviluppato
da me, in particolare si vedrà com’è stato realizzato il file di mapping, cuore di ogni
ORM.
4.1
UML e metodologie per la progettazione del
software
All’inizio degli anni ’90 esistevano molte metodologie orientate agli oggetti; ognuna si
basava sulle esperienze di autori e su punti di vista diversi. Queste metodologie sono
concorrenti sul mercato. Scegliere una metodologia più adatta ad una certa azienda
spesso non è ad alcun titolo una decisione razionale, ma piuttosto è più simile ad
un atto di fede. Nei vent’anni successivi alle prime esperienze con le metodologie
di sviluppo orientate agli oggetti furono investiti molti soldi nello sviluppo di varie
notazioni per la descrizione di problemi tecnici e le relative soluzioni. Cosi sı̀ affermò
il linguaggio UML(Unified Modeling Language). Questo linguaggio fù sviluppato da
tre autori, cioè James Rumbaugh, Grady Booch e Ivar Jacobson i quali a sua volta
avevano sviluppato singolarmente tre metodologie orientate agli oggetti.
1. OMT
2. OOAD
3. OOSE
Ognuno di questi metodi aveva, naturalmente, i suoi punti di forza e i suoi punti
deboli. Ad esempio, l’OMT si rivelava ottimo in analisi e debole nel design. Booch
1991, al contrario, eccelleva nel disegno e peccava in analisi. OOSE aveva il suo punto
di forza nell’analisi dei requisiti e del comportamento di un sistema ma si rivelava
debole in altre aree.
UML è una notazione; i.e. non impone alcuna modalità di lavoro che possa
portare a una metodologia ben precisa. Questo rende possibile adottare varie tecniche
di sviluppo software basandosi su un’unica notazione: UML.
Elenchiamo, qui di seguito, alcuni dei benefici derivanti dall’utilizzo del linguaggio
UML:
• un sistema software, grazie al linguaggio UML, viene disegnato professionalmente e documentato ancor prima che ne venga scritto il relativo codice da
parte degli sviluppatori. Si sarà cosi in grado di conoscere in anticipo il risultato
finale del progetto su cui si sta lavorando;
82
4.1. UML E METODOLOGIE PER LA PROGETTAZIONE DEL
SOFTWARE
83
• poichè la fase di disegno del sistema precede la fase di scrittura del codice, ne
consegue che questa e resa piu agevole ed efficiente, oltre al fatto che in tal
modo e più facile scrivere del codice riutilizzabile in futuro. I costi di sviluppo,
dunque, si abbassano notevolmente con l’utilizzo del linguaggio UML;
• è più facile prevedere e anticipare eventuali buchi nel sistema. Il software che
si scrive, si comporterà esattamente come ci si aspetta senza spiacevoli sorpese
finali;
• l’utilizzo dei diagrammi UML permette di avere una chiara idea, a chiunque sia
coinvolto nello sviluppo, di tutto l’insieme che costituisce il sistema. In questo
modo, si potranno sfruttare al meglio anche le risorse hardware in termini di
memoria ed efficienza, senza sprechi inutili o, al contrario, rischi di sottostima
dei requisiti di sistema;
• grazie alla documentazione del linguaggio UML diviene ancora più facile effettuare eventuali modifiche future al codice. Questo, ancora, a tutto beneficio
dei costi di mantenimento del sistema.
Mostriamo brevemente una sintesi delle operazioni da svolgersi per una corretta
serializzazione del lavoro di progettazione di un software:
• Raccolta dei requisiti: È la prima fase di lavoro congiunto con il cliente, che
partendo dalle sue intenzioni iniziali e dai suoi desideri, ha lo scopo di produrre
un documento informale, scritto in linguaggio naturale, che elenca i requisiti e
le specifiche richiesti. Deve essere il più possibile breve e chiaro;
• Stesura del glossario: In questa fase si deve definire la terminologia del
progetto, identificando con precisione e accurattezza le entità (eventi, persone,
strutturazioni, ecc.) coinvolte nel sistema del mondo reale (ovvero del dominio
del business) che hanno importanza per il sistema informatico obiettivo del
progetto. È importante definire con precisione le entità allo scopo sia di definire
meglio i loro scenari d’uso (Use Case), sia di individuare le classi entità (Class
Diagram di analisi). Il risultato finale è il glossario;
• Stesura degli Use Case - Fase di Analisi: In questa fase devono essere
individuati con precisione gli scenari di interazione fra il sistema e gli attori,
ovvero le entità esterne al sistema con cui esso interagisce e comunica. I passi
necessari in questa fase possono essere cosı̀ suddivisi:
1. Definizione esatta del boundary o confine del sistema (entro sistemi particolarmente complessi questa fase può anche essere applicata a
sottosistemi).
2. Identificazione e definizione degli attori, ossia delle entità esterne con cui
il sistema (o i sottosistemi) oggetto dell’analisi interagiscono e comunicano;
3. Individuazione dei vari scenari di uso/interazione fra sistema ed attori, che
corrisponderanno ai singoli casi d’uso, identificati da elissi nel diagramma;
4. Definizione delle interazioni entro i singoli casi d’uso; tali interazioni,
strutturate nella forma di richiesta dell’attore cui corrisponde una risposta
del sistema, andranno a costituire i campi descrizione dei singoli casi d’uso
(operazione detta in gergo “srotolamento” dello use case);
84
CAPITOLO 4. REALIZZAZIONE DEL MAPPER AUTOMATICO PER
NHIBERNATE
5. Esame dei diagrammi cosı̀ ottenuti e delle loro descrizioni per procedere
alla raccolta a fattore comune di parti fra i singoli use case entro diagrammi, facendo uso delle relazioni extends ed include definibili tra i vari
casi d’uso. Il passo 5 può essere iterato piu volte; occorre tenere conto
della granularità del problema e del grado di definizione e precisione che
si vuole raggiungere. Inoltre si deve considerare che un singolo caso d’uso
spesso da origine ad una singola maschera (sia essa un menu testuale, una
singola finestra in ambiente grafico o una pagina Web).
Il prodotto di questo passo è l’insieme completo degli use case inseriti entro uno
o più diagrammi, ciascuno dei quali corredato da una adeguata descrizione,
strutturata chiaramente in forma di request-response, e considerando sia il
percorso principale di interazione (basic course) sia gli eventuali percorsi alternativi (alternative course). Il diagramma e le descrizioni devono essere ben
strutturati, chiari ed esaurienti in quanto tutti i passi successivi si baseranno
su di essi;
• Stesura del Class Diagram di analisi: In questa fase deve essere realizzato il diagramma delle classi di analisi. Esso deve indicare chiaramente tutte
le classi entità, ossia le classi definibili come proiezioni nel dominio dell’applicazione software dell’entità del problema in cui l’applicazione software andrà
ad operare, più eventuali altre classi individuate nel corso dell’analisi e che
siano di una certa rilevanza per i concetti funzionali che definiscono i requisiti
del progetto. In pratica nel diagramma, che è l’equivalente dal punto di vista
del ruolo del diagramma Entità - Relazione (ER) usato nelle metodologie di
sviluppo più tradizionali, devono essere indicate in modo chiaro:
1. tutte le classi entità che fanno parte del dominio del problema;
2. gli attribuiti caratteristici di tali classi, eventualmente procedendo alla individuazione dei singoli attributi o dei gruppi che consentano una
indentificazione univoca delle istanze delle classi ovvero dei singoli oggetti;
3. le associazioni che intercorrono tra tali classi; queste associazioni (che
corrispondono alle relazioni dei diagrammi ER) sono importanti perchè
in sede di implementazione del codice indicheranno anche la visibilità
necessaria tra le classi, cioe quali altre classi (eventualmente appartenenti
ad altri package o namespace) potranno essere viste da una certa classe
e definendo quindi la loro interdipendenza;
4. i versi di tali associazioni (ad esempio, se la classe magazzino deve conoscere
la classe prodotto, non e sempre vero il contrario);
5. le molteplicità di tali associazioni (es. uno a molti, molti a molti) e l’eventuale necessità di definire classi di associazione. Per esempio il concetto
di proprietà di un’auto, una volta rappresentato nel dominio delle classi,
può costuire una classe di associazione fra l’auto e la persona che ricopre
il ruolo di proprietario;
6. eventuali rapporti di inclusione legati a tali associazioni e suddivisi fra
aggregazione e composizione. Si ricordi che l’eliminazione di una composizione, indicata con il diamante nero, elimina anche tutti i suoi elementi componenti, mentre l’eliminazione di una aggregazione, indicata nella rappresentazione UML dei class diagram con il diamante bian-
4.1. UML E METODOLOGIE PER LA PROGETTAZIONE DEL
SOFTWARE
85
co/nero, non elimina anche i componenti che comunque hanno un ambito
di sopravvivenza indipendente;
7. eventuali rapporti di ereditarietà fra le classi ottenuti applicando i principi
di generalizzazione e specializzazione, ovvero raccogliendo a fattor comune
attributi e metodi o aggiungendone di nuovi.
Il processo che conduce al diagramma finale è ovviamente iterativo e può dirsi
stabilizzato quando tutte le relazioni (in senso ampio) fra le classi sono chiaramente individuate. Il Class Diagram di analisi e fondamentale per tutti i passi
successivi;
• Scelta architetturale: La scelta architetturale è un passo fondamentale in
quanto le fasi successive ne verranno pesantemente condizionate. Esistono comunque regole generali che ne aiutano lo svolgimento quali il pattern ModelView-Controller (MVC) (che abbiamo visto) ed il conseguente approccio multicanale alla realizzazione delle in- terfacce utenti. Seguendo tale metodo si
separa nettamente l’interfaccia utente vera e propria (View) che ha lo scopo
di presentare i dati all’utente ed è ovviamente soggetta a vincoli, dal tipo di
canale di comunicazione utilizzato (interfaccia a finestre grafiche, shell a caratteri, ecc.), dal reattore agli eventi trasmessi dall’utente (Controller) che usa i
metodi forniti dagli strati interni dell’applicazione (Model e relativi Adapter)
per garantire all’utente i servizi associati alle richieste effettuate. Grazie all’approccio multicanale, eventualmente corredato dall’uso di altri strati di Adapter,
diviene possibile riutilizzare (almeno in buona parte) il Controller (ed ovviamente gli strati sottostanti) cambiando solo la View quando si cambia canale,
passando, ad esempio, da una applicazione GUI ad una Web. La scelta dell’architettura deve anche segnalare limiti e criticita nel sistema che sarà realizzato. L’output di questa fase sono documenti tecnici architetturali che saranno
poi corredati da eventuali Component Diagram e Deployment Diagram solo al
termine della fase di progetto vera e propria;
• Definizione del class diagram di progetto - Fase di Progettazione: In
questa fase occorre definire chiaramente tutte le classi che fanno parte dell’applicazione software da implementare. Il Class Diagram di Progetto è l’elenco
completo delle classi e di tutte le relazioni e su di esso si basa anche il dimensionamento della fase di sviluppo (ovvero la scrittura vera e propria del
codice). Il processo che permette di giungere al diagramma delle classi di progetto e necessariamente iterativo. Si parte dal diagramma delle classi di analisi
e progressivamente vengono inserite tutte le classi di servizio che permettono
al programma nel suo insieme di operare correttamente ed in modo efficiente.
Le classi di servizio sono ovviamente fortemente dipendenti nella loro struttura dall’architettura scelta e da eventuali framework utilizzati nel progetto.
Se un diagramma di analisi ben fatto può essere spesso utilizzato con diverse
tecnologie ad oggetti, ovvero essere punto di partenza per progetti analoghi realizzati su piattaforme diverse, un diagramma di progetto è chiaramente molto
più influenzato dalla tecnologia usata. Il processo usa anche altri diagrammi
UML:
1. i diagrammi di interazione (sequence diagram, che evidenzia la sequenza temporale delle interazioni, e collaboration diagram, che chiarisce la
dipendenza fra le classi) sono di importanza fondamentale sia per la
86
CAPITOLO 4. REALIZZAZIONE DEL MAPPER AUTOMATICO PER
NHIBERNATE
definizione dei metodi che le classi offrono (e dei loro argomenti e valori di
ritorno), sia per l’individuazione di eventuali colli di bottiglia che vengono
risolti con l’inserimento di nuove classi o la cancellazione di quelle ridondanti. In teoria ad ogni use case corrisponde almeno un sequence o un collaboration diagram: infatti ogni corso di eventi individuato nell’analisi con
gli use case dovrebbe produrre una precisa sequenza temporale di invocazione di metodi all’interno dell’insieme delle classi costituenti il sistema
software. Non sempre è però indispensabile un’implementazione completa, specie se gli eventi presentano evidenti analogie e similitudini, nel qual
caso basta apportare le opportune descrizioni di accompagnamento;
2. i diagrammi di attività (activity diagram) derivano dagli Use Case è danno loro una sequenza temporale e logica. Inoltra possono aiutare molto
nella definizione della mappa di navigazione tra le finestre, consentendo di definire completamente l’interfaccia utente di un applicativo ed
eventualmente di realizzare i prototipi d’analisi;
3. i diagrammi di stato (statechart diagram) sono anch’essi molto importanti
per valutare l’evoluzione temporale delle singole classi (o meglio degli
oggetti da esse istanziati) o di sottosistemi che esse vanno a costituire,
aiutando ad individuare eventuali condizioni critiche o colli di bottiglia
dell’applicazione.
L’obiettivo finale è comunque la realizzazione del Class Diagram di Progetto,
completo di tutte le classi. Spesso per motivi di chiarezza (specialmente in
progetti grandi dove le classi sono molto numerose) il diagramma viene diviso
in package o namespace, associazioni di classi corrispondenti ad unità funzionali, che indicano esternamente solo le reciproche relazioni. Ciascun package
viene poi rappresentato completamente entro un diagramma di secondo livello.
Quasi sempre questa suddivisione funzionale viene anche portata a livello implementativo servendosi delle aggregazioni tipiche dei linguaggi, come i package
di Java o i namespace di C#. L’obiettivo deve essere sempre quello di avere
un diagramma leggibile, che serve come mappa per lo sviluppo. Da questo
diagramma possono anche essere generati gli scheletri delle classi attraverso
opportuni strumenti, oppure essere estrapolati i Fogli di specifica, ossia i documenti che descrivono ciascuna classe con attributi, metodi, vincoli e controlli
da implementare;
• Definizione delle strutture di contorno : Usando i diagrammi realizzati in
precedenza si arriva a definire le parti implementative di contorno del progetto,
che devono essere opportunamente documentate come segue:
1. definizione della base di dati, attraverso un EER, eventualmente corredato
dagli script di creazione delle tabelle e vincoli che genera la base di dati
nello specifico DBMS scelto;
2. definizione dell’insieme dei singoli componenti software (namespaces, librerie statiche o dinamiche, dll, ecc.) che devono essere prodotti, con
l’indicazione delle loro interdipendenze, attraverso un opportuno Component Diagram;
3. definizione della distribuzione dei componenti sulla o sulle piattaforme
di produzione prescelte attraverso uno o più opportuni Deployment Diagram;
4.2. OBIETTIVI DEL PROGETTO
87
4. stesura di opportuni documenti Readme ed altro che corredino il progetto
e l’installazione; in particolare devono essere chiaramente indicati eventuali limiti e/o malfunzionamenti delle piattaforme software e hardware
utilizzate;
5. stesura dell’opportuno manuale utente dell’applicazione secondo i criteri
stabiliti;
6. definizione delle scadenze e pianificazione dell’esecuzione temporale del
progetto in base ai dimensionamenti svolti e alle risorse a disposizione;
7. definizione dei testi e dei singoli casi di test;
8. pianificazione del collaudo e dell’entrata in produzione;
9. definizione della successiva fase di manutenzione.
Il linguaggio C#
Il linguaggio C# è il nuovo linguaggio semplice, moderno e orientato agli oggetti
progettato da Microsoft per combinare la potenza del C e C++ e la produttività
di Visual Basic. È molto simile a Java, il linguaggio introdotto dalla SUN Microsystems nel 1995, soprattutto in merito alla sintassi, alla grande integrazione
con il Web e alla gestione automatica della memoria. Le caratteristiche di tale
linguaggio sono:
1. C# è case sensitive, differenzia cioè tra maiuscole e minuscole;
2. Le parole chiave del linguaggio fanno uso principalmente del minuscolo;
3. I blocchi di codice sono delimitati da parentesi graffe;
4. Ogni istruzione, a eccezione dei cicli e dei blocchi di codice, va terminata
con il punto e virgola;
4.2
Obiettivi del progetto
Il progetto Generatore Strati Software ha come obiettivo quello che data una tabella
nel mondo dei database relazionali, costruisca in modo automatico le seguenti cose:
1. Il file POCO dove conterrà la classe entità;
2. L’adapter con le query automatiche dove saranno costruiti in maniera
automatica le principali operazioni CRUD (creazione, selezione, cancellazione
e aggiornamento);
3. Il wrapper dove conterrà metodi che richiamano l’adapter. Valido solo per le
query automatiche;
4. L’adapter che usa Nhibernate dove saranno costruiti in maniera automatica le principali operazioni CRUD usando la libreria Nhibernate che abbiamo
visto nel capitolo precedente;
5. Il file XML che serve a Nhibernate per il fare il mapping Object-Relational;
88
CAPITOLO 4. REALIZZAZIONE DEL MAPPER AUTOMATICO PER
NHIBERNATE
Figura 4.1: Use Case Diagram del GSS
Questo progetto ideato dalla ditta Area SP, sarà collocato nel framework 3.5 di
.NET. Per questo abbiamo deciso come scelta di linguaggio di programmazione il
principe del mondo .NET: C#.
Alla fine si dovrà provare a caricare dati in strutture OO da RDBMS e si dovranno confrontare in termini di prestazioni Nhibernate con il mio generatore di strati
software.
4.3
Analisi, progettazione ed implementazione
Nella figura 4.1 viene mostrato il diagramma dei casi d’uso che l’utente può scegliere
nell’utilizzo del software Generatore Strati Software.
L’utente all’inizio deve scegliere il nome del progetto dove vorrà salvare gli ingredienti per l’ORM. Dopo di che, dovrà connettersi ad una sorgente di basi di dati
fornendo una stringa di connessione e una volta che la connessione è avvenuta con
successo, dovrà selezionare una o più tabelle del database che è stato connesso. A
questo punto l’utente avrà la possibilità di generare i seguenti cinque file: file POCO,
file Adapter, file Adapter che usa Nhibernate, file Mapping. Per la generazione degli
Adapter, Wrapper vengono utilizzati dei template, che sono file di testo contenenti
dei tag. Esempio:
using System;
using System.Collections.Generic;
4.3. ANALISI, PROGETTAZIONE ED IMPLEMENTAZIONE
using
using
using
using
using
using
using
using
89
System.Collections;
System.Text;
System.Data;
System.Data.Common;
System.Data.OleDb;
System.Linq;
Poco;
AreaFramework.Wrapper;
namespace $NomeNamespace$.Adapter
{
public class $NomeTabella$_Adapter : ProtoAdapter
{
// ATTRIBUTI
// stringhe query di selezione (analoghe alle precedenti)
private static string sql_Select = "SELECT * FROM $NomeTabella$";
$GeneraQuerySelectDataBy$
private static string sql_SelectBy$Campi$ = sql_Select + "
WHERE ($Campi$ = ?) ";
private static string sql_SelectBy$ChiavePrimaria$ =
sql_Select + " WHERE ($ChiavePrimaria$ = ?) ";
private static string sql_SelectMax$ChiavePrimaria$ =
"SELECT MAX($ChiavePrimaria$) FROM $NomeTabella$";
private static string sql_DeleteBy$ChiavePrimaria$ =
"DELETE FROM $NomeTabella$ WHERE ($ChiavePrimaria$ = ?)";
private static string sql_DeleteByAllField =
"DELETE FROM $NomeTabella$ WHERE
($ChiavePrimaria$ = ? AND $CampiSeparatiAnd$)";
private static string sql_Insert =
"INSERT INTO $NomeTabella$ ($CampiSeparatiVirgola$)
VALUES ($Interrogativi$)";
private static string sql_UpdateBy$ChiavePrimaria$ =
"UPDATE $NomeTabella$
SET $CampiSeparatiVirgolaConAssegnamento$ WHERE
$ChiavePrimaria$ = ? ";
...
...
...
90
CAPITOLO 4. REALIZZAZIONE DEL MAPPER AUTOMATICO PER
NHIBERNATE
Vediamo brevemente la descrizione dei casi d’uso relativi alla figura 6.1.
Configurazione del progetto: L’utente inserisce il nome del progetto e altri
parametri per salvare i file che mano a mano vengono generati;
Connessione ad una base di dati: L’utente può scegliere come inserire la stringa
di connessione per accedere ad una sorgente di basi dati;
Selezione di una o più tabella da DBMS: L’utente dopo essersi connesso ad
una base di dati avrà la possibilità di selezionare una o più tabelle di quel
determinato database;
Scelta della sintassi del linguaggio da generare: L’utente avrà la possibilità
di scegliere il come generare l’Adapter e il Wrapper, ovvero quale sintassi di
linguaggi di programmazione orientati agli oggetti da utilizzare;
Genera file POCO: L’utente ha la possibilità di generare un classe entità che è
isomorfo ad una tabella che sono state selezionate in precedenza;
Genera Adapter: L’utente ha la possibilità di generare un Adapter partendo dalla
lettura di un template specifico;
Genera Wrapper: L’utente ha la possibilità di generare un Wrapper partendo
dalla lettura di un template specifico;
Genera Adapter che usa Nhibernate: L’utente ha la possibilità di generare
un Adapter che usa al suo interno la libreria open source Nhibernate partendo
dalla lettura di un template specifico;
Genera Mapper: L’utente ha la possibilità di generare un Mapper scritto in xml;
Lettura template: L’utente per generare l’adapter e il wrapper ha bisogno di
leggere un template, cioè un file di testo contenente del codice;
Per quanto riguarda la progettazione, ho utilizzato il paradigma MVC, strutturata
in tre livelli principali: LibCodeDB (model), Generatore Strati Software (View) e
LibVisualDb (Controller).
In figura 4.2 è possibile vedere l’organizzazione modulare dell’applicazione, dove
vengono evidenziati i seguenti namespace: LibCodeDb, Generatore Strati Software e
LibVisualDb.
In figura 4.3 è possibile vedere come è fatto il namespace LibCodeDB.
In figura 4.4 viene mostrato il namespace LibCodeDb.LinguaggiProgrammazione.
Mentre nella figura 4.5 viene mostrato il namespace LibCodeDb.Tags.
Per implementare il mio progetto ho utilizzato il concetto dell’ OCP (Open-Close
Principle). Ideato da Bertrand Meyer nel 1988.
Le entità software (Classi, Moduli, Funzioni ecc.)
estensioni, ma devono essere chiuse alle modifiche.
devono essere aperte alle
Ciò vuole dire che occorre strutturare un’applicazione in modo che sia possibile
aggiungere nuove funzionalità con una modifica minima al codice esistente. Occorre
evitare che una semplice modifica si diffonda con effetto domino nelle varie classi
4.3. ANALISI, PROGETTAZIONE ED IMPLEMENTAZIONE
91
Figura 4.2: Struttura a Namespace del Software GSS
Figura 4.3: Il Namespace LibCodeDB
Figura
4.4:
La
struttura
LibCodeDb.LinguaggiProgrammazione
del
namespace
92
CAPITOLO 4. REALIZZAZIONE DEL MAPPER AUTOMATICO PER
NHIBERNATE
Figura 4.5: La struttura del namespace LibCodeDb.Tags
dell’applicazione. Ciò rende il sistema fragile, incline a problemi di regressione e
dispendioso da estendere. Per isolare le modifiche, è possibile scrivere classi e metodi
in modo tale che che non sia mai necessario modificarli una volta scritti. Un esempio
di utilizzo dell’OCP è la classe astratta Linguaggi. Essa fornisce dei metodi virtuali
che una volta che una classe concreta (per esempio Java) eredità da lui, può ridefinirsi
i metodi della classe astratta (polimorfismo dinamico e tecnica override).
Se un giorno qualcuno ci chiedesse di generare gli ingredienti per l’ORM nel
linguaggio Python, per esempio, basta scrivere una classe concreta Python che eredità
da Linguaggi.
4.4
Il progetto compiuto
Vediamo adesso come ho progettato il mapper automatico.
Il file che permette a una libreria ORM di mappare tabelle del mondo RDBMS e
classi entità nel mondo OOP è chiamato file di mapping. Esso come abbiamo visto
quando si è parlato di Nhibernate consiste nello specificare usando sintassi XML in
che modo si mappa la classe con la tabella e le relazioni tra le tabelle.
Le classi che ho usato per creare il mapping, come si vede nella figura 4.3 sono:
1. GeneratoreMapping;
2. DescrittoreSchemaDb;
Il DescrittoreSchemaDb è una classe che permette di estrapolare le relazioni
che intercorrono tra le tabelle di un RDBMS mediante una proprietà dell’oggetto
OleDbConnection.
Nel codice che segue viene mostrato il metodo che ho creato per estrapolare le
relazioni.
/// <summary>
/// Carica dentro i vettori latoUno[] e latoMolti[]
4.4. IL PROGETTO COMPIUTO
///
///
///
///
93
i nomi delle tabelle facente parte rispettivamente
il lato uno e il lato molti delle relazioni che intercorrono
tra le tabelle di un database.
</summary>
private void caricaVettoriRelazioni()
{
tabella = connessione.GetOleDbSchemaTable(
OleDbSchemaGuid.Foreign_Keys,
new object[]
{null, null, null, null });
latoUno = new string[tabella.Rows.Count];
latoMolti = new string[tabella.Rows.Count];
fk = new string[tabella.Rows.Count];
int i = 0;
foreach (DataRow row in tabella.Rows)
{
//Nome della tabella dove sta il lato 1 della relazione
string strTable = row["PK_TABLE_NAME"].ToString();
//Nome della colonna del genitore, ovvero della tabella dove
//c’è il lato 1
string strParentColName = row["PK_COLUMN_NAME"].ToString();
//Nome della chiave esterna della tabella figlia
//relazionate con il padre
string strChildColName = row["FK_COLUMN_NAME"].ToString();
//Nome della tabella figlia relazionata con strTable
string strChild = row["FK_TABLE_NAME"].ToString();
latoMolti[i] = strChild;
latoUno[i] = strTable;
fk[i] = strChildColName;
++i;
}//end ciclo
//end metodo caricaVettoriRelazioni()
Per quanto rigurda la classe GeneratoreMapping ho utilizzato i metodi della
libreria System.Xml. Esso supporta classi che servono per elaborare e creare file
XML.
Qui di seguito viene riportato il codice relativo alla generazione automatica del
mapper in XML.
94
CAPITOLO 4. REALIZZAZIONE DEL MAPPER AUTOMATICO PER
NHIBERNATE
public void costruisciXML(string
string
string
{
XmlDocument documentoMapper =
p_nomeClasse,
p_nomeTabella,
p_percorso)
new XmlDocument();
//Creazione sezione dichiarazione e dtd
XmlDeclaration xDec = documentoMapper.CreateXmlDeclaration("1.0",
"utf-8",
"no");
XmlDocumentType xType = documentoMapper.CreateDocumentType(
"hibernate-mapping",
@"-//Hibernate/Hibernate Mapping DTD 2.0//EN",
@"http://hibernate.sourceforge.net/
hibernate-mapping-2.0.dtd", null);
//Inseriamo, prima della radice del documento, la dichiarazione
documentoMapper.InsertBefore(xDec, documentoMapper.DocumentElement);
//Creiamo il primo nodo, radice
XmlNode root = documentoMapper.CreateNode(XmlNodeType.Element,
"hibernate-mapping",
null);
XmlAttribute attributi = documentoMapper.CreateAttribute("xmlns");
attributi.Value = "urn:nhibernate-mapping-2.2";
attributi = documentoMapper.CreateAttribute("assembly");
attributi.Value = "";
root.Attributes.Append(attributi);
attributi = documentoMapper.CreateAttribute("namespace");
attributi.Value = "";
root.Attributes.Append(attributi);
documentoMapper.InsertAfter(root, xDec);
//Nodo Class : <class ...>...</class>
XmlNode nodoClasse = documentoMapper.CreateNode(
XmlNodeType.Element,
"class", null);
attributi = documentoMapper.CreateAttribute("name");
attributi.Value = p_nomeClasse;
nodoClasse.Attributes.Append(attributi);
attributi = documentoMapper.CreateAttribute("table");
attributi.Value = p_nomeTabella;
nodoClasse.Attributes.Append(attributi);
//Nodo Id :<id ...>....</id>
XmlNode nodoId = documentoMapper.CreateNode(XmlNodeType.Element,
4.4. IL PROGETTO COMPIUTO
95
"id",
null);
attributi = documentoMapper.CreateAttribute("name");
attributi.Value =
ManipolazioneStringhe.trasformaMaiuscoloLetteraIniziale(
tInf["chiavePrimaria"]);
nodoId.Attributes.Append(attributi);
attributi = documentoMapper.CreateAttribute("unsaved-value");
attributi.Value = "0";
nodoId.Attributes.Append(attributi);
attributi = documentoMapper.CreateAttribute("access");
attributi.Value = "property";
nodoId.Attributes.Append(attributi);
attributi = documentoMapper.CreateAttribute("type");
attributi.Value = tipiNibh.getChiave(tInf["tipoChiavePrimaria"]);
nodoId.Attributes.Append(attributi);
//Nodo Id figlio : <id ...> <column ...> ... </column></id>
XmlNode nodoIdFiglio = documentoMapper.CreateNode(XmlNodeType.Element,
"column", null);
attributi = documentoMapper.CreateAttribute("name");
attributi.Value = tInf["chiavePrimaria"];
nodoIdFiglio.Attributes.Append(attributi);
XmlNode nodoIdGenerator = documentoMapper.CreateNode(
XmlNodeType.Element,
"generator", null);
attributi = documentoMapper.CreateAttribute("class");
attributi.Value = "increment";
nodoIdGenerator.Attributes.Append(attributi);
//Creiamo le relazioni "familiari" tra i nodi
root.AppendChild(nodoClasse);
nodoClasse.AppendChild(nodoId);
nodoId.AppendChild(nodoIdFiglio);
nodoId.AppendChild(nodoIdGenerator);
//Sezione propriety
//Nodo Proprietà : <proprety ...> costruzione dinamica
IEnumerator count = nomeAttributi.GetEnumerator();
int i = 0;
while (count.MoveNext())
{
bool presente = false;
96
CAPITOLO 4. REALIZZAZIONE DEL MAPPER AUTOMATICO PER
NHIBERNATE
if (presente == false)
{
XmlNode nodoProperty = documentoMapper.CreateNode(
XmlNodeType.Element,
"property", null);
attributi = documentoMapper.CreateAttribute("name");
attributi.Value =
ManipolazioneStringhe.trasformaMaiuscoloLetteraIniziale(
nomeAttributi[i].ToString());
nodoProperty.Attributes.Append(attributi);
attributi = documentoMapper.CreateAttribute("type");
attributi.Value = tipiNibh.getChiave(
tipiNomeAttributi[i].ToString());
nodoProperty.Attributes.Append(attributi);
XmlNode nodoPropertyFiglio = documentoMapper.CreateNode(
XmlNodeType.Element,
"column", null);
attributi = documentoMapper.CreateAttribute("name");
attributi.Value = nomeAttributi[i].ToString();
nodoPropertyFiglio.Attributes.Append(attributi);
nodoProperty.AppendChild(nodoPropertyFiglio);
nodoClasse.AppendChild(nodoProperty);
}
++i;
}//end ciclo
//Sezione attributi relazionati: Gestione relazione 1 a molti
IEnumerator c = latoUno.GetEnumerator();
int conta = 0;
while (c.MoveNext())
{
if (latoUno[conta].Contains(p_nomeTabella))
{
XmlNode nodoSet = documentoMapper.CreateNode(
XmlNodeType.Element,
"set", null);
attributi = documentoMapper.CreateAttribute("name");
attributi.Value =
ManipolazioneStringhe.trasformaMaiuscoloLetteraIniziale(
latoMolti[conta].ToString());
nodoSet.Attributes.Append(attributi);
attributi = documentoMapper.CreateAttribute("lazy");
attributi.Value = "false";
nodoSet.Attributes.Append(attributi);
XmlNode nodoSetFiglio = documentoMapper.CreateNode(
XmlNodeType.Element,
4.4. IL PROGETTO COMPIUTO
"key", null);
attributi = documentoMapper.CreateAttribute("column");
attributi.Value = fk[conta];
nodoSetFiglio.Attributes.Append(attributi);
attributi = documentoMapper.CreateAttribute("not-null");
attributi.Value = "true";
nodoSetFiglio.Attributes.Append(attributi);
XmlNode nodoSetFiglioDue = documentoMapper.CreateNode(
XmlNodeType.Element,
"one-to-many", null);
attributi = documentoMapper.CreateAttribute("class");
attributi.Value = latoMolti[conta];
nodoSetFiglioDue.Attributes.Append(attributi);
nodoSet.AppendChild(nodoSetFiglio);
nodoSet.AppendChild(nodoSetFiglioDue);
nodoClasse.AppendChild(nodoSet);
}
++conta;
}//end ciclo
//Sezione attributi relazionati: Gestione relazione molti a 1
IEnumerator c2 = latoMolti.GetEnumerator();
int contaMolti = 0;
while (c2.MoveNext())
{
if (latoMolti[contaMolti].Contains(p_nomeTabella))
{
XmlNode nodoManyToOne = documentoMapper.CreateNode(
XmlNodeType.Element,
"many-to-one", null);
attributi = documentoMapper.CreateAttribute("name");
attributi.Value = latoUno[contaMolti];
nodoManyToOne.Attributes.Append(attributi);
attributi = documentoMapper.CreateAttribute("column");
attributi.Value = fk[contaMolti];
nodoManyToOne.Attributes.Append(attributi);
attributi = documentoMapper.CreateAttribute("not-null");
attributi.Value = "true";
nodoManyToOne.Attributes.Append(attributi);
nodoClasse.AppendChild(nodoManyToOne);
}
++contaMolti;
}//end ciclo
//Salviamo il documento XML in PERCORSO
percorso = p_percorso;
97
98
CAPITOLO 4. REALIZZAZIONE DEL MAPPER AUTOMATICO PER
NHIBERNATE
if (File.Exists(percorso))
File.Delete(percorso);
documentoMapper.Save(percorso);
}//end metodo costruisciXML(..)
Il namespace System.Xml è formato dalle seguenti classi:
• XmlNode: Una classe astratta che rappresenta un singolo nodo in un documento XML;
• XmlDocument: Estende XmlNode. Questo è l’implementazione di W3C
DOM. Esso provedere a rappresentare in memoria un albero di un documento
XML, abilitato a navigarlo e a modificarlo;
• XmlDataDocument: Estende XmlDocument. Questo è un documento che
può essere caricato da dati XML o da dati relazionali in ADO.NET;
• XmlResolver: Una classe astratta che risolve esternamente risorse basate su
XML come DTD e riferimenti a schema;
• XmlNodeList: Una lista di XmlNodes che possono essere iterati tra di loro;
• XmlUrlResolver: Estende XmlResolver. Risolve esternamente i nomi delle
risorse tramite un URI.
Come si può vedere dal codice sopra, per prima cosa ho costruito la parte iniziale,
che è uguale per tutti i file con estensione .hbm.xml.
Quindi mi costruisce la seguente cosa: Esempio tratto dall’entità ANAGRAFICA
che vedremo nel prossimo capitolo quando parleremo delle entità e delle basi di dati
che ho utilizzato.
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<hibernate-mapping assembly="" namespace="">
<class name="ANAGRAFICA" table="ANAGRAFICA">
...
...
</class>
</hibernate-mapping>
Poi ho costruito il tag id ... ....id è il risultato è stato:
<id name="Id" unsaved-value="0" access="property" type="System.Int32">
<column name="ID" />
<generator class="increment" />
</id>
4.4. IL PROGETTO COMPIUTO
99
Poi ho costruito il tag property..column...column..property è il risultato è stato:
<property name="Cognome" type="System.String">
<column name="COGNOME" />
</property>
<property name="Nome" type="System.String">
<column name="NOME" />
</property>
<property name="Codice" type="System.String">
<column name="CODICE" />
</property>
<property name="Data_nascita" type="System.DateTime">
<column name="DATA_NASCITA" />
</property>
<property name="Binario" type="System.Boolean">
<column name="BINARIO" />
</property>
<property name="Colore" type="System.String">
<column name="COLORE" />
</property>
Adesso veniamo a vedere come ho generato il file POCO o POJO o POVO a
seconda della scelta di un linguaggio di programmazione orientati agli oggetti.
In sede di Analisi si è scelta la generazione automatica nei seguenti tre linguaggi
di programmazione:
1. CSharp: File POCO;
2. Java: File POJO;
3. Vb.net: File POVO;
Nella figura 4.6 viene mostrata la classe astratta Linguaggi con i suoi metodi
astratti. In questo capitolo ci focalizzeremo sul metodo astratto GeneraTestoPoco.
Per comodità faccio vedere come ho implementato il file POCO. Per i file POJO
e POVO l’implementazione è analoga cambia ovviamente soltanto la sintassi e il tipo
di dato (vedremo nel prossimo capitolo quando parleremo di che basi di dati ho scelto
i tipi di dato da RDBMS al mondo .NET).
Riporto le costanti che ho usato per generare il POCO. Si noti che sono usati solo
ed esclusivamente per la sintassi C#.
CAPITOLO 4. REALIZZAZIONE DEL MAPPER AUTOMATICO PER
100
NHIBERNATE
Figura 4.6: La classe astratta Linguaggi con le classi concrete che ridefiniscono
dei metodi di Linguaggi
//ATTRIBUTI COSTANTI
#region SEZIONE COSTANTI C#
const string INSERISCI_UNDERLINE = "_";
const string CREAZIONE_PUB_STAT_CONST_STRING =
"public static string fieldName";
const string CREAZIONE_CLASSE_PUB = "public class ";
const string CREAZIONE_PUB_VIRT = "public virtual ";
const string DIRETTIVA_USO = "using System;";
const string DIRETTIVA_USO_IESI = "using Iesi;\n
using Iesi.Collections.Generic;\n
using Iesi.Collections;";
const string SPAZIO_NOMI = "namespace ";
const
const
const
const
const
string
string
string
string
string
APRI_GRAFFA = "{";
CHIUDI_GRAFFA = "}";
TONDE = "()";
SPAZIO = " ";
PUNTO_VIRGOLA = "; ";
const
const
const
const
const
const
string
string
string
string
string
string
COMMENTO_COSTANTI = "//ATTRIBUTI COSTANTI";
COMMENTO_ATTRIBUTI = "//ATTRIBUTI";
COMMENTO_COSTRUTTORE = "//COSTRUTTORI";
COMMENTO_PROPRIETA = "//PROPRIETA’";
COMMENTO_FINE_CLASSE = "//end class ";
COMMENTO_FINE = "//end ";
const string VISIBILITA_PRIVATE = "private ";
4.4. IL PROGETTO COMPIUTO
101
const string VISIBILITA_PUBLIC = "public ";
const string INSERIRE_GET = "get ";
const string INSERIRE_SET = "set ";
const string VALORE_DI_RITORNO = "return ";
const string VALORE = "value;";
const string INSIEME = "ISet ";
const string INIZIALLIZZA_INSIEME = " = new HashedSet();";
#endregion
Il metodo astratto che viene ridefinito nella classe concreta CSharp è il seguente:
public override string generaTestoPoco(
string p_nomeNamespace,
string p_nomeTabella,
ArrayList p_nomeAttributi,
ArrayList p_tipoDato,
DescrittoreSchemaDB p_descrittoreDb)
{
latoUno = p_descrittoreDb.ritornaLatoUno();
latoMolti = p_descrittoreDb.ritornaLatoMolti();
fk = p_descrittoreDb.ritornaForeignKey();
nomeColonnaConvertita = new string[p_nomeAttributi.Count];
string testoPoco = null;
testoPoco = sezioneTesta(p_nomeNamespace,
p_nomeTabella,
p_nomeAttributi,
p_tipoDato,
p_descrittoreDb);
testoPoco += sezioneProprieta(p_nomeAttributi,
p_tipoDato,
p_nomeTabella);
testoPoco += sezioneCoda(p_nomeTabella,
p_nomeNamespace);
return Indentazione.indenta(testoPoco, 4);
}//end metodo generaTestoPoco();
CAPITOLO 4. REALIZZAZIONE DEL MAPPER AUTOMATICO PER
102
NHIBERNATE
Come si può notare ho suddiviso la mia generazione automatica del file POCO
in tre sezioni: sezioneTesta, sezioneProprietà e sezioneCoda. Quando si è parlato di
classi entità e abbiamo fatto vedere di come è fatto, è intuibile la scelta che ho fatto
nel generarlo.
La sezioneTesta è cosi fatta:
///
///
///
///
///
///
<summary>
Costruisce la parte iniziale del mio file POCO
</summary>
<param name="p_nomeNamespace">Il nome del namespace</param>
<param name="p_nomeTabella">Il nome della tabella</param>
<param name="p_nomeAttributi">I nomi degli attributi
di una tabella di un database</param>
/// <param name="p_tipoDato">I tipi di dato degli attributi
di una tabella di un database</param>
/// <returns>Una stringa contenente
///
tutta la parte iniziale del file POCO</returns>
private string sezioneTesta(string p_nomeNamespace,
string p_nomeTabella,
ArrayList p_nomeAttributi,
ArrayList p_tipoDato,
DescrittoreSchemaDB p_descrittoreDb)
{
string temp = DIRETTIVA_USO +
Environment.NewLine +
Environment.NewLine +
Environment.NewLine +
SPAZIO_NOMI + p_nomeNamespace +
Environment.NewLine +
APRI_GRAFFA +
Environment.NewLine +
Environment.NewLine +
CREAZIONE_CLASSE_PUB + p_nomeTabella +
Environment.NewLine +
APRI_GRAFFA +
Environment.NewLine +
COMMENTO_COSTANTI +
Environment.NewLine +
Environment.NewLine +
costruzioneAttributiCostanti(p_nomeAttributi) +
Environment.NewLine +
COMMENTO_ATTRIBUTI +
Environment.NewLine +
Environment.NewLine;
IEnumerator count = p_nomeAttributi.GetEnumerator();
4.4. IL PROGETTO COMPIUTO
int i = 0;
while (count.MoveNext())
{
if (nomeColonnaConvertita[i] != null)
temp += VISIBILITA_PRIVATE +
p_tipoDato[i] +
SPAZIO +
nomeColonnaConvertita[i] +
PUNTO_VIRGOLA +
Environment.NewLine;
++i;
}
nomeTabellaRifLatoMolti = new string[latoUno.Length];
nomeTabellaRifLatoUno = new string[latoMolti.Length];
//Controlliamo se quella determinata tabella è soggetta
//a una relazione relativo al lato uno
int t = 0;
while (t < latoUno.Length)
{
if (latoUno[t].Contains(p_nomeTabella))
{
nomeTabellaRifLatoMolti[t] = INSERISCI_UNDERLINE +
latoMolti[t].ToLower();
temp += VISIBILITA_PRIVATE +
INSIEME +
nomeTabellaRifLatoMolti[t] +
PUNTO_VIRGOLA +
Environment.NewLine;
}
++t;
}
//Controlliamo se quella determinata tabella è soggetta
//a una relazione relativo al lato molti
int y = 0;
while (y < latoMolti.Length)
{
if (latoMolti[y].Contains(p_nomeTabella))
{
nomeTabellaRifLatoUno[y] = INSERISCI_UNDERLINE +
latoUno[y].ToLower();
temp += VISIBILITA_PRIVATE +
latoUno[y] +
SPAZIO +
103
CAPITOLO 4. REALIZZAZIONE DEL MAPPER AUTOMATICO PER
104
NHIBERNATE
nomeTabellaRifLatoUno[y] +
PUNTO_VIRGOLA +
Environment.NewLine;
}
++y;
}
temp += Environment.NewLine +
COMMENTO_COSTRUTTORE +
Environment.NewLine +
Environment.NewLine;
//Costruzione del mio costruttore di default
temp += VISIBILITA_PUBLIC +
p_nomeTabella +
TONDE +
Environment.NewLine +
APRI_GRAFFA +
Environment.NewLine;
t = 0;
while (t < nomeTabellaRifLatoMolti.Length)
{
if (nomeTabellaRifLatoMolti[t] != null)
temp += nomeTabellaRifLatoMolti[t] +
INIZIALLIZZA_INSIEME +
Environment.NewLine;
++t;
}
temp += CHIUDI_GRAFFA +
Environment.NewLine;
return temp;
}//end metodo sezioneTesta()
La sezioneProprieta è cosi fatta:
///
///
///
///
<summary>
Costruisce le property del mio file POCO
</summary>
<param name="p_listaAttributi">I nomi degli attributi
di una tabella di un database</param>
/// <param name="p_tipoDato">I tipi di dato degli attributi
di una tabella di un database</param>
/// <returns>La stringa contenente la parte delle proprietà</returns>
private string sezioneProprieta(ArrayList p_listaAttributi,
4.4. IL PROGETTO COMPIUTO
105
ArrayList p_tipoDato,
string p_nomeTabella)
{
string temp = Environment.NewLine +
COMMENTO_PROPRIETA +
Environment.NewLine +
Environment.NewLine;
IEnumerator count = p_listaAttributi.GetEnumerator();
int i = 0;
while (count.MoveNext())
{
bool presente = ManipolazioneStringhe.verificaPresenzaFk(
p_listaAttributi[i].ToString(), fk);
bool presente = false;
if (presente == false)
{
temp += CREAZIONE_PUB_VIRT +
p_tipoDato[i] +
SPAZIO +
ManipolazioneStringhe.trasformaMaiuscoloLetteraIniziale(
p_listaAttributi[i].ToString()) +
Environment.NewLine +
APRI_GRAFFA +
Environment.NewLine +
INSERIRE_GET +
APRI_GRAFFA +
SPAZIO +
VALORE_DI_RITORNO +
nomeColonnaConvertita[i] +
PUNTO_VIRGOLA +
CHIUDI_GRAFFA +
Environment.NewLine +
INSERIRE_SET +
APRI_GRAFFA +
SPAZIO +
nomeColonnaConvertita[i] +
" = " +
VALORE +
SPAZIO +
CHIUDI_GRAFFA +
Environment.NewLine +
CHIUDI_GRAFFA +
Environment.NewLine +
Environment.NewLine;
}
++i;
}
CAPITOLO 4. REALIZZAZIONE DEL MAPPER AUTOMATICO PER
106
NHIBERNATE
//Controlliamo se quella determinata tabella è soggetta
//a una relazione relativo al lato uno
int t = 0;
while (t < latoUno.Length)
{
if (latoUno[t].Contains(p_nomeTabella))
{
temp += CREAZIONE_PUB_VIRT +
INSIEME +
ManipolazioneStringhe.trasformaMaiuscoloLetteraIniziale(
latoMolti[t]) +
Environment.NewLine +
APRI_GRAFFA +
Environment.NewLine +
INSERIRE_GET +
APRI_GRAFFA +
SPAZIO +
VALORE_DI_RITORNO +
nomeTabellaRifLatoMolti[t] +
PUNTO_VIRGOLA +
CHIUDI_GRAFFA +
Environment.NewLine +
INSERIRE_SET +
APRI_GRAFFA +
SPAZIO +
nomeTabellaRifLatoMolti[t] +
" = " +
VALORE +
SPAZIO +
CHIUDI_GRAFFA +
Environment.NewLine +
CHIUDI_GRAFFA +
Environment.NewLine +
Environment.NewLine;
}
++t;
}
//Controlliamo se quella determinata tabella è soggetta a una relazione
//relativo al lato molti
int y = 0;
while (y < latoMolti.Length)
{
if (latoMolti[y].Contains(p_nomeTabella))
{
temp += CREAZIONE_PUB_VIRT +
latoUno[y] +
SPAZIO +
4.4. IL PROGETTO COMPIUTO
107
ManipolazioneStringhe.trasformaMaiuscoloLetteraIniziale(
latoUno[y]) +
Environment.NewLine +
APRI_GRAFFA + Environment.NewLine +
INSERIRE_GET +
APRI_GRAFFA +
SPAZIO +
VALORE_DI_RITORNO +
nomeTabellaRifLatoUno[y] +
PUNTO_VIRGOLA +
CHIUDI_GRAFFA +
Environment.NewLine +
INSERIRE_SET +
APRI_GRAFFA +
SPAZIO +
nomeTabellaRifLatoUno[y] +
" = " +
VALORE +
SPAZIO +
CHIUDI_GRAFFA +
Environment.NewLine +
CHIUDI_GRAFFA +
Environment.NewLine +
Environment.NewLine;
}
++y;
}
return temp;
}//end metodo sezioneProprieta()
La sezioneCoda è cosi fatta:
/// <summary>
/// Costruisce la parte finale del mio file POCO
/// </summary>
/// <param name="p_nomeTabella">Il nome della tabella</param>
/// <param name="p_nomeNamespace">il nome del namespace</param>
/// <returns>La stringa contenente la parte finale del file POCO</returns>
private string sezioneCoda(string p_nomeTabella, string p_nomeNamespace)
{
string temp = Environment.NewLine +
CHIUDI_GRAFFA +
COMMENTO_FINE_CLASSE +
p_nomeTabella +
CAPITOLO 4. REALIZZAZIONE DEL MAPPER AUTOMATICO PER
108
NHIBERNATE
Environment.NewLine +
CHIUDI_GRAFFA +
COMMENTO_FINE +
SPAZIO_NOMI +
SPAZIO +
p_nomeNamespace;
return temp;
}//end metodo sezioneCoda()
Capitolo 5
Realizzazione del prototipo operativo
In questo capitolo viene spiegato in dettaglio le entità e le basi di dati che ho utilizzato.
Inoltre verrà visto come ho generato l’Adapter e il Wrapper nella versione con query
automatiche e versione con Nhibernate.
5.1
Le entità e la base di dati utilizzata
Per realizzare il prototipo operativo, abbiamo già visto le classi che ho utilizzato e
abbiamo parlato di come ho realizzato sia il mapper che l’oggetto entità.
Abbiamo detto più volte che esiste una stretta relazione tra le classi entità e le
tabelle di un RDBMS.
I DBMS che ho utilizzato sono:
1. SQL SERVER 2005;
2. MICROSOFT ACCESS 2003;
3. MYSQL 5.0;
4. ORACLE 10g;
Diamo un occhiata alla conversione tra tipo di dato nel mondo RDBMS specifico
per i 4 RDBMS sopra elencati e il tipo di dato corrispondente nel mondo .NET.
Inoltre ci sarà anche il tipo di conversione che ho effettuato tramite il mio Generatore
Strati Software.
La conversione dei tipi di dato: RDBMS — .NET
Nella tabella 4.1 vengono riportati le conversioni di tipo di dato relative al DBMS:
SQLSERVER 2005.
Nella tabella 4.2 vengono riportati le conversioni di tipo di dato relative al DBMS:
MICROSOFT ACCESS 2003.
Nella tabella 4.3 vengono riportati le conversioni di tipo di dato relative al DBMS:
MYSQL 5.0.
Prima di passare a vedere i tipi di dato di ORACLE, mi soffermo sulla tabella
dei tipi di MYSQL.
Ho riscontrato che per i tipi bool e boolean sono trattati come tinyint(1), ovvero
come short (Int16). 1
Nella tabella 4.4 vengono riportati le conversioni di tipo di dato relative al DBMS:
ORACLE 10g.
1
Per maggiori dettagli vedere: http://database.html.it/guide/lezione/2444/tipi-di-dati/
109
110
CAPITOLO 5. REALIZZAZIONE DEL PROTOTIPO OPERATIVO
Tipo mondo RDBMS
int
bigint
binary(50)
bit
char(10)
datetime
decimal(18,0)
float
image
money
nchar(10)
ntext
numeric(18,0)
nvarchar(50)
nvarchar(MAX)
real
smalldatetime
smallint
smallmoney
sql variant
text
timestamp
uniqueidentifier
varbinary(50)
varbinary(MAX)
varchar(50)
varchar(MAX)
xml
tinyint
Tipo .NET
System.Int32
System.Int64
System.Byte[]
System.Boolean
System.String
System.DateTime
System.Decimal
System.Double
System.Byte[]
System.Decimal
System.String
System.String
System.Decimal
System.String
System.String
System.Single
System.DateTime
System.Int16
System.Decimal
System.Object
System.String
System.Byte[]
System.Guid
System.Byte[]
System.Byte[]
System.String
System.String
System.String
System.Byte
Conversione usando GSS
int
long
byte[]
bool
string
DateTime
int
double
byte[]
decimal
string
string
int
string
string
float
DateTime
short
decimal
object
string
byte[]
Guid
byte[]
byte[]
string
string
string
byte
Tabella 5.1: Conversioni di tipo di dato tra DBMS a .NET e conversione dato
da GSS per SQLSERVER 2005
5.2. VERSIONE CON QUERY AUTOMATICHE
Tipo mondo RDBMS
testo
memo
Numerico: byte
Numerico: intero
Numerico: intero lungo
Numerico: precisione singola
Numerico: precisione doppia
Numerico: IDReplica
Numerico: decimale
data/ora
Valuta: numero generico
Valuta: valuta
Valuta: euro
Valuta: fisso
Valuta: standard
Valuta: percentuale
Valuta: notazione scientifica
contatore
Si/No: si/no
Si/No: vero/falso
Si/No: on/off
oggetto OLE
collegamento ipertestuale
Tipo .NET
System.String
System.String
System.Byte
System.Int16
System.Int32
System.Single
System.Double
System.Guid
System.Decimal
System.DateTime
System.Decimal
System.Decimal
System.Decimal
System.Decimal
System.Decimal
System.Decimal
System.Decimal
System.Int32
System.Boolean
System.Boolean
System.Boolean
System.Byte[]
System.String
111
Conversione usando GSS
string
string
byte
short
int
float
double
Guid
int
DateTime
decimal
decimal
decimal
decimal
decimal
decimal
decimal
int
bool
bool
bool
byte[]
string
Tabella 5.2: Conversioni di tipo di dato tra DBMS a .NET e conversione dato
da GSS per Access 2003
Un osservazione da fare è che i tipi NUMERIC, REAL, SMALLINT, DEC vengono
trasformati rispettivamente nei tipi NUMBER, FLOAT, NUMBER, NUMBER.
Come base di dati utilizzata per provare il Generatore Strati Software è un
database di nome ProtoNews che mi ha fornito l’azienda.
5.2
Versione con Query automatiche
Per generare l’adapter che usa la versione con query automatiche, ho utilizzato la
seguente tecnica come illustra la figura 5.1.
Ho utilizzato la classe astratta Tag che come si vede nel codice seguente offre le
seguenti funzionalità:
public abstract class Tag
{
112
CAPITOLO 5. REALIZZAZIONE DEL PROTOTIPO OPERATIVO
Tipo mondo RDBMS
bigint
binary
bit
blob
bool
boolean
char
date
datetime
decimal
double
enum
float
int
longblob
longtext
mediumblob
mediumint
mediumtext
numeric
real
set
smallint
text
time
timestamp
tinyblob
tinyint
tinytext
varbinary
varchar
year
Tipo .NET
System.Int64
System.String
System.String
System.Byte[]
System.Int16
System.Int16
System.String
System.DateTime
System.DateTime
System.String
System.Double
System.String
System.Single
System.Int32
System.Byte[]
System.String
System.Byte[]
System.Int32
System.String
System.String
System.Double
System.String
System.Int16
System.String
System.String
System.DateTime
System.Byte[]
System.Int16
System.String
System.String
System.String
System.Int16
Conversione usando GSS
long
string
string
byte[]
short
short
string
DateTime
DateTime
string
double
string
float
int
byte[]
string
byte[]
int
string
string
double
string
short
string
string
DateTime
byte[]
short
string
string
string
short
Tabella 5.3: Conversioni di tipo di dato tra DBMS a .NET e conversione dato
da GSS per MYSQL 5.0
5.2. VERSIONE CON QUERY AUTOMATICHE
Tipo mondo RDBMS
varchar2
number
clob
blob
bfile
char
char varying
character
character varying
date
dec
decimal
double precision
float
int
integer
interval day
interval year
long
long raw
long varchar
national char
national char varying
national character
national character varying
nchar
nchar varying
nclob
numeric
raw
real
rowid
smallint
timestamp
urowid
varchar
binary double
binary float
Tipo .NET
System.String
System.Decimal
System.String
System.Byte[]
System.Byte[]
System.String
System.String
System.String
System.String
System.DateTime
System.Decimal
System.Decimal
System.Double
System.Double
System.Decimal
System.Decimal
System.String
System.String
System.String
System.Byte[]
System.String
System.String
System.String
System.String
System.String
System.String
System.String
System.String
System.Decimal
System.Byte[]
System.Double
System.String
System.Decimal
System.DateTime
System.String
System.String
System.Double
System.Single
113
Conversione usando GSS
string
int
string
byte[]
byte[]
string
string
string
string
DateTime
int
int
double
double
int
int
string
string
string
byte[]
string
string
string
string
string
string
string
string
int
byte[]
double
string
int
DateTime
string
string
double
float
Tabella 5.4: Conversioni di tipo di dato tra DBMS a .NET e conversione dato
da GSS per ORACLE 10g
114
CAPITOLO 5. REALIZZAZIONE DEL PROTOTIPO OPERATIVO
Figura 5.1: Tecnica utilizzata per generare l’adapter nella versione con query
automatiche
//COSTRUTTORE
/// <summary>
/// Costruttore di default della classe
/// </summary>
public Tag() { }
public abstract string rimpiazzati(Linguaggi p_linguaggio,
PopolatoreDatiDb p_pd,
string p_riga);
public static Tag tagFactory(string p_tipoTag)
{
switch (p_tipoTag)
{
case "NomeNamespace":
return new NomeNamespace(); //Il nome del namespace
case "NomeTabella":
return new NomeTabella(); //Il nome della tabella
case "ChiavePrimaria":
return new ChiavePrimaria();
5.2. VERSIONE CON QUERY AUTOMATICHE
115
case "GeneraQuerySelectDataBy":
return new GeneraQuerySelectDataBy();
case "GeneraMetodoSelectDataBy":
return new GeneraMetodoSelectDataBy();
case "CampiSeparatiAnd":
return new CampiSeparatiAnd();
case "CampiSeparatiVirgola":
return new CampiSeparatiVirgola();
case "CampiSeparatiVirgolaConAssegnamento":
return new CampiSeparatiVirgolaConAssegnamento();
case "Interrogativi":
return new Interrogativi();
case "TipoNomeChiavePrimaria":
return new TipoNomeChiavePrimaria();
case "NomeEntita":
return new NomeEntita();
case "ListaParametri":
return new ListaParametri();
case "TipoCampo":
return new TipoCampo();
case "ListaParametriNoEntita":
return new ListaParametriNoEntita();
case "GeneraParametri":
return new GeneraParametri();
case "TipoChiavePrimaria":
return new TipoChiavePrimaria();
case "ControlloID":
return new ControlloID();
case "ControlloOverride":
return new ControlloOverride();
default:
throw new System.NotSupportedException("The tag type " +
p_tipoTag.ToString() +
" is not recognized.");
}
}
}//end classe astratta Tag
Il template nella versione query automatiche è il seguente:
using
using
using
using
using
System;
System.Collections.Generic;
System.Collections;
System.Text;
System.Data;
116
CAPITOLO 5. REALIZZAZIONE DEL PROTOTIPO OPERATIVO
using
using
using
using
using
System.Data.Common;
System.Data.OleDb;
System.Linq;
Poco;
AreaFramework.Wrapper;
namespace $NomeNamespace$.Adapter
{
public class $NomeTabella$_Adapter : ProtoAdapter
{
// ATTRIBUTI
// stringhe query di selezione (analoghe alle precedenti)
private static string sql_Select = "SELECT * FROM $NomeTabella$";
$GeneraQuerySelectDataBy$
private static string sql_SelectBy$Campi$ = sql_Select +
" WHERE ($Campi$ = ?) ";
private static string sql_SelectBy$ChiavePrimaria$ = sql_Select +
" WHERE ($ChiavePrimaria$ = ?) ";
private static string sql_SelectMax$ChiavePrimaria$ =
"SELECT MAX($ChiavePrimaria$) FROM $NomeTabella$";
private static string sql_DeleteBy$ChiavePrimaria$ =
"DELETE FROM $NomeTabella$ WHERE ($ChiavePrimaria$ = ?)";
private static string sql_DeleteByAllField =
"DELETE FROM $NomeTabella$ WHERE ($ChiavePrimaria$ = ? AND
$CampiSeparatiAnd$)";
private static string sql_Insert =
"INSERT INTO $NomeTabella$ ($CampiSeparatiVirgola$) VALUES
($Interrogativi$)";
private static string sql_UpdateBy$ChiavePrimaria$ =
"UPDATE $NomeTabella$ SET $CampiSeparatiVirgolaConAssegnamento$
WHERE $ChiavePrimaria$ = ? ";
//-----------------------/////////////////////////////////////////////
// COSTRUTTORE
public $NomeTabella$_Adapter(DbConnection p_conn) : base(p_conn)
{}// end costruttore
/////////////////////////////////////////////
/// <summary>
/// Metodo per ottenere l’elenco completo dei dati
5.2. VERSIONE CON QUERY AUTOMATICHE
117
/// </summary>
/// <returns></returns>
public DataTable GetData()
{
return this.ExecuteQuery(sql_Select);
} // end method GetData
public DataTable GetDataBy$ChiavePrimaria$($TipoNomeChiavePrimaria$)
{
ArrayList ht = new ArrayList();
ht.Add(new DictionaryEntry("$ChiavePrimaria$", p$ChiavePrimaria$));
return this.ExecuteQuery(sql_SelectBy$ChiavePrimaria$, ht);
} // end method GetDataBy$ChiavePrimaria$
$GeneraMetodoSelectDataBy$
public DataTable GetDataBy$Campi$($TipoCampi$ p$CampiParametro$)
{
ArrayList ht = new ArrayList();
ht.Add(new DictionaryEntry("$Campi$", $CampiParametroConControllo$));
return this.ExecuteQuery(sql_SelectBy$Campi$, ht);
}// end method GetDataBy$Campi$
$END$
// METODI CHE RITORNANO VALORI SCALARI
public int GetMax$ChiavePrimaria$()
{
return ExecuteInt32Scalar(sql_SelectMax$ChiavePrimaria$, null);
}// end method GetMax$ChiavePrimaria$
/////////CANCELLAZIONE///////////////
// METODI DML
public int DeleteQuery($TipoNomeChiavePrimaria$)
{
ArrayList ht = new ArrayList();
ht.Add(new DictionaryEntry("$ChiavePrimaria$", p$ChiavePrimaria$));
return this.ExecuteNonQuery(sql_DeleteBy$ChiavePrimaria$, ht);
}// end method DeleteOneRecordBy$ChiavePrimaria$
public int DeleteByEntity($NomeEntita$ p$NomeEntita$)
{
118
CAPITOLO 5. REALIZZAZIONE DEL PROTOTIPO OPERATIVO
ArrayList ht = new ArrayList();
$ListaParametri$
ht.Add(new DictionaryEntry("$Campi$", $NomeEntita$.$Proprieta$));
return ExecuteNonQuery(sql_DeleteByAllField, ht);
} // end method DeleteByEntity
/////////INSERIMENTO///////////////
public int InsertQuery($TipoCampo$)
{
ArrayList ht = new ArrayList();
$ListaParametriNoEntita$
ht.Add(new DictionaryEntry("$Campi$", $CampiConControllo$));
return ExecuteNonQuery(sql_Insert, ht);
}// end method InsertQuery
public int InsertByEntity($NomeEntita$ p$NomeEntita$)
{
ArrayList ht = new ArrayList();
$ListaParametri$
ht.Add(new DictionaryEntry("$Campi$", $NomeEntita$.$Proprieta$));
return ExecuteNonQuery(sql_Insert, ht);
}// end method InsertByEntity
/////////AGGIORNAMENTO///////////////
public int UpdateQuery($TipoCampo$, $TipoNomeChiavePrimaria$)
{
ArrayList ht = new ArrayList();
$ListaParametriNoEntita$
ht.Add(new DictionaryEntry("$Campi$", $CampiConControllo$));
ht.Add(new DictionaryEntry("$ChiavePrimaria$", p$ChiavePrimaria$));
return ExecuteNonQuery(sql_UpdateBy$ChiavePrimaria$, ht);
5.2. VERSIONE CON QUERY AUTOMATICHE
119
}// end method UpdateQuery
} // end class $NomeTabella$_Adapter
} // end namespace
Qui mostro il cuore della conversione automatica: la classe ElaboraTemplate.
///
///
///
///
///
<summary>
Leggi un template(file di testo)
</summary>
<param name="p_nomeFile">percorso del template</param>
<returns>stringa contenente il testo del template convertito</returns>
public string leggiConvertaTemplate(string p_nomeFile,
Linguaggi p_linguaggio)
{
fileTemplateDb = new FileStream(p_nomeFile,
FileMode.OpenOrCreate,
FileAccess.Read);
StreamReader sr = new StreamReader(fileTemplateDb);
string testoConvertito = leggiRigaESostituisci(sr, 4, p_linguaggio);
//Rilascio le risorse
sr.Close();
fileTemplateDb.Close();
return testoConvertito;
}//end leggiConvertaTemplate()
Il metodo leggiRigaESostituisci è il seguente:
/// <summary>
/// Legge una riga dal template e sostituisce tutti i tag con del codice
/// </summary>
/// <param name="p_sr">Lo stream Reader</param>
/// <param name="p_quantoIndentare">Quanto identare</param>
/// <returns></returns>
private string leggiRigaESostituisci(StreamReader p_sr,
int p_quantoIndentare,
Linguaggi p_linguaggio)
120
CAPITOLO 5. REALIZZAZIONE DEL PROTOTIPO OPERATIVO
{
string testo = null;
int dollaroIniziale = -1;
int dollaroFinale = -1;
string tmp;
string riga = null;
do
{
codice = p_sr.ReadLine();
if (codice != null)
{
codice = codice.Trim();
riga = codice;
#region SEZIONE USATA PER GENERARE L’ADAPTER
#region Relativo al tag $GeneraQuerySelectDataBy$
if (riga.Contains(TAG + TAG_GENERA_QUERY_SELECT + TAG))
codice = codice.Remove(TAG_GENERA_QUERY_SELECT.Length + 2);
#endregion
#region Relativo al tag $ListaParametri$
per quei metodi che usano come parametro l’oggetto entità
if (riga.Contains(TAG + TAG_LISTA_PARAMETRI + TAG))
codice = codice.Remove(TAG_LISTA_PARAMETRI.Length + 2);
#endregion
#region Relativo al tag $ListaParametriNoEntita$
per quei metodo che usano come parametro il nome
e il tipo degli attributi di una tabella
if (riga.Contains(TAG + TAG_LISTA_PARAMETRI_NO_ENTITA + TAG))
codice = codice.Remove(TAG_LISTA_PARAMETRI_NO_ENTITA.Length + 2);
#endregion
#region Relativo al Tag $GeneraMetodoSelectDataBy$
if (riga.Contains(TAG + TAG_GENERA_METODO_SELECT + TAG))
{
string metodo = null;
while (codice.Contains(TAG + "END" + TAG) == false)
{
5.2. VERSIONE CON QUERY AUTOMATICHE
121
metodo += codice;
codice = Environment.NewLine +
p_sr.ReadLine();
}
codice = ManipolazioneStringhe.rimuoviTag(codice,
TAG +
"END"+
TAG);
codice = TAG + TAG_GENERA_METODO_SELECT + TAG;
riga = metodo;
}//end costruzione blocco relativo
//alla generazione dei metodi per le query di selezione.
#endregion
#endregion
#region SEZIONE USATA PER GENERARE IL WRAPPER
#region Relativo al tag $GeneraParametri$
if (riga.Contains(TAG + TAG_GENERA_PARAMETRI + TAG))
codice = codice.Remove(TAG_GENERA_PARAMETRI.Length + 2);
#endregion
#endregion
#region SEZIONE USATA IN COMUNE PER GENERARE L’ADAPTER E IL WRAPPER
while(codice.Contains(TAG))
{
dollaroIniziale = riga.IndexOf(TAG) + 1;
tmp = riga.Substring(dollaroIniziale);
dollaroFinale = tmp.IndexOf(TAG);
tmp = tmp.Substring(0, dollaroFinale);
codice = codice.Replace(TAG + tmp + TAG,
Tag.tagFactory(tmp).rimpiazzati(p_linguaggio, pd, riga));
riga = codice;
}
#endregion
}
testo += codice + Environment.NewLine;
}
while (codice != null);
testo = Indentazione.indenta(testo, p_quantoIndentare);
return testo;
122
CAPITOLO 5. REALIZZAZIONE DEL PROTOTIPO OPERATIVO
}//end metodo leggirigaesostituisci()
Il tutto sta nel seguente frammento di codice:
while(codice.Contains(TAG))
{
dollaroIniziale = riga.IndexOf(TAG) + 1;
tmp = riga.Substring(dollaroIniziale);
dollaroFinale = tmp.IndexOf(TAG);
tmp = tmp.Substring(0, dollaroFinale);
codice = codice.Replace(TAG + tmp + TAG,
Tag.tagFactory(tmp).rimpiazzati(p_linguaggio, pd, riga));
riga = codice;
}
Quello che viene generato in automatico è il seguente frammento di codice tratto
dal template Adapter:
Stringhe di select
private static string sql_Select = "SELECT * FROM $NomeTabella$";
$GeneraQuerySelectDataBy$
private static string sql_SelectBy$Campi$ =
sql_Select +
"WHERE ($Campi$ = ?) ";
private static string sql_SelectBy$ChiavePrimaria$ =
sql_Select +
"WHERE ($ChiavePrimaria$ = ?) ";
private static string sql_SelectMax$ChiavePrimaria$ =
"SELECT MAX($ChiavePrimaria$)
FROM $NomeTabella$";
Il tag GeneraQuerySelectDataBy permette di generare la stringa private static
string sql SelectByCampi =... tante volte quanti sono i campi/attributi della tabella
del RDBMS selezionato. Esempio di stringa generata:
private static string sql_Select = "SELECT * FROM Anagrafica";
private static string sql_SelectByNome =
sql_Select +
"WHERE (Nome = ?)";
ecc.
5.3. VERSIONE CON NHIBERNATE
123
Stringhe di cancellazione In questo frammento usiamo il tag CampiSeparatiAnd.
Esso genera in maniera dinamica la seguente stringa: Id = ? AND Nome = ? AND
ecc...
private static string sql_DeleteBy$ChiavePrimaria$ =
"DELETE FROM $NomeTabella$ WHERE ($ChiavePrimaria$ = ?)";
private static string sql_DeleteByAllField =
"DELETE FROM $NomeTabella$ WHERE ($ChiavePrimaria$ = ? AND
$CampiSeparatiAnd$)";
Stringhe di inserimento
In questo frammento usiamo i tag: CampiSeparatiV irgola e CampiSeparatiV irgolaConAssegnamento.
Essi generano rispettivamente le seguenti stringhe: Id, Nome, Cognome, ecc. e Id =
?, Nome = ? e Cognome = ?. In più abbiamo il tag Interrogativi che mi costruisce
una stringa dinamica di simboli ’ ?’ tanti quanti sono gli attributi di una tabella.
private static string sql_Insert =
"INSERT INTO $NomeTabella$ ($CampiSeparatiVirgola$) VALUES
($Interrogativi$)";
Stringhe di aggiornamento
private static string sql_UpdateBy$ChiavePrimaria$ =
"UPDATE $NomeTabella$ SET $CampiSeparatiVirgolaConAssegnamento$
WHERE $ChiavePrimaria$ = ? ";
5.3
Versione con Nhibernate
Per generare l’adapter che usa la versione con Nhibernate, ho utilizzato la seguente
tecnica come illustra la figura 5.2.
Di seguito riporto il template Adapter versione Nhibernate.
using System;
using System.Collections.Generic;
using System.Linq;
124
CAPITOLO 5. REALIZZAZIONE DEL PROTOTIPO OPERATIVO
Figura 5.2: Tecnica utilizzata per generare l’adapter nella versione Nhibernate
using
using
using
using
using
using
using
System.Text;
NHibernate;
NHibernate.Cfg;
System.Reflection;
Iesi.Collections;
System.Collections;
NHibernate.Criterion;
namespace Adapter.Adapter
{
public class NhibernateAdapter
{
public NhibernateAdapter()
{}
//Apertura della sessione
public static ISession OpenSession()
{
Configuration c = new Configuration();
c.AddAssembly(Assembly.GetCallingAssembly());
ISessionFactory f = c.BuildSessionFactory();
5.3. VERSIONE CON NHIBERNATE
125
return f.OpenSession();
}//end metodo OpenSession()
//metodi
//SELECT ALL
///
///
///
///
///
///
///
///
///
///
<summary>
Metodo che utilizza Nhibernate
per prelevare tutti i campi di una tabella
Precisamente esegue sotto la seguente query:
select * from nome_tabella
--------------------------nome_tabella equivalente a oggetto_entità
</summary>
<param name="p_tipoEntita">Il tipo dell’Oggetto Entità/param>
<returns>Lista ordinata del risultato di una query di tipo
select</returns>
public IList selectAll(Type p_tipoEntita, ISession pValue)
{
PropertyInfo[] propr = p_tipoEntita.GetProperties();
if (pValue == null)
{
using (ISession sessione = OpenSession())
{
return
sessione.CreateCriteria(p_tipoEntita).
AddOrder(Order.Asc(propr[0].Name)).List();
}
}
else
{
return pValue.CreateCriteria(p_tipoEntita).
AddOrder(Order.Asc(propr[0].Name)).List();
}
}
/// <summary>
///
/// </summary>
/// <param name="p_tipoEntita"></param>
/// <returns></returns>
public string selectMaxChiavePrimaria(Type p_tipoEntita)
{
PropertyInfo[] propr = p_tipoEntita.GetProperties();
using (ISession sessione = OpenSession())
{
String qIdMax = String.Format("select max(t.{0}) from {1} t ",
126
CAPITOLO 5. REALIZZAZIONE DEL PROTOTIPO OPERATIVO
propr[0].Name, p_tipoEntita.Name);
Console.WriteLine(qIdMax);
IQuery query = sessione.CreateQuery(qIdMax);
Object val = query.UniqueResult();
return val.ToString();
}
}
//INSERIMENTO
/// <summary>
///
/// </summary>
/// <param name="p_entita"></param>
/// <returns>Messaggio </returns>
public void insert(object p_entita, ISession pValue)
{
if (pValue == null)
{
//apro una sessione
using (ISession sessione = OpenSession())
{
//inizio una transazione
using (ITransaction transazione =
sessione.BeginTransaction())
{
//Salvo e eseguo il commit
sessione.Save(p_entita);
transazione.Commit();
}
}
}
else
{
pValue.Save(p_entita);
}
} //end insert(...)
//INSERIMENTO INTELLIGENTE / UPDATE
/// <summary>
///
/// </summary>
/// <param name="p_entita"></param>
/// <returns></returns>
public void insertIntelligence(object p_entita)
{
5.3. VERSIONE CON NHIBERNATE
//apro una sessione
using (ISession sessione = OpenSession())
{
//inizio una transazione
using (ITransaction transazione =
sessione.BeginTransaction())
{
//Salvo e eseguo il commit
sessione.SaveOrUpdate(p_entita);
transazione.Commit();
}
}
}//end insertIntelligence(...)
//AGGIORNAMENTO
/// <summary>
///
/// </summary>
/// <param name="p_entita"></param>
/// <returns></returns>
public void update(object p_entita, ISession pValue)
{
if (pValue == null)
{
//apro una sessione
using (ISession sessione = OpenSession())
{
sessione.Update(p_entita);
}
}
else
{
pValue.Update(p_entita);
}
}//end update(...)
//CANCELLAZIONE
/// <summary>
///
/// </summary>
/// <param name="p_entita"></param>
/// <returns></returns>
public void delete(object p_entita, ISession pValue)
{
if (pValue == null)
{
127
128
CAPITOLO 5. REALIZZAZIONE DEL PROTOTIPO OPERATIVO
//apro una sessione
using (ISession sessione = OpenSession())
{
sessione.Delete(p_entita);
}
}
else
{
pValue.Delete(p_entita);
}
}//end delete(...)
}
}
Capitolo 6
Confronto di prestazioni
In questo capitolo illustreremo un confronto in termini di tempo e scalabilità tra la
versione di query automatiche e la versione che usa Nhibernate.
I test di performance sono state effettuate nella seguente macchina:
• Nome Sistema Operativo: Microsoft Windows Vista Ultimate;
• Versione: 6.0.6001 Service Pack 1 Build 6001;
• Processore: Pentium(R) Dual-Core CPU E5200 @ 2.50GHz, 2520 Mhz, 2
core, 2 processori logici;
• Versione/data BIOS: American Megatrends Inc. V3.2C, 01/08/2008;
• Versione SMBIOS: 2.5;
• Memoria fisica installata - RAM: 4.00 GB;
I test che ho fatto hanno utilizzato i seguenti prodotti DBMS:
1. MYSQL 5.0
2. ORACLE 10g
3. SQLSERVER 2005
4. FIREBIRD 5.0
5. POSTGRES 8.5
Ma i test effettivi ovvero quelli veramente fatti sono state per MYSQL, ORACLE
e SQLSERVER 2005. Per FIREBIRD e POSTGRES ho riscontrato problematiche
legate alla connessione, forse legate alle nuove versioni dei due prodotti.
Per ogni DBMS ho eseguito i test di velocità nella versione query automatiche e
versione Nhibernate testando le seguenti operazioni:
• Metodo INSERT(HASH TABLE);
• Metodo INSERT(OGGETTO ENTITÀ);
• Metodo DELETE(HASH TABLE);
• Metodo DELETE(OGGETTO ENTITÀ);
• Metodo UPDATE(HASH TABLE);
• Metodo UPDATE(OGGETTO ENTITÀ);
129
130
CAPITOLO 6. CONFRONTO DI PRESTAZIONI
Figura 6.1: La tabella BUFFER usata per il test di performance
• Metodo SELECT(HASH TABLE);
• Metodo SELECT(OGGETTO ENTITÀ);
Per ogni metodo ho effettuato i test con numeri di record pari a: 10, 100, 1000,
5000, 10000, 50000, 100000, 500000, 1000000, 5000000 e 10000000. Il test consiste
nel prelievo della velocità del metodo per otto volte e si prende alla fine una media
aritmetica. Questo per ogni record. Per esempio: inserimento di 10 record, prelievo
della velocità nelle due versioni effettuando il test di velocità otto volte, calcolo la
media. Inserimento di 100 record e cosi via.
Tutti i test sono stati effettuati usando una tabella di nome BUFFER (vedi figura
6.1).
Test di performance su SQLSERVER 2005
Nelle varie operazioni (insert, update, delete, select) i grafici vengono costruiti interpolando i punti (numero di record, media aritmetica delle 8 prove). Cosi facendo ho
ottenuto delle spline del primo ordine. Nell’asse delle ascisse abbiamo il numero di
record mentre nell’ordinata abbiamo la media aritmetica espressa in secondi. Gli
andamenti delle due spline sono colorate di blu per la versione con query automatiche
e di rosso per la versione che usa Nhibernate.
Operazione INSERT
Nella figura 6.2 viene mostrato l’andamento delle due spline relative all’inserimento di record. Dal grafico si può notare che i due metodi scalano bene inizialmente e si
può dire che fino a un milione di record riescono ad andare quasi alla stessa velocità.
Si può notare che impiegano mediamente 770,00 secondi ad inserire un milione di
record. Ma ho notato un crash da parte di Nhibernate dopo aver inserito 3.471.900
record impiegandoci un tempo pari a 3338,841 secondi. Mentre scala benissimo la
versione con query automatiche, infatti nell’inserire 10 milioni di record ha impiegato
mediamente 7090,57 secondi.
Una considerazione in questo caso va fatta sulla gestione delle risorse, precisamente sulla gestione della memoria. Ho notato che usando la versione con query
131
Figura 6.2: Confronto di prestazioni relativo all’inserimento di record entro la
tabella Buffer
automatiche partendo da un utilizzo della memoria pari a 12 Mb resto attorno ai
14 Mb. Mentre nell’altra versione partendo da un utilizzo della memoria pari a 20
Mb continua a salire fino ad andare in crash, precisamente a 1.3 Gb. Deduco che
usare Nhibernate c’è un notevole spreco di risorse nel caso di inserimento dentro una
tabella.
Nelle quattro figure seguenti (figura 6.3) sono mostrati gli andamenti delle due
versioni in modo più dettagliato, ovvero si è scelto uno zoom sulle scale: [10, 1000],
[1000, 10000], [10000, 100000] e [100000,1000000].
Nella tabella che segue sono espresse le valutazioni sui grafici che sono stati ottenuti zoomando ogni fetta dell’andamento della figura 6.2. Precisamente ottengo le
seguenti valutazioni:
Operazione DELETE
Nella figura 6.4 viene mostrato l’andamento delle due spline relative alla cancellazione di record. Dal grafico si può notare che i due metodi scalano bene inizialmente
fino ad arrivare a circa 10000 record. Dopo di che la versione che usa Nhibernate è
più veloce rispetto alla versione che usa le query automatiche. Questo fino ad arrivare
a circa un milione di record. Alla fine ho notato un crash di Nhibernate a circa un
milione e rotti di record mentre l’altro continua a scalare bene.
Nelle quattro figure seguenti (figura 6.5) sono mostrati gli andamenti delle due
132
CAPITOLO 6. CONFRONTO DI PRESTAZIONI
Figura 6.3: Diversi tipi di Zoom relativi al grafico dell’inserimento
Zoom
Intervallo [10, 1000]
Intervallo [1000, 10000]
Intervallo [10000, 100000]
Intervallo [100000, 1000000]
Intervallo [1000000, 10000000]
GSS con query
automatiche
0,30 sec.
4,03 sec.
40,64 sec.
411,60 sec.
3732,28 sec.
GSS con uso
Nhibernate
0,35 sec.
3,7 sec.
37,82 sec.
443,30 sec.
crash
Tabella 6.1: Valutazioni delle prestazioni relative all’inserimento
133
Figura 6.4: Confronto di prestazioni relativo alla cancellazione di record entro
la tabella Buffer
versioni in modo più dettagliato, ovvero si è scelto uno zoom sulle scale: [10, 1000],
[1000, 10000], [10000, 100000] e [100000,1000000].
Nella tabella che segue sono espresse le valutazioni sui grafici che sono stati ottenuti zoomando ogni fetta dell’andamento della figura 6.4. Precisamente ottengo le
seguenti valutazioni:
Operazione UPDATE
Nella figura 6.6 viene mostrato l’andamento delle due spline relative all’aggiornamento di record. Dal grafico si può notare che i due metodi scalano bene inizialmente
fino ad arrivare a circa 100000 record. Dopo di che la versione che usa Nhibernate è
più veloce rispetto alla versione che usa le query automatiche. Questo fino ad arrivare
a circa un milione di record. Alla fine ho notato un crash di Nhibernate a circa un
milione e rotti di record mentre l’altro continua a scalare bene.
Nelle quattro figure seguenti (figura 6.7) sono mostrati gli andamenti delle due
versioni in modo più dettagliato, ovvero si è scelto uno zoom sulle scale: [10, 1000],
[1000, 10000], [10000, 100000] e [100000,1000000].
Nella tabella che segue sono espresse le valutazioni sui grafici che sono stati ottenuti zoomando ogni fetta dell’andamento della figura 6.6. Precisamente ottengo le
seguenti valutazioni:
Operazione SELECT
Nella figura 6.8 viene mostrato l’andamento delle due spline relative alla select di
record. Dal grafico si può notare che i due metodi scalano bene inizialmente fino ad
arrivare a circa 100000 record. Dopo di che la versione che usa le query automatiche
è più veloce rispetto alla versione che usa Nhibernate. Questo fino ad arrivare a circa
134
CAPITOLO 6. CONFRONTO DI PRESTAZIONI
Figura 6.5: Diversi tipi di Zoom relativi al grafico della cancellazione
Zoom
Intervallo [10, 1000]
Intervallo [1000, 10000]
Intervallo [10000, 100000]
Intervallo [100000, 1000000]
Intervallo [1000000, 10000000]
GSS con query
automatiche
0,19 sec.
3,00 sec.
31,21 sec.
313,05 sec.
3100,54 sec.
GSS con uso
Nhibernate
0,20 sec.
2,17 sec.
21,74 sec.
216,52 sec.
crash
Tabella 6.2: Valutazioni delle prestazioni relative alla cancellazione
135
Figura 6.6: Confronto di prestazioni relativo all’aggiornamento di record entro
la tabella Buffer
un milione di record. Alla fine ho notato un crash sempre di Nhibernate a circa un
milione e rotti di record mentre l’altro continua a scalare bene.
Nelle quattro figure seguenti (figura 6.9) sono mostrati gli andamenti delle due
versioni in modo più dettagliato, ovvero si è scelto uno zoom sulle scale: [10, 1000],
[1000, 10000], [10000, 100000] e [100000,1000000].
Nella tabella che segue sono espresse le valutazioni sui grafici che sono stati ottenuti zoomando ogni fetta dell’andamento della figura 6.8. Precisamente ottengo le
seguenti valutazioni:
Test di performance su MYSQL 5.0
Operazione INSERT
Nella figura 6.10 viene mostrato l’andamento delle due spline relative all’inserimento di record. Dal grafico si può notare che i due metodi scalano bene e hanno
inizialmente lo stesso andamento. Ma si può notare che ancora una volta la versione
con query automatiche è migliore in termini di velocità di inserimento Ho osservato
un crash da parte di tutte e due le versioni al record 100000. Questo è dovuto alla
seguente eccezione lanciata dall’applicativo: MYSQL for Away. Conclusione è colpa
del provider MYSQLOLEDB.
Nelle tre figure seguenti (figura 6.11) sono mostrati gli andamenti delle due versioni in modo più dettagliato, ovvero si è scelto uno zoom sulle scale: [10, 1000],
[1000, 10000] e [10000, 100000].
136
CAPITOLO 6. CONFRONTO DI PRESTAZIONI
Figura 6.7: Diversi tipi di Zoom relativi al grafico dell’aggiornamento
Zoom
Intervallo [10, 1000]
Intervallo [1000, 10000]
Intervallo [10000, 100000]
Intervallo [100000, 1000000]
Intervallo [1000000, 10000000]
GSS con query
automatiche
0,26 sec.
3,50 sec.
38,41 sec.
410,44 sec.
12432,58 sec.
GSS con uso
Nhibernate
0,21 sec.
3,00 sec.
21,50 sec.
212,39 sec.
crash
Tabella 6.3: Valutazioni delle prestazioni relative all’aggiornamento
137
Figura 6.8: Confronto di prestazioni relativo alla selezione di record entro la
tabella Buffer
Nella tabella che segue sono espresse le valutazioni sui grafici che sono stati ottenuti zoomando ogni fetta dell’andamento della figura 6.10. Precisamente ottengo
le seguenti valutazioni:
Operazione DELETE
Nella figura 6.12 viene mostrato l’andamento delle due spline relative alla cancellazione di record. Dal grafico si può notare che i due metodi scalano bene inizialmente
fino ad arrivare a circa 10000 record. Dopo di che la versione che usa query automatiche è più veloce rispetto alla versione che usa Nhibernate. Alla fine ho notato un
crash sia di Nhibernate e sia di query automatiche. La causa di ciò è stato spiegato
prima.
Nelle tre figure seguenti (figura 6.13) sono mostrati gli andamenti delle due versioni in modo più dettagliato, ovvero si è scelto uno zoom sulle scale: [10, 1000],
[1000, 10000] e [10000, 100000].
Nella tabella che segue sono espresse le valutazioni sui grafici che sono stati ottenuti zoomando ogni fetta dell’andamento della figura 6.12. Precisamente ottengo
le seguenti valutazioni:
Operazione UPDATE
Nella figura 6.14 viene mostrato l’andamento delle due spline relative all’aggiornamento di record. Dal grafico si può notare che i due metodi scalano bene inizialmente
fino ad arrivare a circa 30000 record. Dopo di che la versione che usa le query automatiche è più veloce rispetto alla versione che usa Nhibernate. Alla fine ho notato un
crash sia di Nhibernate e sia di query automatiche. La causa di ciò è stato spiegato
prima.
Nelle tre figure seguenti (figura 6.15) sono mostrati gli andamenti delle due ver-
138
CAPITOLO 6. CONFRONTO DI PRESTAZIONI
Figura 6.9: Diversi tipi di Zoom relativi al grafico della select
Zoom
Intervallo [10, 1000]
Intervallo [1000, 10000]
Intervallo [10000, 100000]
Intervallo [100000, 1000000]
Intervallo [1000000, 10000000]
GSS con query
automatiche
0,01 sec.
0,11 sec.
1,13 sec.
12,12 sec.
120,60 sec.
GSS con uso
Nhibernate
0,02 sec.
0,15 sec.
1,72 sec.
19,28 sec.
crash
Tabella 6.4: Valutazioni delle prestazioni relative alla select
139
Figura 6.10: Confronto di prestazioni relativo all’inserimento di record entro
la tabella Buffer
Figura 6.11: Diversi tipi di Zoom relativi al grafico dell’inserimento
140
CAPITOLO 6. CONFRONTO DI PRESTAZIONI
Zoom
Intervallo [10, 1000]
Intervallo [1000, 10000]
Intervallo [10000, 100000]
Intervallo [100000, 1000000]
Intervallo [1000000, 10000000]
GSS con query
automatiche
13,04 sec.
191,91 sec.
1934,50 sec.
crash
crash
GSS con uso
Nhibernate
13,83 sec.
211,82 sec.
2045,31 sec.
crash
crash
Tabella 6.5: Valutazioni delle prestazioni relative all’inserimento
Figura 6.12: Confronto di prestazioni relativo alla cancellazione di record entro
la tabella Buffer
sioni in modo più dettagliato, ovvero si è scelto uno zoom sulle scale: [10, 1000],
[1000, 10000] e [10000, 100000].
Nella tabella che segue sono espresse le valutazioni sui grafici che sono stati ottenuti zoomando ogni fetta dell’andamento della figura 6.14. Precisamente ottengo
le seguenti valutazioni:
Operazione SELECT
Nella figura 6.16 viene mostrato l’andamento delle due spline relative alla select
di record. Dal grafico si può notare sostanzialmente che i due andamenti scalano alla
stessa velocità circa fino a record 50000. Dopo di che la versione che usa Nhibernate è
più veloce rispetto alla versione che usa le query automatiche. Alla fine ho notato un
crash sia di Nhibernate e sia di query automatiche. La causa di ciò è stato spiegato
prima.
Nelle tre figure seguenti (figura 6.17) sono mostrati gli andamenti delle due versioni in modo più dettagliato, ovvero si è scelto uno zoom sulle scale: [10, 1000],
[1000, 10000] e [10000, 100000].
Nella tabella che segue sono espresse le valutazioni sui grafici che sono stati ot-
141
Figura 6.13: Diversi tipi di Zoom relativi al grafico della cancellazione
Zoom
Intervallo [10, 1000]
Intervallo [1000, 10000]
Intervallo [10000, 100000]
Intervallo [100000, 1000000]
Intervallo [1000000, 10000000]
GSS con query
automatiche
14,10 sec.
192,74 sec.
1883,86 sec.
crash
crash
GSS con uso
Nhibernate
13,13 sec.
199,72 sec.
2023,45 sec.
crash
crash
Tabella 6.6: Valutazioni delle prestazioni relative alla cancellazione
tenuti zoomando ogni fetta dell’andamento della figura 6.16. Precisamente ottengo
le seguenti valutazioni:
Test di performance su ORACLE 10g
Operazione INSERT
Nella figura 6.18 viene mostrato l’andamento delle due spline relative all’inserimento di record. Dal grafico si può notare che i due metodi scalano bene e hanno
inizialmente lo stesso andamento. Ma si può notare che ancora una volta la versione
con query automatiche è migliore in termini di velocità di inserimento. Ho osservato
un crash da parte di tutte e due le versioni al record 100000. Questo è dovuto ad un
eccezione di Oracle.
Nelle tre figure seguenti (figura 6.19) sono mostrati gli andamenti delle due versioni in modo più dettagliato, ovvero si è scelto uno zoom sulle scale: [10, 1000],
[1000, 10000] e [10000, 100000].
142
CAPITOLO 6. CONFRONTO DI PRESTAZIONI
Figura 6.14: Confronto di prestazioni relativo all’aggiornamento di record
entro la tabella Buffer
Figura 6.15: Diversi tipi di Zoom relativi al grafico dell’aggiornamento
143
Zoom
Intervallo [10, 1000]
Intervallo [1000, 10000]
Intervallo [10000, 100000]
Intervallo [100000, 1000000]
Intervallo [1000000, 10000000]
GSS con query
automaticheb
12,96 sec.
194,63 sec.
1840,62 sec.
crash
crash
GSS con uso
Nhibernate
13,22 sec.
202,35 sec.
1958,32 sec.
crash
crash
Tabella 6.7: Valutazioni delle prestazioni relative all’aggiornamento
Figura 6.16: Confronto di prestazioni relativo alla selezione di record entro la
tabella Buffer
Figura 6.17: Diversi tipi di Zoom relativi al grafico della select
144
CAPITOLO 6. CONFRONTO DI PRESTAZIONI
Zoom
Intervallo [10, 1000]
Intervallo [1000, 10000]
Intervallo [10000, 100000]
Intervallo [100000, 1000000]
Intervallo [1000000, 10000000]
GSS con query
automatiche
0,02 sec.
0,25 sec.
3,29 sec.
crash
crash
GSS con uso
Nhibernate
0,03 sec.
0,35 sec.
2,69 sec.
crash
crash
Tabella 6.8: Valutazioni delle prestazioni relative alla select
Figura 6.18: Confronto di prestazioni relativo all’inserimento di record entro
la tabella Buffer
Figura 6.19: Diversi tipi di Zoom relativi al grafico dell’inserimento
145
Zoom
Intervallo [10, 1000]
Intervallo [1000, 10000]
Intervallo [10000, 100000]
Intervallo [100000, 1000000]
Intervallo [1000000, 10000000]
GSS con query
automatiche
2,17 sec.
30,32 sec.
290,27 sec.
crash
crash
GSS con uso
Nhibernate
3,14 sec.
40,63 sec.
389,47 sec.
crash
crash
Tabella 6.9: Valutazioni delle prestazioni relative all’inserimento
Figura 6.20: Confronto di prestazioni relativo alla cancellazione di record entro
la tabella Buffer
Nella tabella che segue sono espresse le valutazioni sui grafici che sono stati ottenuti zoomando ogni fetta dell’andamento della figura 6.18. Precisamente ottengo
le seguenti valutazioni:
Operazione DELETE
Nella figura 6.20 viene mostrato l’andamento delle due spline relative alla cancellazione di record. Dal grafico si può notare che i due metodi scalano bene e hanno
circa lo stesso andamento. Ma si può notare che la versione con Nhibernate è un
tantino migliore in termini di velocità di cancellazione.
Nelle tre figure seguenti (figura 6.21) sono mostrati gli andamenti delle due versioni in modo più dettagliato, ovvero si è scelto uno zoom sulle scale: [10, 1000],
[1000, 10000] e [10000, 100000].
Nella tabella che segue sono espresse le valutazioni sui grafici che sono stati ottenuti zoomando ogni fetta dell’andamento della figura 6.20. Precisamente ottengo
le seguenti valutazioni:
Operazione UPDATE
Nella figura 6.22 viene mostrato l’andamento delle due spline relative all’aggiornamento di record. Dal grafico si può notare che i due metodi scalano bene e hanno
146
CAPITOLO 6. CONFRONTO DI PRESTAZIONI
Figura 6.21: Diversi tipi di Zoom relativi al grafico della cancellazione
Zoom
Intervallo [10, 1000]
Intervallo [1000, 10000]
Intervallo [10000, 100000]
Intervallo [100000, 1000000]
Intervallo [1000000, 10000000]
GSS con query
automatiche
1,52 sec.
20,95 sec.
201,55 sec.
crash
crash
GSS con uso
Nhibernate
1,34 sec.
18,43 sec.
189,77 sec.
crash
crash
Tabella 6.10: Valutazioni delle prestazioni relative alla cancellazione
circa lo stesso andamento. Ma si può notare che la versione con Nhibernate è un
tantino migliore in termini di velocità di aggiornamento.
Nelle tre figure seguenti (figura 6.23) sono mostrati gli andamenti delle due versioni in modo più dettagliato, ovvero si è scelto uno zoom sulle scale: [10, 1000],
[1000, 10000] e [10000, 100000].
Nella tabella che segue sono espresse le valutazioni sui grafici che sono stati ottenuti zoomando ogni fetta dell’andamento della figura 6.22. Precisamente ottengo
le seguenti valutazioni:
Operazione SELECT
Nella figura 6.24 viene mostrato l’andamento delle due spline relative alla select
di record. Dal grafico si può notare che i due metodi hanno circa lo stesso andamento
fino a 5000 record e rotti. Poi gli andamenti si diversificano in maniera abbastanza
grossa e la migliore in termini di velocità di selezione è la versione con le query
automatiche.
Nelle tre figure seguenti (figura 6.25) sono mostrati gli andamenti delle due ver-
147
Figura 6.22: Confronto di prestazioni relativo all’aggiornamento di record
entro la tabella Buffer
Figura 6.23: Diversi tipi di Zoom relativi al grafico dell’aggiornamento
Zoom
Intervallo [10, 1000]
Intervallo [1000, 10000]
Intervallo [10000, 100000]
Intervallo [100000, 1000000]
Intervallo [1000000, 10000000]
GSS con query
automatiche
1,57 sec.
20,84 sec.
213,53 sec.
crash
crash
GSS con uso
Nhibernate
1,37 sec.
19,98 sec.
200,45 sec.
crash
crash
Tabella 6.11: Valutazioni delle prestazioni relative all’aggiornamento
148
CAPITOLO 6. CONFRONTO DI PRESTAZIONI
Figura 6.24: Confronto di prestazioni relativo alla selezione di record entro la
tabella Buffer
Figura 6.25: Diversi tipi di Zoom relativi al grafico della select
sioni in modo più dettagliato, ovvero si è scelto uno zoom sulle scale: [10, 1000],
[1000, 10000] e [10000, 100000].
Nella tabella che segue sono espresse le valutazioni sui grafici che sono stati ottenuti zoomando ogni fetta dell’andamento della figura 6.24. Precisamente ottengo
le seguenti valutazioni:
Conclusioni finali sulle prestazioni
Nella tabella che segue sono espresse le velocità in secondi delle operazioni (INSERT,
DELETE UPDATE e SELECT) relative alla versione con query automatiche. È stato
149
Zoom
Intervallo [10, 1000]
Intervallo [1000, 10000]
Intervallo [10000, 100000]
Intervallo [100000, 1000000]
Intervallo [1000000, 10000000]
GSS con query
automatiche
0,04 sec.
0,32 sec.
3,15 sec.
crash
crash
GSS con uso
Nhibernate
0,06 sec.
0,55 sec.
5,67 sec.
crash
crash
Tabella 6.12: Valutazioni delle prestazioni relative alla select
DBMS
SQLSERVER
MYSQL
ORACLE
INSERT
40,64 sec.
1934,50 sec.
290,27 sec.
DELETE
31,21 sec.
1883,86 sec.
201,53 sec.
UPDATE
38,41 sec.
1840,62 sec.
213,53 sec.
SELECT
1,13 sec.
3,29 sec.
3,15 sec.
Tabella 6.13: Confronto velocità in sec. tra DBMS e utilizzo GSS con query
automatiche nel caso di 100000 record
DBMS
SQLSERVER
MYSQL
ORACLE
INSERT
37,82 sec.
2045,31 sec.
389,47 sec.
DELETE
21,74 sec.
2023,45 sec.
189,77 sec.
UPDATE
21,50 sec.
1958,32 sec.
200,45 sec.
SELECT
1,72 sec.
2,69 sec.
5,67 sec.
Tabella 6.14: Confronto velocità in sec. tra DBMS e utilizzo GSS con uso di
Nhibernate nel caso di 100000 record
scelto un confronto su 100000 record.
Come si può notare il DBMS che è più scalabile e che impiega il minor tempo è
SQLSERVER. Poi segue ORACLE e infine MYSQL.
Nella tabella che segue sono espresse le velocità in secondi delle operazioni (INSERT, DELETE UPDATE e SELECT) relative alla versione con uso di Nhibernate.
Anche qui è stato scelto un confronto su 100000 record.
Come si può notare anche usando la versione Nhibernate, SQLSERVER rimane
il DBMS più scalabile e che impiega il minor tempo.
Capitolo 7
Conclusioni
In questo capilo finale si tirerrano le somme sul lavoro svolto e le possibile espansioni
future.
7.1
Bilancio del lavoro svolto
L’utilizzo di un framework .NET ha permesso di lavorare con l’accesso alle base di
dati in maniera molto semplice e a mio avviso ben pulita. Nel complesso il progetto
Generatore Strati Software è risultato un ottimo generatore automatico di ingredienti
per crearsi un piccolo ORM fatto in casa. Sono risultati molto comodi durante la fase
di implementazione del progetto l’utilizzo di design pattern: quali MVC, ecc. A mio
avviso se uno dovesse chiedersi: ma è meglio usare il Generatore Strati Software nella
versione con le query automatiche o con la versione Nhibernate, io gli risponderei in
questo modo:
Dal punto di vista dello sviluppatore è preferibile usare l’accoppiata: Poco e
Nhibernate, poichè per interagire con una base di dati utilizzi solo ed esclusivamente
l’oggetto entità e non scrivi nessuna riga di SQL. Ma come abbiamo visto nel capitolo precedente nei test di performance, la versione con le query automatiche è di
gran lunga la più veloce e per certi versi la più scalabile. Quindi dal punto di vista
dell’utente è preferibile usare il Generatore con la versione query automatiche.
7.2
Esperienze e conoscenze acquisite
Durante i mesi di stage presso l’azienda, mi sono occupato dell’Analisi, progettazione
e implementazione del mio Generatore Strati Software. Ho imparato un nuovo linguaggio: il C# e ho acquisito una certa esperienza professionale e di qualità nello
sviluppare il software. Un dovereso grazie al Prof. Ing. Giulio Destri e un ringraziamento anche all’Ing. Alberto Picca per avermi fatto capire come si lavora a un
livello professionale e il come bisogna essere bravi nel documentare tutto e al meglio.
Molto spesso si ignora l’importanza che riveste la documentazione del software. Ho
riscontrato che se tu lavori con metodo, ovvero documenti tutto e in maniera precisa
e cerchi di organizzare al meglio tutto il lavoro, impieghi minor tempo e cosi facendo
puoi ottenere un software di qualità.
7.3
Espansioni future
Le possibili espansioni future sono:
1. Creare una versione Web del progetto;
150
7.3. ESPANSIONI FUTURE
151
2. Gestione di relazioni tra le entità di un database complesse (es. relazioni
ricorsive ecc.);
3. Gestione delle chiavi primarie multiple;
4. Maggiore tolleranza agli errori del caricamento del template (es. uso delle
espressioni regolari);
5. Test di performance più mirati (per esempio test sull’inserimento a cascata,
tenere conto delle relazioni tra le tabelle, ecc.).
Bibliografia
[1]
Stefano Paraboschi Riccardo Torlone Paolo Atzeni, Stefano Ceri. Basi di dati.
Modelli e linguaggi di interrogazione. McGraw-Hill, Via Ripamonti, 89 - 20139
Milano, 2 edition, 2002.
[2]
Craig Larman. Applying UML and Patterns. An introduction to Object-Oriented
Analysis and Design and the Unified Process. Prentice Hall, 2 edition, 2002.
[3]
M. De Marco. Sistemi Informativi Aziendali. Franco Angeli Edizioni, 2000.
[4]
A. Mills G. Bellinger, D. Castro. Data, Information, Knowledge, and Wisdom.
1994.
[5]
N. Shedroff. Information Interaction Design: A Unified Field Theory of Design.
1994.
[6]
Giulio Destri. Introduzione ai sistemi informativi aziendali. Monte Università
di Parma, Borgo Bruno Longhi, 10 - 43100 Parma, 1 edition, 2007.
[7]
Thomas Grechenig Monika Köhle Wolfgang Zuser, Stefan Biffl. Ingegneria del
software con UML e Unified Process. McGraw-Hill, Via Ripamonti, 89 - 20139
Milano, 1 edition, 2004.
[8]
J. Widom H.G. Molina, J.D. Ullman. Database Systems. The complete book.
Prentice Hall, Upper Saddle River, New Jersey 07458, 1 edition, 2002.
[9]
ANSI/X3/SPARC. Study Group on Database Management System. Interim
Report- ACM FDT Bullettin. 1975.
[10] A. Klug D.C. Tsichritzis. The ANSI/X3/SPARC DBMS Framework Report of
the study Group on Database Management Systems. 1978.
[11] L.D. Martino E. Bertino. Sistemi di basi di dati orientate agli oggetti. AddisonWesley Masson, 1992.
[12] B. Meyer. Object-Oriented software construction. Prentice Hall, Upper Saddle
River, New Jersey 07458, 2 edition, 1997.
[13] R.Johnson J. Vlissides E. Gamma, R. Helm. Design Patterns: Elements of
Reusable Object Oriented Software. Addison-Wesley Masson, 1995.
[14] B.G. Whitenack K. Brown. A Pattern Language for Object-RDBMS Integration.
The static Pattern. Technical Journal Knowledge Systems Corporation.
[15] D. Moore M. Stonebraker. Object-Relation DBMSs, The next great wave. Morgan
Kauffman, San Francisco CA, 1996.
[16] Design Pattern MVC. http://ootips.org/mvc-pattern.html.
152
153
[17] Sun
Microsystems,
Inc.
Design
http://java.sun.com/blueprints/patterns/MVC.html.
pattern
MVC.
[18] Garlan David Shaw, Mary. Software Architecture: Perspectives on a emerging
discipline. Prentice Hall, Upper Saddle River, New Jersey 07458, 1996.
[19] Keith Bennet. Legacy Systems. IEEE Software, 1995.
[20] Alan Bertossi. Algoritmi e strutture di dati. UTET Libreria, Via Ormea, 75 10125 Torino, 2004.
[21] J. Lajoie Stanley B. Lippman. C++ : Corso di programmazione. Addison-Wesley
Masson, 3 edition, 2000.
[22] D.A. Chappell. Enterprise Service Bus. O’Reilly, 2004.
[23] R. Verganti E. Bartezzaghi, G. Spina. Organizzare le PMI per la crescita. Come
sviluppare i più avanzati modelli organizzativi: gestione per processi, lavoro per
progetti, sviluppo delle competenze. Ed. Il Sole 24 Ore, 1999.
[24] V.E. Miller M.E. Porter. How information gves you competitive advantage.
Harward Business Review, 1985.
[25] R. Anthony. Planning and control systems: A framework for analysis. Harward
Business Review, 1965.
[26] J. Laudon K. Laudon. Management dei sistemi informativi. Pearson Education
Italia, Milano, 2004.
[27] Giulio Destri. Dispense per il corso di Ingegneria del Software: DOTNET. Giulio
Destri, 2004.
[28] C. Bauer G. King Pierre Henri Kuaté, T. Harris. Nhibernate in action. MEAP,
2008.