Dipartimento di Ingegneria dell`Informazione

annuncio pubblicitario
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
Scarica