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.