UNIVERSITÀ DEGLI STUDI DI FIRENZE FACOLTÀ DI INGEGENRIA – DIPARTIMENTO DI SISTEMI E INFORMATICA ___________________ HIBERNATE OPEN SOURCE OBJECT/RELATIONAL MAPPING IMPLEMENTATION FOR JAVA PERSISTENCE Jacopo Torrini – [email protected] Laboratorio di tecnologie del software www.stlab.dsi.unifi.it Hibernate 2 Indice generale 1 Introduzione......................................................................................................................................4 2 La persistenza in Java....................................................................................................................... 6 2.1 Persistenza nelle applicazioni object-oriented.......................................................................... 7 2.2 Paradigm mismatch...................................................................................................................7 2.2.1 Problema della granularità................................................................................................ 7 2.2.2 Problema dei sottotipi....................................................................................................... 8 2.2.3 Problema dell'identità........................................................................................................8 2.2.4 Problema delle associazioni.............................................................................................. 8 2.2.5 Problema della navigabilità delle associazioni..................................................................8 2.2.5.1 Il problema delle n+1 select.......................................................................................9 2.3 ORM......................................................................................................................................... 9 3 Introduzione ad Hibernate.............................................................................................................. 11 3.1 Un esempio di applicazione con Hibernate.............................................................................12 3.2 Mapping di una classe con una tabella di un database............................................................14 3.3 Configurazione di Hibernate...................................................................................................14 4 Mapping delle classi persistenti...................................................................................................... 17 4.1 Associazioni tra entità............................................................................................................. 18 4.1.1 Associazioni many-to-one................................................................................................18 4.1.2 Associazioni one-to-many............................................................................................... 20 4.1.3 Associazioni bidirezionali............................................................................................... 22 4.2 Ereditarietà..............................................................................................................................23 4.2.1 Table per concrete class...................................................................................................23 4.2.2 Table per class hierarchy................................................................................................. 24 4.2.3 Table per subclass........................................................................................................... 24 4.3 Generazione automatica del database..................................................................................... 24 5 Persistenza con Hibernate...............................................................................................................25 5.1 Persistence Manager................................................................................................................26 5.2 Perisitence lifecycle................................................................................................................ 26 5.3 Identità degli oggetti............................................................................................................... 26 5.4 Persistence Manager............................................................................................................... 26 5.5 Persistenza transitiva...............................................................................................................27 6 Interrogazioni con Hibernate..........................................................................................................29 6.1 Recuperare oggetti per Id........................................................................................................ 30 6.2 Interrogazioni con HQL..........................................................................................................30 7 Un esempio pratico......................................................................................................................... 32 7.1 Installazione dei prerequisiti....................................................................................................33 7.1.1 Java...................................................................................................................................33 7.1.2 Eclipse..............................................................................................................................33 7.1.3 Hibernate..........................................................................................................................33 7.1.4 HSQLDB......................................................................................................................... 33 7.2 Creazione di un nuovo progetto...............................................................................................34 7.3 Creazione delle classi del dominio..........................................................................................35 7.4 Creazione del file di configurazione di Hibernate.................................................................. 37 7.5 Creazione dei file di mapping................................................................................................. 38 7.6 Utilizzo di Hibernate in una classe Test.................................................................................. 38 8 Appendice: lo standard JPA............................................................................................................40 3 Capitolo 1 Introduzione 5 Capitolo 1 Introduzione La persistenza è uno dei concetti fondamentali nello sviluppo di applicazioni sia web che desktop. L'approccio utilizzato nella gestione di dati persistenti rappresenta una decisione chiave che impatta radicalmente sui tempi di sviluppo, la portabilità e la manutenibilità dell'applicazione. Quando si parla di persistenza con Java, si intende normalmente la memorizzazione di dati su un database relazionale con l'utilizzo del linguaggio SQL tramite un'insieme di interfacce standard. Se da un lato questo approccio permette di utilizzare convenientemente tutte le potenzialità offerte dai database relazionali, accedendo anche a quelle funzioni proprietarie che li rendono più performanti o più adatti al particolare caso d'uso, dall'altro introduce una serie di problematiche che vanno dall'obbligo di scrivere ogni query, persino per le operazioni più elementari CRUD (Create Read Update e Delete) col linguaggio SQL, la non portabilità su altri database relazionali dovuta ai vari dialetti di questi ultimi ed infine la differenza tra la rappresentazione di dati del mondo object-oriented e quella del mondo relazionale. A proposito dell'ultimo problema Martin Fowler in [POEAA] propone un'insieme di pattern architetturali per il mapping tra i database relazionali e linguaggi object-oriented. In seguito Gaving King e Christian Bauer forniscono un'implementazione per Java di questi concetti, dando vita al progetto open source Hibernate ([HIA], [JPWH]). 2 La persistenza in Java 7 Capitolo 2 La persistenza in Java In Java l'accesso ad un database relazionale viene normalmente effettuato tramite le API JDBC (Java Database Connectivity). L'SQL può essere scritto a mano direttamente nel codice Java, o può essere generato al volo durante l'esecuzione del programma. Le operazioni possibili sono operazioni di basso livello, quali l'esecuzione di una query, la valorizzazione dei parametri della query, la possibilità di scorrere tra i risultati dell'interrogazione o leggere il valore dei suoi campi. Nelle applicazioni in ambito Enterprise l'interesse maggiore si concentra soprattutto sul problema della modellazione dei dati e della business logic. In questi ambiti applicativi il codice di accesso ai dati risulta spesso un'operazione meccanica e tediosa, prona ad errori e di difficile manutenibilità. Quello che serve è la possibilità di persistere grafi complessi di oggetti senza occuparsi degli aspetti implementativi di basso livello. Perché allora utilizzare un database relazionale? I database relazionali dominano il mercato della gestione della persistenza dei dati, e di solito sono un requisito e non una scelta progettuale. Sono molti anni che vengono studiati e ottimizzati, rendendoli strumenti performanti e affidabili. Inoltre la stessa base di dati può essere utilizzata da più applicazioni scritte con linguaggi differenti non necessariamente object-oriented. 2.1 Persistenza nelle applicazioni object-oriented Nelle applicazioni object-oriented la persistenza permette agli oggetti di vivere oltre i confini del processo che li ha creati, e gli oggetti possono essere salvati sul disco e ripristinati su richiesta. In questo ambito applicativo, soprattutto quando si utilizza un domain model ([POEAA]), lo stato dell'applicazione è definito da grafi di oggetti tra loro interconnessi. Un'applicazione non lavora quindi direttamente con una rappresentazione tabulare dei suoi dati, contrariamente a quello che succede in una base di dati relazionale, la logica applicativa risiede completamente nella parte Java e non nelle stored procedures del database e viene fatto uso di concetti complessi come l'ereditarietà, il polimorfismo e la composizione. Di contro le operazioni SQL, quali le proiezioni o le join, hanno come risultato rappresentazioni tabellari dei dati. Le differenze presentate vengono comunemente definite dal termine paradigm mismatch, e meritano un'analisi dettagliata. Nel seguito verranno presentate solamente le problematiche derivanti dal paradigm mismatch, mentre nei prossimi capitoli saranno descritte le soluzioni che Hibernate propone. 2.2 2.2.1 Paradigm mismatch Problema della granularità Nel cercare di far corrispondere proprietà di un oggetto Java con colonne di un database relazionale si deve affrontare il problema della differenza di granularità delle due rappresentazioni. Se ad esempio si vuole persistere un oggetto Address, attributo della classe User, su una tabella USERS, è molto probabile che al singolo oggetto Address corrispondano più colonne della tabella USERS, quali la via, il cap, il numero civico ecc. Nonostante che i database relazionali supportino gli user-defined datatypes (UDT), che permettono di creare anche dalla parte relazionale tipi di dato strutturati come Address, l'uso di queste funzionalità rende non molto portabile l'applicazione. Hibernate 2.2.2 8 Problema dei sottotipi In Java, come in tutti i linguaggi object-oriented, l'ereditarietà permette di definire una classe a partire da una superclasse: la sottoclasse eredita attributi e metodi della superclasse. Inoltre un'associazione può essere polimorfica: se ad esempio un attributo di una classe è di tipo A, si può assegnare a tale attributo un'istanza una classe B sottoclasse di A. Nonostante alcuni database definiscano il concetto di type ineritance, la maggior parte di essi non permette l'uso di tabelle che ereditano i campi da altre tabelle. In generale comunque un database non può definire un'associazione polimorfica, in quanto una foreign key viene definita tra un campo di una tabella e una e una sola tabella target. 2.2.3 Problema dell'identità Quando due oggetti o due record di una tabella devono essere confrontati per determinare se sono identitci, esistono approcci diversi tra gli oggetti Java e i record di un database. In Java abbiamo il concetto di identity, quando due riferimenti sono associati alla stessa istanza di oggetto, e di equality, quando due istanze differenti possono essere considerate uguali per valore (ad esempio il confronto tra due stringhe). Nei database relazionali l'identità di una riga è specificata direttamente dalla chiave primaria della tabella. Spesso la chiave primaria non rappresenta nemmeno un valore caratterizzante la riga, ma serve esclusivamente per individuare in modo univoco il record (chiave surrogata). 2.2.4 Problema delle associazioni Nei linguaggi object oriented le associazioni tra entità sono implementate con le object references. Ma nel mondo relazionale un associazione è rappresentata da una foreign key. I riferimenti sono per loro natura direzionali, per cui se si necessita di bidirezionalità nell'associazione, è necessario implementare due riferimenti incrociati, uno per oggetto. Le foreign key invece non hanno direzionalità, rappresentano solo un legame tra record di due tabelle. Per quanto rigurarda la molteplicità delle associazioni, esiste un enorme differenza tra le due rappresentazioni: se da un lato non è semplice determinare la molteplicità di un associazione di una classe Java (ad esempio distinguere one-to-one da many-to-one), differentemente dalle associazioni di un database relazionale, nel mondo object-oriented le associazioni possono essere più complesse: associazioni one-to-many o many-to-many, rappresentate da una collezione di riferimenti ad oggetti. Nei database relazionali queste associazioni non sono possibili o almeno hanno una rappresentazione differente: le associazioni one-to-many non possono essere implementate, devono essere ribaltate come associazioni many-to-one, poiché non è possibile avere un campo che rappresenta una collezione di chiavi esterne. Inoltre le associazioni many-to-many devono essere implementate con una tabella aggiuntiva di associazione. 2.2.5 Problema della navigabilità delle associazioni Come detto in precedenza, un'istanza di un oggetto Java ha di solito associazioni ad istanze di altri oggetti. Queste associazioni creano un grafo di oggetti strettamente interconnessi. Tramite l'uso di metodi getter dell'oggetto è possibile ottenere le istanze degli oggetti associati, permettendo in pratica la navigabilità tra gli oggetti del grafo. 9 Capitolo 2 La persistenza in Java Tutto questo non ha un corrispondente nel mondo relazionale. Quando si esegue una query, si devono stabilire i confini di estrazione dei dati, inserendo nell'interrogazione le join necessarie tra le tabelle da cui estrarre i dati. Una volta eseguita, la query non è modificabile nella struttura per cui non è possibile estendere la ricerca ad altre tabelle se non rieseguendo una nuova query. Un sistema che si prefigge di creare un mapping tra il mondo object-oriented e quello relazionale deve risolvere alcuni meccanismi di interrogazione automatica durante l'esplorazione del grafo degli oggetti. Poiché non è pensabile che il database venga completamente caricato in memoria all'inizializzazione dell'applicazione, è chiaro che il grafo degli oggetti, il cui stato è persistito nel database, deve essere in qualche modo limitato e confinato. Solo durante l'esplorazione delle associazioni, in maniera automatica, devono essere fatte nuove interrogazioni al database per inizializzare rami del grafo di oggetti (ed effettivamente Hibernate fa questo). Sebbene questo meccanismo sembri allettante, nasconde un problema di efficienza piuttosto grosso, problema che, se non affrontato, rischia di rendere estremamente inefficiente un'applicazione. Il problema è conosciuto col nome di n+1 select problem 2.2.5.1 Il problema delle n+1 select Si supponga che un oggetto Parent abbia un'associazione uno a molti con una classe Child. Nel caso in cui si debbano richiedere n oggetti di tipo Parent, il numero di query necessarie per avere gli oggetti completamente inizializzati è pari a n+1, di cui: 1 query è necessaria per ottenere le istanze degli oggetti Parent, mentre n servono per inizializzare ciascuna collezione di Child contenuta in ogni Parent. Se si dovesse pensare all'equivalente operazione fatta in un database relazionale, basterebbe una singola query che mette in join il parent e il child per avere tutti i dati richiesti. 2.3 ORM Object Relational Mapping è il sistema di persistenza automatico e trasparente di oggetti in tabelle di un database relazionale. Ogni oggetto viene persistito nel database tramite l'inserimento di nuovi record i cui campi contengono i valori degli attributi dell'oggetto. Di solito ad ogni oggetto corrisponde un record di una particolare tabella associata alla classe dell'oggetto. L'associazione tra la classe e la tabella viene ottenuta tramite l'utilizzo di file di descrizione (file di mapping), in cui si specificano le modalità di mapping tra gli attributi dell'oggetto e i campi della tabella. Ogni interrogazione viene effettuata utilizzando un linguaggio simil-SQL che permette di scrivere query utilizzando il nome delle classi e degli attributi. Le interrogazioni vengono convertite dal tool ORM in istruzioni SQL da eseguire sul database relazionale sottostante. I resultset delle interrogazioni vengono convertiti nei corrispondenti oggetti in maniera del tutto trasparente. Dal punto di vista dello sviluppatore uno strumento ORM permette di effettuare delle richieste di istanze di particolari classi, con filtri basati sulle proprietà delle classi, i cui risultati sono liste di oggetti. Tutti i dettagli sottostanti al livello object-oriented sono praticamente nascosti. • In definitiva una soluzione ORM consiste in: API per eseguire le operazioni di base (CRUD) sugli oggetti delle classi persistenti Hibernate • • • • • • • 10 Un linguaggio per la costruzione di query sulle classi e le proprietà delle classi Un sistema per specificare il mapping tramite metadata Un sistema interno per l'interazione con oggetti transazionali, per il dirty checking, il fetching delle associazioni lazy. I benefici che si hanno nell'utilizzo di una soluzione ORM sono svariati: Produttività: il codice che si occupa della persistenza dei dati è la parte forse più tediosa di un'applicazione. Un tool ORM elimina molto di questo codice, semplificandolo e automatizzandolo al massimo Manutenibilità: dovendo scrivere meno codice è più facile manutenere l'applicazione. Inoltre in soluzioni fatte “a mano”, che trasformano il modello ad oggetti in query relazionali e record di database in oggetti, è molto più difficile far variare il modello di dominio insieme al modello relazionale. Con un tool ORM si mantengono separati i due modelli tramite l'uso di uno strato intermedio di mapping, che spesso minimizza anche la propagazione di variazione tra i due modelli. Performance: anche se a prima vista sembra che lo strato aggiuntivo di mapping introduca dei perggioramenti alla performace, ci sono talmente tante opzioni di ottimizzazioni (utilizzo di cache di primo e secondo livello, utilizzo di batch query ecc) che in realtà, in applicazioni con un numero elevato di accessi in lettura, è possibile ottenere delle performance estremamente più efficienti di una semplice soluzione basata su SQL. Indipendenza dal tipo di database: questa è una delle caratteristiche più interessanti di uno strumento di ORM, il quale astrae l'applicazione dal sottostante database SQL e dal suo dialetto. ORM introduce un linguaggio proprietario per le interrogazioni e le operazioni CRUD. Questo linguaggio viene poi convertito automaticamente nell'SQL del database sottostante. Cambiano un parametro nella configurazione dell'ORM, è possibile portare l'applicazione su altri database senza praticamente toccare una riga di codice. Inoltre l'ORM conosce molto bene (spesso molto meglio del programmatore) il dialetto del particolare database, e ottimizza le query utilizzando quanto più possibile funzioni SQL proprietarie. 3 Introduzione ad Hibernate Hibernate 12 Hibernate cerca di essere una soluzione completa al problema della gestione di dati persistenti in Java. Si pone nel mezzo tra l'applicazione e un database relazionale, lasciando lo sviluppatore libero di concentrarsi sugli aspetti di business dell'applicazione. Hibernate non è una soluzione intrusiva e si adatta bene ad applicazioni nuove ed esistenti, e non richiede modifiche distruttive al resto dell'applicazione. 3.1 Un esempio di applicazione con Hibernate Prima di approfondire i concetti fondamentali per l'utilizzo di Hibernate, è utile dare un occhiata al codice per eseguire operazioni di base di tipo CRUD, giusto per avere un'idea della complessità di utilizzo. Questo esempio mostra come rendere persistenti e come ottenere la lista di istanze di una classe User . Definiamo la classe User: Listato 3.1: La classe User public class User{ private Long id; private String name; private String lastName; public User() {} public User( String name, String lastName ){ this.name = name; this.lastName = lastName; } public Long getId(){ return id; } public void setId( Long id ){ this.id = id; } public String getName(){ return name; } public void setName( String name ){ this.name = name; } public String getLastName(){ return lastName; } } public void setLastName( String lastName ){ this.lastName = lastName; } Il codice che salva su database un'istanza di User è il seguente 13 Capitolo 3 Introduzione ad Hibernate Listato 3.2: Rendere persistente un'istanza User user = new User( “Beppe”, “Rossi” ); Session session = getSessionFactory().openSession(); Transaction tx = session.beginTransaction(); session.save( user ); tx.commit(); session.close(); Ovviamente sono stati omessi tutti i dettagli relativi al metodo getSessionFactory(), al mapping tra la classe User e il database e non è ancora stato spiegato cosa sono gli oggetti Session e Transaction, interfacce definite in Hibernate. Dall'esempio risulta chiara comunque la semplicità con cui si rende persistente un oggetto Java: si istanzia, si apre una transazione, si chiama il metodo save() di Session e si esegue il commit della transazione. Niente di più. L'oggetto Java verrà persistito sul DB nella tabella associata alla classe User, come nuovo record della tabella. Ogni proprietà dell'oggetto sarà salvata in una colonna distinta della tabella. L'esempio che segue restituisce l'istanza di User il cui id (di tipo Long) è pari a 10: Listato 3.3: Ricerca per Id Session session = getSessionFactory(); Transaction tx = session.beginTransaction(); User users = (User) session.get( User.class, new Long(10) ); tx.commit(); session.close(); Per recuperare tutte le istanze di User salvate nel database: Listato 3.4: Lista di tutti gli User Session session = getSessionFactory(); Transaction tx = session.beginTransaction(); List<User> users = session.createQuery(“select u from User u”).list(); tx.commit(); session.close(); Infine per cancellare un'istanza persistente: Listato 3.5: Cancellazione di un'istanza User user = ... //user e' un istanza persistente caricata dal database Session session = getSessionFactory(); Transaction tx = session.beginTransaction(); session.delete( user ); tx.commit(); session.close(); Hibernate 3.2 14 Mapping di una classe con una tabella di un database Come introdotto nel precedente capitolo, per rendere persistente una classe è necessario specificare un mapping tra la classe e il database. Hibernate utilizza dei file XML per 1 definire questa associazione . Ogni classe persistente ha il suo file di mapping associato. Di solito il file XML sta nello stesso package della classe persistente e ha lo stesso nome della classe con estensione “.hbm.xml”. Il file di mapping per User ha quindi nome User.hbm.xml. Listato 3.6: User.hbm.xml <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="domain"> <class name="User" table="USERS" > <id name="id" column="id" type="long"> <generator class="native" /> </id> <property name="name" column="name" type="string" /> <property name="lastName" column="last_name" type="string" /> </class> </hibernate-mapping> Al di là dei dettagli sull'intestazione del file XML, è possibile notare come il mapping venga definito tramite: • un elemento <class> in cui è specificato il nome della classe Java e la tabella corrispondente del database. • All'interno di <class> si trova la definizione dell'id dell'oggetto tramite l'elemento <id>, in cui si definisce quale sia la politica di generazione di tale id. Nell'esempio viene definito un id con una politica che delega al database sottostante il compito di creare l'id (ad esempio un campo auto-increment) tramite l'elemento <generator class=”native”>. • Seguono poi le definizioni delle proprietà tramite l'elemento <property>, ognuna delle quali viene mappata con una colonna della tabella. Non tutto quello che è stato scritto sul file XML è necessario. Hibernate è in grado, ad esempio, di inferire il tipo di dato a partire dalla proprietà della classe Java, rendendo superfluo l'attributo “type” dell'elemento <property>. Pure l'attributo “column”, che specifica il nome della colonna associata, può essere omesso. Così facendo Hibernate assume che la colonna abbia lo stesso nome della proprietà. 3.3 Configurazione di Hibernate Per poter utilizzare Hibernate deve essere costruita innanzitutto una SessionFactory. La come dice il nome, è la factory che permette la creazione di una Session di Hibernate, la quale può essere vista in maniera semplicistica come la cache delle istanze di oggetti Java persistenti e permette l'interazione con il database (la Session viene trattata in SessionFactory, 1 Esiste un'alternativa all'utilizzo dei file XML per la creazione del mapping, ossia l'utilizzo delle Java annotations. Questo presuppone l'utilizzo di una distribuzione Java uguale o superiore alla 5 e l'utilizzo di alcune librerie aggiuntive che rendono Hibernate un'implementazione per JPA (si veda a tal proposito 8 Appendice: lo standard JPA 15 Capitolo 3 Introduzione ad Hibernate maniera approfondita nel seguito). La SessionFactory deve essere creata una sola volta, è molto dispendiosa a livello di risorse e contiene tutti gli aspetti di configurazione per l'accesso ad un database. La SessionFactory viene configurata attraverso un file XML di nome hibernate.cfg.xml che normalmente risiede nella root del classpath: Listato 3.7: hibernate.cfg.xml <?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> <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property> <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property> <property name="hibernate.connection.url">jdbc:mysql://localhost/testdb> <property name="hibernate.connection.username">testdbuser</property> <property name="hibernate.connection.password">testdbpasswd</property> <property name="hibernate.connection.provider_class"> org.hibernate.connection.C3P0ConnectionProvider </property> <property name="hibernate.c3p0.min_size">5</property> <property name="hibernate.c3p0.max_size">20</property> <property name="hibernate.c3p0.timeout">300</property> <property name="hibernate.c3p0.max_statements">50</property> <property name="hibernate.c3p0.idle_test_period">3000</property> <mapping resource="domain/User.hbm.xml" /> </session-factory> </hibernate-configuration> Il file specifica: • • • • • il driver JDBC di connessione al database il tipo di dialetto per il particolare database da usare; cambiando dialetto e driver è possibile usare un qualsiasi altro database (tra quelli supportati da Hibernate, e sono davvero tanti) senza modificare assolutamente l'applicazione I parametri di connessione al database Il gestore del pool di connessioni al database (in questo caso c3p0) I file di mapping (hbm.xml) da usare per definire le classi persistenti La SessionFactory viene inizializzata tramite le seguenti righe di codice: Listato 3.8: Inizializzazione della SessionFactory Configuration cfg = new Configuration(); SessionFactory sessionFactory = cfg.configure().buildSessionFactory(); Come già detto, la SessionFactory deve essere istanziata una sola volta (almeno una per database utilizzato all'interno della stessa applicazione) e deve essere accessibile in ogni parte dell'applicazione in cui è necessario interagire con un oggetto di tipo Session. Per Hibernate 16 questo normalmente la SessionFactory viene incapsulata dentro un singleton ([GOF]) o, in ambito di applicazioni web, come risorsa JNDI ([JNDIT]) o come variabile di una sessione HTTP. 4 Mapping delle classi persistenti Hibernate 18 Una delle cose interessanti che differenziano Hibernate da altri sistemi di persistenza (ad esempio gli entity beans nella vecchia specifica EJB2) consiste nel fatto che le classi che devono essere rese persistenti, e qui in seguito chiamate entità, non devono implementare alcuna interfaccia. In pratica un qualsiasi modello di dominio composto da semplici classi Java (comunemente chiamate POJO, Plain Old Java Objects) possono essere rese persistenti con Hibernate. La persistenza in Hibernate è trasparente, ossia gli oggetti Java non conoscono assolutamente il database sottostante e non conoscono nemmeno il loro stato di persistenza. Il meccanismo di persistenza è esternalizzato verso un persistence manager (la Session di Hibernate). La trasparenza di Hibernate in realtà impone alcune specifiche sulle classi da persistere: ogni classe deve avere il costruttore di default e, a meno di altre configurazioni particolari, Hibernate usa metodi setter e getter per impostare i valori nelle proprietà di un oggetto. Questa “limitazione” è del tutto plausibile, anche perché normalmente ogni oggetto Java implementa i metodi richiesti da Hibernate. Infine, tutti gli attributi che rappresentano una collezione di oggetti devono utilizzare l'interfaccia della collezione (ad esempio Set o List) e non una delle loro implementazioni (HashSet o ArrayList), ma anche questa è una buona pratica di progettazione di una classe di dominio. 4.1 4.1.1 Associazioni tra entità Associazioni many-to-one In un modello di dominio ogni classe ha un'insieme di attributi i cui valori possono essere o un dato primitivo, includendo anche per estensione quegli attributi il cui tipo è assimilabile ad un dato primitivo (String, Date, Long, Integer ecc), o un'associazione ad un'entità, ossia un oggetto il cui tipo è una classe persistente del modello di dominio (per adesso si ignorano le collezioni di oggetti). Come visto in precedenza, il mapping dei dati primitivi è definito, nel file di mapping associato alla classe persistente, tramite l'elemento <property>, assegnando il corretto valore all'attributo “type”. Quando invece un attributo è un riferimento ad un'altra classe del modello di dominio, abbiamo un'associazione tra entità. Le associazioni tra entità vengono rappresentate, nel mondo relazionale, con relazioni many-to-one. Infatti se ogni classe del modello di dominio corrisponde ad una tabella del database, la relazione tra due entità altro non è che l'associazione tra due tabelle fatte tramite foreign key. Riprendendo l'esempio del paragrafo 3.2 Mapping di una classe con una tabella di un database, viene aggiunta una nuova entità, la classe Department, che rappresenta il diparitmento di appartenenza dell'utente. La classe User viene aumentata con un riferimento ad un'istanza di Department. User La classe Department sarà così definita: Department 19 Capitolo 4 Mapping delle classi persistenti Listato 4.1: La classe Department public class Department{ private Long id; private String name; private int depCode; public Department(){} public Long getId(){ return id; } public void setId( Long id ){ this.id = id; } public String getName(){ return name; } public void setName( String name ){ this.name = name; } public int getDepCode(){ return depCode; } } public void setDepCode( int depCode ){ this.depCode = depCode; } La classe User viene così modificata (le parti in grassetto sono le aggiunte): Listato 4.2: La classe User modificata public class User{ private private private private Long id; String name; String lastName; Department department; public User() {} ... //metodi getter e setter dei vecchi attributi public Department getDepartment(){ return department; } } public void setDepartment( Department department ){ this.department = department; } Il file di mapping per Department è il seguente: Hibernate 20 Listato 4.3: Department.hbm.xml <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="domain"> <class name="Department" table="DEPARTMENTS" > <id name="id" column="id" type="long"> <generator class="native" /> </id> <property name="name" column="name" type="string" /> <property name="depCode" column="dep_code" type="integer" /> </class> </hibernate-mapping> Il file di mapping di grassetto): User viene modificato in questo modo (le parti aggiunte sono in Listato 4.4: User.hbm.xml modificato <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="domain"> <class name="User" table="USERS" > <id name="id" column="id" type="long"> <generator class="native" /> </id> <property name="name" column="name" type="string" /> <property name="lastName" column="last_name" type="string" /> <many-to-one name=”department” class=”Department” column=”dep” not-null=”true”/> </class> </hibernate-mapping> Questo secondo file contiene l'elemento <many-to-one>, il quale definisce l'associazione tra la classe User e la classe Department come molti-a-uno tra le due tabelle corrispondenti. Nel database la relazione tra l'utente e il dipartimento viene implementata tramite la chiave esterna definita dal campo dep della tabella USERS. Da notare l'attributo not-null, che specifica la nullabilità della proprietà, rafforzando a livello di codice Java i vincoli del campo del database, permettendo così ad Hibernate di controllare che non ci siano vincoli non soddisfatti prima dell'inserimento nel database. Inoltre questo attributo permette di far stabilire ad Hibernate la precedenza di salvataggio delle entità in caso di persistenza transitiva (si veda 5.5 Persistenza transitiva). 4.1.2 Associazioni one-to-many Con Hibernate è possibile gestire attributi che sono collezioni di entità. Nel modello di dominio questi attributi devono essere definiti tramite l'interfaccia che definisce il tipo di collezione (Collection, List, Set, ecc), e sono specificati nel file di mapping tramite un elemento di tipo <bag>, <set> o <list>. Si supponga ad esempio che il modello di dominio sia rappresentato dalla seguente 21 Capitolo 4 Mapping delle classi persistenti associazione tra User e Department: User Department In questo caso non abbiamo più l'associazione tra User e Department, bensì un attributo di tipo Set di User (non si accettano duplicati) nella classe Department. Listato 4.5: La classe Department modificata public class Department{ private private private private Long id; String name; int depCode; Set<User> users = new HashSet<User>(); public Department(){} ... //metodi getter e setter public Set<User> getUsers(){ return users; } } public void setUsers( Set<User> users ){ this.users=users; } La prima cosa che deve essere notata è che nei database relazionali questo tipo di associazione non esiste. Il legame tra le tabelle permette di specificare una sola foreign key per campo verso i record di un'altra tabella. Le tabelle del database quindi non subiscono alcuna modifica, e la relazione è sempre implementata dalla chiave esterna definita sul campo dep della tabella USERS. Nel mondo Java il modello viene invece ribaltato e si concentra l'attenzione sulla relazione di aggregazione tra il dipartimento e i suoi utenti. Il file di mapping di Department viene così modificato: Hibernate 22 Listato 4.6: Department.hbm.xml modificato <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="domain"> <class name="Department" table="DEPARTMENTS" > <id name="id" column="id" type="long"> <generator class="native" /> </id> <property name="name" column="name" type="string" /> <property name="depCode" column="dep_code" type="integer" /> <set name="users" table="USERS" lazy="true"> <key column="dep" not-null="true" /> <one-to-many class="User" /> </set> </class> </hibernate-mapping> L'elemento <set> è composto da: • l'attributo name, nome della proprietà • l'attributo table, nome della tabella associata • l'attributo lazy, che se impostato a true permette di non inizializzare subito la collezione quando l'oggetto viene caricato dal database; la collezione verrà valorizzata quando vi si accederà per la prima volta • l'elemento <key> che definisce la chiave esterna nella tabella USERS • l'element <one-to-many> che definisce il set come una relazione tra entrità one to many e l'entità contenuta nella collezione è del tipo User. 4.1.3 Associazioni bidirezionali Le relazioni many-to-one e one-to-many appena viste possono essere fuse insieme per creare una relazione bidirezionale. Normalmente è utile modificare il modello delle classi in modo da avere nella classe dalla parte one (quella che contiene la collezone) un metodo add che si occupa di aggiungere alla collezione il nuovo elemento e di impostare nell'elemento un riferimento all'oggetto a cui si aggiunge: Listato 4.7: Aggiunte alla classe Department ... public void addUser( User user ){ users.add( user ); user.setDepartment( this ); } ... La relazione bidirezionale contiene un problema di aggiornamento multiplo: se specifichiamo nei file di mapping sia la proprietà many-to-one sia quella one-to-many, abbiamo due rappresentazioni differenti alla stessa foreign key. Per cui l'aggiunta di un nuovo utente ad un dipartimento viene intesa da Hibernate come un doppio aggiornamento 23 Capitolo 4 Mapping delle classi persistenti alle istanze persistenti. Hibernate non rileva trasparentemente il fatto che due modifiche si riferiscono alla stessa colonna di database, poiché non è stato ancora indicato da alcuna parte la bidirezionalità dell'associazione. Per fare in modo che venga effettuato un solo update è necessario aggiungere l'attributo inverse all'elemento <set> di Department.hbm.xml. Con questa indicazione, Hibernate considererà solamente le modifiche fatte dalla parte di User, e in particolare quando viene settato l'attributo department. Le aggiunte fatte solo dalla parte della collezione di Department verranno ignorate: Listato 4.8: Department.hbm.xml con l'attributo “inverse” <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="domain"> <class name="Department" table="DEPARTMENTS" > <id name="id" column="id" type="long"> <generator class="native" /> </id> <property name="name" column="name" type="string" /> <property name="depCode" column="dep_code" type="integer" /> <set name="users" table="USERS" lazy="true" inverse="true"> <key column="dep" not-null="true" /> <one-to-many class="User" /> </set> </class> </hibernate-mapping> 4.2 Ereditarietà Una delle problematiche introdotte nel paragrafo 2.2 Paradigm mismatch che un tool di ORM deve affrontare è il mapping di classi che appartengono ad una gerarchia di ereditarietà. Hibernate offre almeno tre soluzioni differenti di mapping: • • • 4.2.1 Table per concrete class: Si usa una tabella per ogni classe concreta (non astratta). Table per class hierarchy: Si usa una sola tabella per tutte le classi di una gerarchia. Table per subclass: Si usa una tabella per ogni classe, concreta o astratta che sia. Table per concrete class Tutte le proprietà delle superclassi sono inserite nella tabella associata alla classe concreta. Questo tipo di mapping non supporta molto bene le associazioni polimorfiche. Si supponga ad esempio di avere una classe Client che ha un'associazione con una classe Super, super-classe di una gerarchia di altre due classi Sub1 e Sub2. Le tabelle risultanti saranno solo 3, chiamate rispettivamente C, S1 e S2. Risulta impossibile determinare quale sia la tabella della gerarchia di classi verso cui associare la foreign key di C, in quanto non esiste una tabella associata a Super e la scelta di una tra S1 o S2 sarebbe ovviamente errata. Hibernate 4.2.2 24 Table per class hierarchy Una intera gerarchia di classi viene mappata in un'unica tabella. Tutte le proprietà della gerarchia di classi vengono fuse insieme. Un campo della tabella serve da discriminante per determinare il tipo di classe associato al record. Hibernate istanzia correttamente la classe concreta tenendo conto di quest'ultimo. I vantaggi di questo approccio sono diversi: è possibile usare associazioni polimorfiche a oggetti di gerarchie di classi mappate con questa strategia. Infatti nella stessa tabella coesistono record associati a istanze di tipo diverso. L'interrogazione è molto veloce poiché include una sola tabella (a differenza del prossimo tipo di mapping che utilizza più tabelle) Di contro nella stessa tabella confluiscono campi associati a proprietà di classi che possono stare su rami differenti della stessa gerarchia. Questi possono avere senso per un ramo e non averne per un altro. Per cui Hibernate ha bisogno di poter rendere nulli tutti quei campi dei record per cui essi non hanno motivo di esistere. Questo comporta di dover rendere nullabili tutti i campi che si differenziano nella gerarchia, perdendo un'importante funzionalità di controllo dei valori di un campo. 4.2.3 Table per subclass L'ultima opzione è quella in cui si crea una tabella per ogni classe della gerarchia, che sia astratta o meno. Le tabelle vengono messe in join con una foreign key sulla chiave primaria. Ogni volta che viene fatto un inserimento, le proprietà di ogni classe della gerarchia vengono suddivise in più record, ognuno dei quali viene salvato nella tabella corrispondente. Le interrogazioni vengono effettuate eseguendo una outer join su gli id di tutte le tabelle incluse nella gerarchia. In questo modo i record che vengono recuperati sono composti dall'unione di tutti i campi di tutte le tabelle, ma ogni record ha valorizzati (diversi da null) soltanto i campi che hanno senso per il tipo di oggetto da istanziare associato al record. L'aspetto positivo di questo modello è che è completamente polimorfico. Inoltre può evolvere in maniera naturale insieme all'aggiunta di nuove classi nella gerarchia: è sufficiente aggiungere una nuova tabella in join con quella della superclasse. Il lato negativo è che le interrogazioni possono subire (a seconda del database sottostante) dei rallentamenti, in quanto le outer join sono più dispendiose. 4.3 Generazione automatica del database Un aspetto interessante di Hibernate è la capacità di generare il database a partire dai file di mapping. Effettivamente i file di mapping contengono tutte le informazioni necessarie per costruire lo schema. Inoltre, nel file di mapping, è possibile specificare ulteriori attributi per definire alcune caratteristiche che deve avere il campo associato del database (ad esempio per una string è possibile specificare la lunghezza del campo VARCHAR associato. Questa funzionalità risulta estremamente utile in fase di sviluppo. Dato che il tipo di database sottostante può essere cambiato senza problemi, è possibile utilizzare, in fase di debug o per i test unitari, delle implementazioni leggere di database (ad esempio HSQL che può essere creato anche in memoria). Ogni volta che si esegue il test hibernate crea il database da zero, senza bisogno di mantenere allineato a mano il modello di dominio e lo schema del DB. 5 Persistenza con Hibernate Hibernate 5.1 26 Persistence Manager La persistenza in Hibernate si ottiene tramite il Persistence Manager. Tramite il Persistence Manager gli oggetti Java possono essere salvati, trovati ed eliminati dal database. Esso controlla pure le transazioni del database e degli aspetti di cache degli oggetti persistenti. In Hibernate il Persistence Manager è implementato da un'oggetto di tipo Session. 5.2 Perisitence lifecycle Un oggetto Java non è reso persistente nel momento in cui viene istanziato. Un oggetto inizialmente non è associato al database e il suo stato si dice essere transient. Nel momento in cui l'oggetto è collegato al Persistence Manager allora si dice che è persistent. Ci sono due modi per rendere persistente un oggetto: chiamare il metodo save della Session di Hibernate o recuperare un'istanza dal database tramite le funzioni di ricerca del Persistence Manager. Una volta che un oggetto è persitente, ogni cambiamento al suo stato viene propagato anche al database. In realtà queste modifiche nei record del database avvengono solo in determinati momenti stabiliti da Hibernate stesso. Questa caratteristica prende il nome di transparent transaction-level-write-behind, ossia Hibernate propaga le modifiche il più tardi possibile, ma nasconde i dettagli all'applicazione. Se la sessione a cui è associato un oggetto persistente viene chiusa (eseguendo il commit delle operazioni) o se l'oggetto viene sganciato dal Persistence Manager tramite appositi metodi di Session, l'oggetto entra nello stato detached. Un oggetto detached è sempre associato al database (il suo id non è nullo) ma le modifiche effettuate sull'oggetto non vengono propagate al database. Un oggetto detached può tornare ad essere persistente, riagganciando l'oggetto alla sessione tramite appositi metodi. Quando un oggetto persistente viene cancellato, esso viene rimosso dal database e il suo stato torna ad essere transiente. 5.3 Identità degli oggetti Per chiarire uno dei punti che fanno parte del paradigm mismatch, ossia il problema dell'identità, è interessante analizzare come Hibernate gestisca le istanze persistenti. Prima di affrontare la questione, è importante tenere presente la differenza che c'è tra database identity ( a.getId().equals(b.getId()) ) e object identity.( a == b ). In Hibernate vale il seguente principio: • All'interno di una Session di Hibernate esiste una sola istanza di oggetto che rappresenta una particolare riga di una tabella del database Questo principio, comunemente detto transaction-scoped identity, ci garantisce che se richiediamo due volte lo stesso record del database otteniamo sempre la stessa istanza di oggetto Java. Quindi nell'ambito dello stesso persistence manager la database identity equivale alla object identity. 27 5.4 Capitolo 5 Persistenza con Hibernate Persistence Manager In questo paragrafo vengono presentati alcuni esempi di utilizzo della Session di Hibernate per rendere persistent, detached e transient alcuni oggetti Java. Per l'esempio prendiamo la classe User definita nel Listato 3.1: La classe User. Listato 5.1: Oggetti persistenti User user = new User( “Franco”, “Rossi” ); Session session = getSessionFactory().openSession(); Transaction tx = session.beginTransaction(); session.save( user ); tx.commit(); session.close(); L'oggetto user viene creato e attaccato alla Session di Hibernate tramite il metodo save, passando in questo modo allo stato persistent. Questo produce, ad un certo momento, un insert di un nuovo record nella tabella USERS. Conclusa la transazione la sessione viene chiusa e l'oggetto diventa detached. Se la sessione non viene chiusa, l'oggetto rimane nello stato persistent ed è quindi possibile modificarlo ulteriormente. Le modifiche verranno rese persistenti alla fine di una nuova transazione: Listato 5.2: Modifiche ad un oggetto persistente User user = new User( “Franco”, “Rossi” ); Session session = getSessionFactory().openSession(); Transaction tx = session.beginTransaction(); session.save( user ); tx.commit(); //Si termina la prima transazione ma non si chiude la sessione //Si riapre poi una nuova transazione. //In realta' non e' necessario aprire la sessione prima delle modifiche all'oggetto. tx = session.beginTransaction(); user.setName( “Gianni” ); tx.commit(); session.close(); Come si vede dall'esempio, se un oggetto rimane attaccato alla sessione (persistent) viene automaticamente sincronizzato col database ogni volta che si esegue un commit di una transazione. Hibernate mette a disposizione un'insieme di metodi per gestire lo stato persistente degli oggetti: è possibile attaccare un oggetto detached alla sessione, obbligando a sincronizzare eventualmente le modifiche, oppure rileggendo lo stato attuale da database. È possibile eliminare un oggetto dalla sessione senza eliminarlo dal database. È possibile caricare un oggetto richiedendo un lock di sola lettura o lettura-scrittura. 5.5 Persistenza transitiva Un'altra caratteristica interessante di Hibernate è quella della persistenza transitiva. La persistenza transitiva permette di rendere persistenti non solo gli oggetti che manualmente Hibernate 28 attacchiamo alla sessione, ma pure tutti gli oggetti che in qualche modo sono raggiungibili da esso. In pratica possiamo decidere se, una volta reso persistente un oggetto, tutti gli oggetti referenziati dall'oggetto debbano essere persistenti, anche ricorsivamente. Questo meccanismo risulta molto utile poiché lo sviluppatore non deve più preoccuparsi di salvare ogni oggetto o eseguire i singoli update. Quando un'applicazione interagisce con un oggetto, può modificarlo, aggiungere elementi ad una sua proprietà di tipo collezione o cambiare il riferimento ad un entità, modificare lo stato o cambiare uno qualsiasi di questi oggetti correlati; quando verrà eseguito il commit della transazione, ogni modifica, inserimento o cancellazione a tutto il grafo di elementi sarà eseguita in automatico. La politica di persistenza transitiva (indicata in Hibernate col termine cascade) può essere specificata per ogni attributo della classe all'interno del file di mapping, dando allo sviluppatore un controllo fine sul modo con cui essa opera. Listato 5.3: Department.hbm.xml con l'attributo “cascade” <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="domain"> <class name="Department" table="DEPARTMENTS" > <id name="id" column="id" type="long"> <generator class="native" /> </id> <property name="name" column="name" type="string" /> <property name="depCode" column="dep_code" type="integer" /> <set name="users" table="USERS" lazy="true" inverse="true" cascade="save-update"> <key column="dep" not-null="true" /> <one-to-many class="User" /> </set> </class> </hibernate-mapping> L'attributo cascade=”save-update” permette di rendere persistenti tutte le aggiunte di elementi o le modifiche agli elementi della collezione. 6 Interrogazioni con Hibernate Hibernate 6.1 30 Recuperare oggetti per Id Come è già stato mostrato nei precedenti esempi Hibernate è in grado di recuperare oggetti da un database utilizzando semplici metodi di Session. Se vogliamo ottenere un'istanza persistente dato l'id del record è sufficiente utilizzare il metodo get: Listato 6.1: Recuperare un oggetto per id Session session = getSessionProvider().openSession(); Transaction tx = session.beginTransaction(); User user = session.get( User.class, new Long( 12 ) ); tx.commit(); session.close(); 6.2 Interrogazioni con HQL Ovviamente il metodo mostrato al paragrafo precedente non è sufficiente. Hibernate mette a disposizione un vero e proprio linguaggio simil-SQL, l'HQL (Hibernate Query Language), che permette di interrogare il database utilizzando le classi Java e le proprietà della classe al posto delle tabelle e dei campi. L'esempio che segue mostra come creare un'interrogazione che cerca tra gli utenti quelli che hanno nome “Mario”: Listato 6.2: HQL per un interrogazione semplice Session session = getSessionProvider().openSession(); Transaction tx = session.beginTransaction(); Query q = session.createQuery( “select u from User u where u.name = :fname” ); q.setString( “fname”, “Mario” ); List<User> users = q.list(); tx.commit(); session.close(); Si nota subito l'assoluta somiglianza con SQL, ma User e u.name rappresentano la classe User e il suo attributo name. Un aspetto interessante che rende più semplice HQL dell'SQL standard è il fatto che le join con Hibernate sono implicitamente scritte nel mapping e nel modello di dominio. Normalmente, a differenza di SQL, con HQL non è necessario scrivere join per ottenere le entità associate ad User. Se ad esempio riprendiamo la classe User del Listato 4.2: La classe User modificata, non è necessario mettere in join la classe Department nella query di interrogazione scritta sopra. Infatti, a seconda della politica di cascade dell'associazione, nel momento in cui viene inizializzato un oggetto di tipo User, l'associazione a Department viene o inizializzata o rimpiazzata con un proxy che inizializza l'associazione al suo primo accesso. Se invece abbiamo bisogno di eseguire un'interrogazione che ponga dei criteri anche sulla classe Department, è sufficiente includere nella where la proprietà di department deferenziandola col nome dell'attributo department: 31 Capitolo 6 Interrogazioni con Hibernate Listato 6.3: Query con join implicita Query q = session.createQuery( “select u from User u where u.name=:fusername and u.department.name=:fdepname” ); q.setString( “fusername”, “Mario” ); q.setString( “fdepname”, “My department” ); List<User> users = q.list(); Da notare che nella query non è stata scritta alcuna join. Hibernate sa che per ottenere una where sul nome del dipartimento deve eseguire una join SQL, ma questo rimane trasparente allo sviluppatore che ha scritto l'HQL. Con HQL è anche possibile eseguire join esplicite e select multiple. Anche in questo caso la sintassi HQL è molto più semplice e compatta di SQL: Listato 6.4: Query con join esplicita Query q = session.createQuery( “select u, d from User u, u.department d” + “ where u.name=:fusername and d.name=:fdepname” ); q.setString( “fusername”, “Mario” ); q.setString( “fdepname”, “My department” ); List<User> users = q.list(); In questo caso la join è ottenuta da u.department grazie al fatto che Hibernate conosce già l'associazione, grazie alla presenza dei file di mapping. Gli elementi della lista risultante da questa interrogazione sono array bidimensionali contenenti le istanze dei due oggetti richiesti. 7 Un esempio pratico 33 Capitolo 7 Un esempio pratico In questo capitolo viene presentato un esempio pratico di utilizzo di Hibernate. L'esempio è minimale, non si concentra assolutamente sulla presentazione dei dati o sulla loro immissione, bensì sull'utilizzo basilare di Hibernate. Per questo motivo, il database utilizzato per l'esempio è HSQLDB, un database leggero che può essere creato in memoria e che quindi non richiede alcuna installazione. Ovviamente per questo esempio può essere utilizzato un qualsiasi altro database supportato da Hibernate, modificando la configurazione della SessionFactory e aggiungendo la libreria-driver per l'accesso al particolare DB. L'esempio viene costruito facendo uso dell'ambiente di sviluppo open-source Eclipse (www.eclipse.org). 7.1 7.1.1 Installazione dei prerequisiti Java Nel sistema deve essere installata una versione della JDK di Java. Si consiglia l'ultima versione della Java 6. Per scaricare e installare una JDK consultare il seguente sito: http://www.oracle.com/technetwork/java/javase/downloads/index.html 7.1.2 Eclipse Eseguire il download dell'ultima versione di Eclipse dal sito www.eclipse.org, facendo attenzione di prendere quella per il proprio sistema operativo (esistono versioni per Windows, Mac e Linux). Tra le versioni presenti è sufficiente scaricare Eclipse IDE for Java Developers. Scompattare l'archivio scaricato in una qualsiasi cartella. Lanciare Eclipse dall'eseguibile presente nella cartella scompattata. 7.1.3 Hibernate Scaricare l'ultima versione di Hibernate distribution dal sito www.hibernate.org (al tempo di questo documento è la versione 3.6.0). Scompattare l'archivio in una qualsiasi cartella. 7.1.4 HSQLDB Scaricare l'ultima versione del database dal sito http://hsqldb.org/. Scompattare l'archivio in una qualsiasi cartella. Hibernate 7.2 34 Creazione di un nuovo progetto Da dentro Eclipse eseguire il comando File-New-Java project. Impostare “Hibernate Test” come nome del progetto e cliccare su Finish. Nella finestra “Package Explorer” che dovrebbe comparire sulla sinistra selezionare col destro il nome del progetto e aggiungere una nuova cartella chiamata “lib” tramite il comando New-Folder. Aprire un browser del file system, entrare nella cartella in cui è stato scompattato Hibernate, selezionare il file “hibernate3.jar” e copiarlo nella cartella lib del progetto appena creata (il copy-paste funziona tranquillamente tra browser ed Eclipse). Entrare nella cartella “lib” della distribuzione Hibernate, poi nella sottocartella “required”, selezionare tutti i file in essa contenuta e copiarli nella cartella “lib” del progetto. Entrare nella cartella “lib” della distribuzione Hibernate, poi nella sottocartella “jpa” della distribuzione Hibernate e copiare il file contenuto nella cartella “lib” del progetto. Entrare nella cartella dove è stato scompattato HSQLDB, entrare nella sottocartella “lib” e copiare il file “hsqldb.jar” nella cartella “lib” del progetto. Infine, selezionare tutti i file contenuti nella cartella lib del progetto, cliccare col destro e eseguire il comando “Build path-Add to build path”. 35 Capitolo 7 Un esempio pratico Con questa procedura sono state inserite dentro il progetto tutte le librerie necessarie per utilizzare Hibernate e HSQLDB e il progetto è stato configurato per utilizzarle nel proprio class-path. 7.3 Creazione delle classi del dominio Cliccare col destro sulla cartella “src” del progetto, eseguire il comando New-Class e impostare la classe come nell'immagine che segue: Il nome del package è “domain”, il nome della classe è “User”. Il resto della finestra rimane come di default. Cliccando su Finish si ottiene un nuovo file java sotto source. Riempire la classe come segue. package domain; public class User { private private private private Long id; String name; String lastName; Department department; public User() { } Hibernate public User(String name, String lastName) { this.name = name; this.lastName = lastName; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public Department getDepartment() { return department; } public void setDepartment(Department department) { this.department = department; } } @Override public String toString() { return name + " " + lastName + " - " + department; } Allo stesso modo creare la classe Department nello stesso package: package domain; import java.util.HashSet; import java.util.Set; public class Department { private private private private Long id; String name; int depCode; Set<User> users = new HashSet<User>(); public Department() { } public Department(String name, int depCode) { this.name = name; this.depCode = depCode; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } 36 37 Capitolo 7 Un esempio pratico public String getName() { return name; } public void setName(String name) { this.name = name; } public int getDepCode() { return depCode; } public void setDepCode(int depCode) { this.depCode = depCode; } public Set<User> getUsers() { return users; } public void setUsers(Set<User> users) { this.users = users; } public void addUser(User user) { users.add(user); user.setDepartment(this); } } 7.4 @Override public String toString() { return name + " (" + depCode + ")"; } Creazione del file di configurazione di Hibernate Direttamente nella cartella src creare un nuovo file con estensione .xml, contenente il seguente codice: <?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> <property <property <property <property <property name="hibernate.connection.driver_class">org.hsqldb.jdbcDriver</property> name="hibernate.connection.url">jdbc:hsqldb:htest</property> name="hibernate.connection.username">sa</property> name="hibernate.connection.password"></property> name="hibernate.dialect">org.hibernate.dialect.HSQLDialect</property> <property name="hbm2ddl.auto">create</property> <mapping resource="domain/User.hbm.xml"/> <mapping resource="domain/Department.hbm.xml"/> </session-factory> </hibernate-configuration> In questo file di configurazione è stato specificato l'utilizzo del driver di HSQLDB, con nome utente pari a “sa” (l'utente di default per questo tipo di database) e password vuota. Come dialetto è stato configurato quello di HSQLDB. La proprietà “hbm2ddl” permette la creazione automatica del database (verrà distrutto e ricreato ad ogni avvio) Hibernate 7.5 38 Creazione dei file di mapping All'interno del package “domain” creare i due file di mapping. Il file di mapping di User: <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="domain"> <class name="User" table="USERS"> <id name="id" column="id" type="long"> <generator class="native" /> </id> <property name="name" column="name" type="string" /> <property name="lastName" column="last_name" type="string" /> <many-to-one name="department" class="Department" column="dep" not-null="true" /> </class> </hibernate-mapping> Il file di mapping di Department: <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="domain"> <class name="Department" table="DEPARTMENTS"> <id name="id" column="id" type="long"> <generator class="native" /> </id> <property name="name" column="name" type="string" /> <property name="depCode" column="dep_code" type="integer" /> <set name="users" table="USERS" lazy="true" inverse="true" cascade="save-update"> <key column="dep" not-null="true" /> <one-to-many class="User" /> </set> </class> </hibernate-mapping> 7.6 Utilizzo di Hibernate in una classe Test All'interno del package “ domain” creare una classe Test con un main. All'interno del main si crea la SessionFactory e da questa le Session per interagire con il database. package domain; import java.util.List; import import import import import org.hibernate.Query; org.hibernate.SessionFactory; org.hibernate.Transaction; org.hibernate.cfg.Configuration; org.hibernate.classic.Session; public class Test { @SuppressWarnings("unchecked") public static void main(String[] args) { // creo la session factory 39 Capitolo 7 Un esempio pratico Configuration configuration = new Configuration(); configuration.configure(); SessionFactory sf = configuration.buildSessionFactory(); // creo 2 utenti associati ad 1 dipartimento User user1 = new User("Mario", "Rossi"); User user2 = new User("Stefania", "Verdi"); Department dep1 = new Department("dipartimento1", 10); dep1.addUser(user1); dep1.addUser(user2); // salvo il dipartimento Session session = sf.openSession(); Transaction tx = session.beginTransaction(); session.save(dep1); tx.commit(); session.close(); // provo ad interrogare il database per gli utenti session = sf.openSession(); tx = session.beginTransaction(); List<User> users = session.createQuery("select u from User u").list(); for (User u : users) System.out.println(u); tx.commit(); session.close(); } } // provo ad interrogare il database per un particolare utente session = sf.openSession(); tx = session.beginTransaction(); Query q = session .createQuery("select u from User u where u.name=:fname"); q.setParameter("fname", "Mario"); users = q.list(); for (User u : users) System.out.println(u); tx.commit(); session.close(); 8 Appendice: lo standard JPA 41 Capitolo 8 Appendice: lo standard JPA Hibernate non è l'unico strumento ORM per Java. Esistono altre soluzioni più o meno note che implementano più o meno bene alcune delle funzionalità sopra descritte. In JavaEE esiste una specifica (da non molto tempo) per la persistenza di oggetti su un database relazionale, JPA (Java Persistence API, [JPWH]), che fa parte della nuova specifica EJB3 ([EJB3IA], [EJB30]). JPA si propone come lo standard per la persistenza in Java, definendo un sistema di mapping basato su XML o Java Annotation, un linguaggio di interrogazione ad oggetti, JPQL, e l'integrazione con JTA, Java transaction API, per la gestione delle transazioni. In realtà JPA non è altro che un'insieme di interfacce che definiscono la specifica. Quando si utilizza JPA è necessario anche includere una libreria che implementi questa specifica. Hibernate può essere utilizzato come implementazione della specifica JPA (aggiungendo alle librerie standard quelle relative a JPA). Usato in questo modo Hibernate è del tutto trasparente, poiché non si utilizza HQL o mapping proprietari di Hibernate. In ogni caso, se si desidera, è sempre possibile sfruttare alcune funzionalità proprietarie usando delle estensioni che Hibernate definisce per JPA. Bibliografia POEAA: Martin Fowler, Patterns of Enterprise Application Architecture, 2002 HIA: Christian Bauer, Gavin King, Hibernate in Action, 2004 JPWH: Christian Bauer, Gavin King, Java Persistence with Hibernate, 2006 GOF: Erich Gamma, Richard Helm, Ralph Johnson, John M. Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, 1994 JNDIT: Oracle, JNDI Tutorial, http://java.sun.com/products/jndi/tutorial/ EJB3IA: Debu Panda, Reza Rahman, Derek Lane, EJB3 in action, 2007 EJB30: Richard Monson-Haefel, Bill Burke, Enterprise JavaBeans 3.0, 2006