Programmazione Java Avanzata Hibernate (Parte 2) Ing. Giuseppe D'Aquì
Testi Consigliati Beginning Hibernate 2 nd edition (Apress) Sul sito è possibile scaricare, tra gli extra, il codice sorgente e il capitolo 3 Hibernate Getting Started http://docs.jboss.org/hibernate/core/3.6/quickstart/e 2
Pro e contro Annotations Pro: Le annotation sono standard JPA, quindi sono usate in modo identico da tutti gli ORM JPA-compatibili Meno verbosità Intuitive Contro: Le versioni obsolete di Hibernate supportano solo XML Se il database è usato da più applicazioni, si rischia di distribuire il mapping su applicazioni diverse 3
Chiave Composta Se in una tabella la chiave primaria è costituita da più campi, si ha una chiave composta Quando non ci sono particolari vincoli (ovvero abbiamo il controllo dello schema del database), in genere si preferisce semplificare lo sviluppo generando una chiave surrogata Una chiave surrogata non ha significato al di fuori del database, è un campo che rimpiazza i molti campi della chiave composta 4
Chiave Composta Quando però ci sono vincoli la chiave composta deve essere mappata su Hibernate Ci sono tre modalità, tutte e tre prevedono la creazione di una classe che contenga il mapping dei campi della chiave composta Es. se la chiave composta è (nome, cognome), allora serve una classe (differente dalla corrente) che contenga come variabili membro String nome e String cognome 5
Chiave composta: modo 1 La classe con gli attributi chiave deve essere pubblica ed esterna public class UserPrimaryKey{...} La classe va annotata con @Embeddable, e devono essere implementati i metodi hashcode() ed equals() Nella classe originaria, invece, bisogna sostituire gli attributi della chiave composta con un unica variabile id (annotata con @Id) private UserPrimaryKey id; 6
Chiave composta: modo 2 La classe con gli attributi chiave va creata come nel modo 1, ma internamente alla classe originaria La classe non va annotata con @Embeddable La classe originaria ha sempre la variabile id ma annotata con @EmbeddedId 7
Chiave composta: modo 3 La classe con gli attributi chiave va creata come nel modo 2 Si annota solo la classe originaria con @IdClass Passandogli la classe che fa le funzioni di chiave @IdClass(User.UserPrimaryKey.class) Nella classe originaria non c'è più id ma vanno messi gli attributi originali annotati con @Id 8
Chiave composta: riassunto Il codice SQL generato in tutti i casi è uguale Ci sono particolarità di utilizzo: Modo 1: La classe con gli attributi è esterna ed embeddable (vedi relazioni one-to-one), può essere riutilizzata Modo 3: possiamo utilizzare gli attributi chiave direttamente dalla classe originaria [getnome() e getcognome()], invece di ottenere prima l'oggetto id Modo 2: una via di mezzo 9
Mapping di relazioni Le relazioni tra tabelle possono essere di diverso tipo One-to-One (uno-a-uno) One-to-Many (uno-a-molti) Many-to-Many (molti-a-molti) 10
One-to-one: tipi Le relazioni uno-a-uno, in Hibernate, sono di due tipi: Classiche, due tabelle corrispondono a due oggetti Component o embedded, due oggetti che risiedono su una sola tabella (es. l'indirizzo) 11
One-to-one embedded Una relazione one-to-one embedded si ha quando un'entità è interamente racchiusa all'interno di un'altra entità, ossia è un componente dell'alltra entità La classe componente va annotata con @Embeddable, non possiede @Id Nella classe principale viene posta una variabile membro con lo stesso tipo della classe componente, annotata con @Embedded 12
One-to-one embedded public class User { } @Embedded public Address getaddress(){...} @Embeddable public class Address{ } private String via; private String civico; //getters/setters 13
One-to-one La relazione uno-a-uno convenzionale è in genere sospetta, perché potrebbe nascondere un errore di progettazione In genere, infatti, si tende ad accorpare tutti gli attributi in una sola entità Ci sono dei casi in cui, però, è preferibile avere due tabelle che rappresentano due aspetti differenti di una certa entità ideale Aspetti che devono essere gestiti differentemente, o che devono evolvere in modo indipendente Es. Profilo Autenticazione e Anagrafiche 14
One-to-one Per mappare una relazione uno-a-uno basta annotare con @OneToOne la variabile membro (o il getter) che contiene l'oggetto correlato 15
One-to-one classica public class User { @OneToOne public Address getaddress(){...} } @Entity public class Address{ @Id private int id; private String via; private String civico; } //getters/setters 16
One-to-one L'annotazione @OneToOne può avere i seguenti attributi: cascade: indica se le operazioni su una entità devono propagarsi alle altre della relazione fetch: indica il tipo di acquisizione, eager (carica tutti gli oggetti del grafo delle relazioni) o lazy (carica gli oggetti correlati solo se servono) optional: serve per stabilire se ci possono essere NULL 17
One-to-one bidirezionale La relazione OneToOne è bidirezionale quando l'oggetto A contiene un riferimento all'oggetto B, e l'oggetto B contiene un riferimento all'oggetto A Es. User.getAddress() e Address.getUser() In questo caso uno dei due oggetti deve essere considerato principale Quello la cui tabella contiene la chiave primaria dell'altro come riferimento L'oggetto secondario dovrà usare l'attributo mappedby nella sua annotazione @OneToOne 18
One-to-one bidirezionale @Entity public class User { //User è l'oggetto principale @OneToOne public Address getaddress(){...} } @Entity public class Address{ //Address è l'oggetto secondario //il mappedby si riferisce alla proprietà dell'oggetto principale @OneToOne(mappedBy=address) } public User getuser(){...} 19
One-to-one - JoinColumn Per default, Hibernate cercherà come foreign key un campo della tabella con queste caratteristiche: Inizia con il nome della proprietà dell'oggetto principale (User ha getaddress(), quindi address ) Concatenato con underscore _ Concatenato con il nome della chiave primaria dell'oggetto secondario (Es, Address ha getid(), quindi id Nell'esempio quindi cercherà una colonna chiamata address_id 20
One-to-one - JoinColumn È possibile sovrascrivere questo default con due tipi di annotazioni: @PrimaryKeyJoinColumn : usato assieme a @OneToOne sull'oggetto principale, indica che la foreign key è la stessa chiave primaria (ovvero, le due tabelle possiedono la stessa chiave primaria) @JoinColumn(name= address_fk ) : usato assieme a @OneToOne sull'oggetto principale, indica che la foreign key sta in una colonna con un nome preciso (in questo caso address_fk ) 21
Cascading delle operazioni Il cascading è la propagazione delle operazioni di aggiornamento e rimozione su oggetti collegati tra loro da relazioni Esempio: se viene cancellato un User deve essere cancellato il corrispondente Address? Hibernate definisce diversi tipi di cascading, definiti nelle relazioni Se non si specifica niente, per default non si ha cascading 22
Cascading delle operazioni Tipi di cascading: CascadeType.MERGE : propaga gli UPDATE CascadeType.PERSIST : propaga il primo inserimento (INSERT) CascadeType.REFRESH : propaga l'aggiornamento dal database verso gli oggetti (SELECT) CascadeType.DETACH : propaga la rimozione dell'oggetto dalla persistenza CascadeType.REMOVE : propaga la rimozione dei dati dell'oggetto (DELETE) CascadeType.ALL : tutti i precedenti 23
One-to-Many La relazione uno-a-molti può essere vista da due prospettive diverse L'oggetto A ha una relazione con molti oggetti B Ogni oggetto B ha una relazione con uno e un solo oggetto A Hibernate mappa queste due prospettive usando @OneToMany e @ManyToOne 24
One-to-Many @OneToMany va sull'oggetto che contiene @ManyToOne va sull'oggetto contenuto JPA considera, per convezione, oggetto principale quello che ha @ManyToOne che, quindi, dovrà specificare un mappedby 25
One-to-Many @Entity public class User { //User è l'oggetto secondario @OneToMany(mappedBy= user ) public Set<Telephone> gettelephone(){...} } @Entity public class Telephone{ //Telephone è l'oggetto principale @ManyToOne public User getuser(){...} } 26
One-to-Many - JoinColumn Valgono le stesse considerazioni di JoinColumn fatte nel caso One-to-One Nell'esempio Hibernate cercherà una colonna chiamata user_id nella tabella telephone (oggetto principale) Possiamo però specificare un'altra colonna utilizzando @JoinColumn insieme a @ManyToOne nell'oggetto principale 27
Ordering Quando vengono mappate delle Collection, possiamo definire una colonna sulla quale queste verranno ordinate @OneToMany(mappedBy= user ) public List<Telephone> gettelephone(){...} //List è un tipo collection ordinato Si può aggiungere a @OneToMany l'annotazione @OrderBy, che permette di specificare quale proprietà di Telephone usare per l'ordinamento @OneToMany(mappedBy user ) @OrderBy( prefisso ASC ) public List<Telephone> gettelephone(){...} 28
Many-to-Many In una relazione Many-to-Many entrambi gli oggetti coinvolti utilizzeranno @ManyToMany Uno dei due deve essere l'oggetto principale, l'altro conterrà l'attributo mappedby Una relazione molti-a-molti nel modello relazionale ha bisogno di una tabella ausiliaria che contiene le foreign key di entrambe le tabelle 29
Many-to-Many La tabella ausiliaria ha, per default, il nome: Tabellaprincipale_Tabellasecondaria E chiavi Tabellaprincipale_id e Tabellasecondaria_id Questi default possono essere sovrascritti con @JoinTable, usato lato oggetto principale @JoinTable( name= studenti_e_corsi, joincolumns={@joincolumn(name= corso_id )}, inversejoincolumns{@joincolumn(name= matricola )} ) 30
Many-to-Many: nota bene Quando c'è una relazione molti-a-molti che contiene attributi specifici, forse non si tratta di una mera relazione tra tabelle ma è una entità di fatto Es. se la tabella Studente_Corso contiene l'attributo voto, non è più una semplice tabella di join ma un'entità Esame, che ha relazioni uno-a-molti con Studente e con Corso La tabella di join, per Hibernate, contiene solo le foreign key e non altri attributi 31
Ereditarietà L'ereditarietà in Hibernate e JPA può essere rappresentata in tre modi: Single table: una singola tabella che contiene padri e figli Joined: una tabella per il padre e una per ciascuno dei figli, ma i figli contengono solo gli attributi non in comune con il padre Table-per-class: tabelle complete per ogni figlio 32
Ereditarietà: single table Single table prevede una singola tabella per i padri e per i figli Una singola tabella contiene più specializzazioni di una stessa entità base: gli attributi di un figlio non avranno senso nel caso di un fratello (e saranno quindi NULL) La strategia single table si usa annotando la classe padre con @Inheritance(strategy= SINGLE_TABLE) 33
Ereditarietà: single table Quando si usa una singola tabella per tutta la gerarchia, bisogna specificare una colonna discriminante che contiene informazioni sul tipo dell'oggetto Si aggiunge @DiscriminatorColumn nelle annotazioni della classe padre @Entity @DiscriminatorColumn(name= tipologia ) public class Padre {...} 34
Ereditarietà: single table Per default Hibernate, come discriminante, crea/cerca una colonna chiamata DTYPE di tipo stringa Questo default può essere sovrascritto: discriminatortype: può essere DiscriminatorType.STRING DiscriminatorType.CHAR DiscriminatorType.INTEGER lenght: la lunghezza del campo, usata solo nel caso stringa 35
Ereditarietà: single table I valori contenuti nella colonna discriminante sono, per default, i nomi delle classi figlio Volendo usare valori differenti si dovranno annotare le classi figlio con @DiscriminatorValue( valore ) 36
Ereditarietà: Joined Le annotazioni sono identiche al caso Single Table C'è @DiscriminatorColumn, @DiscriminatorValue eccetera Si attua annotando la classe padre con @Inheritance(strategy = JOINED) A basso livello Hibernate creerà/cercherà una tabella per il padre......e una tabella per ogni figlio, che conterrà soltanto gli attributi che non sono già presenti nel padre 37
Ereditarietà: Table Per Class Con questa strategia, tutti gli oggetti (padre e figli), se concreti, avranno tabelle che contengono tutti i loro attributi Basta annotare la classe padre con @Inheritance(strategy = TABLE_PER_CLASS) 38
Ereditarietà: Quale Strategia? Usando Joined si avrà uno schema più mantenibile, perchè ogni modifica di un figlio impatta solo sulla tabella del figlio, e ogni modifica al padre impatta solo sulla tabella padre Usando table-per-class, ogni modifica al padre impatterà anche su tutte le tabelle dei figli mentre le performance sono migliori del caso Joined, perché non ci sono join Single Table ha migliori performance, perché tutte le query vengono fatte su una sola tabella, ma può diventare disordinata 39
Mapped Superclass Un caso speciale di ereditarietà si ha quando la classe padre non è resa persistente e quindi, in teoria, non dovrebbe possedere annotazioni Hibernate In realtà per far funzionare l'ereditarietà dovremo annotare la classe padre come se fosse persistente, e poi aggiungere @MappedSuperClass Subito dopo @Entity, per specificare che la classe padre è solo mappata e non deve essere resa persistente 40
Altre annotazioni 41
Dati temporali Le proprietà di tipo java.util.date vengono di default mappate su attributi di tipo TIMESTAMP Questo comportamento può essere modificato con l'annotazione @Temporal @Temporal(TemporalType.DATE) @Temporal(TemporalType.TIME) @Temporal(TemporalType.TIMESTAMP) 42
Large Objects I Large Objects sono attributi di una tabella che contengono oggetti molto grandi: stringhe estese (LONGTEXT) sequenze di byte (BLOB, Binary Large OBject) Per specificare che una certa proprietà va salvata in un attributo di tipo large, basta annotarla con @Lob @Lob public String gettitle() { } Si creerà un attributo che non sarà varchar 43
Generare Indici Questa è una annotazione non inclusa in JPA, solo in Hibernate Gli indici possono essere applicati: Alla singola colonna, con l'annotazione @Index(name= nomedellindice ) Su diverse colonne contemponaneamente, annotando la classe con: @Table(appliesTo= nometabella, indexes = { }) @Index(name= nomeindice1, columnnames={ col1, col2 } 44
Named Query Una Named Query è una query (in HQL) a cui diamo un nome, per poter essere richiamata successivamente È composta quindi da una coppia (nome, espressione) Si definisce con annotazione di classe: @Entity @NamedQuery(name= findallstudents, query= from Students ) public class Student{ } 45
Named Query La caratteristica delle Named Query è quella di essere riutilizzabili Se vogliamo riutilizzarle tra più classi, dobbiamo definirle a livello di package Si crea un file chiamato package-info.java che conterrà le annotazioni che devono essere condivise tra tutte le classi del package @NamedQuery(name= findallstudents, query= from Students ) package it.unirc.pja.example2 46
Hibernate Console 47
Hibernate Console La Hibernate Console è un tool che fa parte del plugin per Eclipse Permette di svolgere le seguenti funzioni Visualizzazione rapida dei mapping e degli schemi Scrittura di query di test HQL (parametrizzate e non) Generazione dello schema del database a partire dai mapping Generazione di diagrammi che rappresentano i mapping 48
Hibernate console: esempio Vedi 49
Metodi base di Session 50
Metodi base di Session Abbiamo già visto che l'oggetto Session è fondamentale Svolge la funzione di interfaccia tra i nostri oggetti Java e Hibernate Session ha un certo numero di metodi utilizzati per richiamare le funzionalità di Hibernate 51
Salvataggio Quando si crea un nuovo oggetto che possiede mapping Hibernate, questo non viene automaticamente reso persistente Si deve salvare per la prima volta in una Session, tramite il metodo save() session.save(oggetto); 52
Nota Bene: Confronto Un certo oggetto reso persistente avrà due modi per essere identificato: Identificativo di istanza della classe (che rappresenta l'oggetto) Chiave primaria (che rappresenta la tupla) Nell'ambito di una stessa sessione entrambi gli identificativi potranno essere usati, mentre su più sessioni l'identificativo di istanza potrebbe cambiare Bisogna quindi evitare di usare l'operatore == (che confronta l'identificativo di istanza); meglio usare equals(), magari implementata da noi con il confronto tra le primary key 53
Caricamento Il caricamento di oggetti persistenti avviene tramite load() Ha due parametri: La classe della entity, passata come nome (String) o come oggetto Class L'id dell'oggetto (chiave primaria) Se la chiave specificata non esiste verrà lanciata una eccezione Simile a load() è get(): se la chiave non esiste restituisce NULL invece di una eccezione 54
Caricamento: Lock modes Sia load() che get() supportano un terzo parametro, opzionale, che serve a specificare il lock degli oggetti caricati Il lock è usato per evitare problemi di aggiornamento concorrente I lock vengono rilasciati alla fine della transazione Attenzione: usare i lock può creare problemi di deadlock, se più thread in contemporanea acquisiscono parte delle risorse e non le rilasciano (vedi Problema dei 5 filosofi) 55
Caricamento: Lock modes Lock mode disponibili: NONE: default; legge dal database solo se l'oggetto richiesto non è in cache READ: legge semre l'oggetto dal database UPGRADE: se supportato dal database/dialect, imposta un lock di aggiornamento; se non è possibile ottenerlo, attende che vengano rilasciati i lock che ostacolano UPGRADE_NOWAIT: come UPGRADE, ma se non è possibile ottenere il lock lancia subito una eccezione 56
Aggiornamento dal DB Se vogliamo ottenere l'ultima versione dell'oggetto dal database (eliminando le modifiche fatte da noi) basta utilizzare il metodo refresh() In realtà Hibernate nella maggior parte dei casi lo fa in automatico, non c'è bisogno di chiamare refresh() direttamente Il caso particolare si ha quando l'oggetto viene aggiornato da applicazioni esterne oppure da query SQL: in questo caso Hibernate non può sapere che l'oggetto è stato aggiornato 57
Update Se un oggetto è persistente, ogni modifica che operiamo su di esso viene accodata ed eseguita da Hibernate alla fine della sessione Non è necessario eseguire esplicitamente una operazione di update (con il metodo update()) Nel caso volessimo salvare tutte le operazioni effettuate sul db prima del termine della sessione, possiamo usare il metodo flush() 58
Update: flush() modes La strategia di flushing automatica prevede di salvare ogni oggetto prima di eseguire una query che restituisce l'oggetto stesso Esistono altre strategie, impostabili con setflushmode(): ALWAYS: prima di ogni query effettua flush(), salvando tutti gli oggetti. Lento COMMIT: esegue flush() solo in fase di commit MANUAL: non esegue mai flush(), dobbiamo richiamarlo esplicitamente nel codice 59
Delete Se si deve cancellare un oggetto persistente si può usare il metodo delete() Il parametro di delete() è un Object, che può essere: L'oggetto da cancellare Un oggetto dello stesso tipo di quello da cancellare, con la proprietà chiave impostata (questo si usa se non abbiamo l'oggetto completo ma conosciamo solo l'id) 60
SaveOrUpdate() Il metodo save() rende persistente un oggetto (INSERT) mentre update() lo aggiorna (UPDATE) La prima volta va usato save(), le successive update() Se la prima volta si usa update(), o nelle successive save(), ci sarà un errore Se non sappiamo se usare save() o update(), possiamo usare saveorupdate() che effettua una select per vedere se l'oggetto esiste, e poi chiama save() o update() 61
Disassociazione Può capitare di aver bisogno che un certo oggetto non sia più persistente Dato che ogni modifica dentro una sessione viene tracciata da Hibernate, per poi eseguire gli aggiornamenti, vorremmo evitare che venissero tracciate operazioni che non vanno riportate sul database In questo caso si usa il metodo evict(), che disassocia un oggetto dalla sessione corrente 62
Hibernate Query Language 63
Sintassi base Hibernate fornise un proprio linguaggio di query, basato suglio oggetti e modellato in modo molto simile a SQL Poiché dall'interno di Hibernate si possono sfruttare le informazioni di mapping, molte delle query in HQL sono più compatte delle corrispondenti versioni SQL Ovviamente, HQL viene tradotto in SQL prima di essere inviato al database 64
Sintassi base SELECT È identico all'equivalente SQL, solo che la parte di proiezione (SELECT nome, cognome) è opzionale: se non si specifica si assume SELECT * La clausola FROM è seguita dal nome di una classe, piuttosto che di una tabella 65
CreateQuery Un oggetto di tipo Query può essere ottenuto, a partire dalla corrispondente stringa HQL, tramite il metodo createquery() di Session Query q = session.createquery( from User ); 66
Controllare codice SQL generato Il codice HQL viene convertito in SQL prima di essere inviato al database Può capitare che, in alcuni casi, la traduzione sia una query SQL inefficiente Possiamo visualizzare le traduzioni tramite: Hibernate console in Eclipse File di Log, se abilitiamo la proprietà show_sql nel file di configurazione di Hibernate 67
Filtri condizionali Ovvero la clausola WHERE Possiamo usare tutti gli operatori tipici di SQL (OR, AND, =, <>, like eccetera) In più possiamo definire dei parametri, per realizzare query parametriche I parametri si inseriscono come :nomeparametro Es: From User where name=:name 68
Named Parameters I Named Parameters ci aiutano a difendere la nostra applicazione dagli attacchi di SQL Injection Infatti la sostituzione tra parametri e valore effettivo avviene controllando il tipo esatto dell'oggetto passato Es. Query query = session.createquery( from User where address=:address ); query.setentity( address, myaddress); 69
Pagination Normalmente in una web application restituiremo solo un certo numero di dati all'utente Se sono molti, questo significa mostrare una pagina alla volta L'oggetto query possiede due metodi per supportare questa funzionalità: setfirstresult(int) : indica la tupla di partenza setmaxresults(int) : indica quante tuple prelevare 70
Risultato unico L'oggetto Query ha un metodo list() che restituisce tutti i risultati di una query Possiede anche un metodo uniqueresult() che restituisce solo un oggetto Se l'oggetto è più di uno, lancia una eccezione Quando vogliamo ottenere solo il primo risultato, dovremo usare una combinazione di uniqueresult() e setmaxresults(1)
Order by HQL supporta la clausola Order By che viene usata come in SQL order by nomeproprietà [desc asc] Se vogliamo ordinare per più proprietà, basta separarle con la virgola
Join Tramite le join si possono usare più classi in una sola query Hibernate supporta più tipi di join: Inner, cross, left outer, right outer, full outer Se ci sono i mapping non c'è bisogno di specificare le condizioni di join
Aggregazioni HQL supporta gli operatori di aggregazione: avg(name) count(name *) max(name) min(name) sum(name)
Aggiornamenti in blocco Quando serve aggiornare o cancellare un certo numero di oggetti contemporaneamente, usare un ciclo for potrebbe essere inefficiente Si possono usare gli equivalenti HQL di UPDATE e DELETE Basta creare una query di update/delete, e poi chiamarne il metodo executeupdate()
SQL Nativo L'uso di SQL nativo si dovrebbe evitare per ottenere la massima portabilità; se proprio è necessario, possiamo creare una query in SQL utilizzando il metodo Session.createSQLQuery(String)