Programmazione Java Avanzata Hibernate (Parte 2) Ing. Giuseppe D'Aquì Testi Consigliati ● Beginning Hibernate 2nd 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 3 di distribuire il mapping su applicazioni diverse 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 ● ● Passandogli la classe che fa le funzioni di chiave ● ● @IdClass @IdClass(User.UserPrimaryKey.class) Nella classe originaria non c'è più “id” ma vanno messi gli attributi originali annotati con 8 @Id 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 ● ● Ci sono dei casi in cui, però, è preferibile avere due tabelle che rappresentano due aspetti differenti di una certa entità “ideale” ● ● In genere, infatti, si tende ad accorpare tutti gli attributi in una sola entità 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 ● ● In questo caso uno dei due oggetti deve essere considerato “principale” ● ● Es. User.getAddress() e Address.getUser() 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 31 le foreign key e non altri attributi 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 37 nel padre 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 essere40 resa persistente 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 53 le primary key 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 55 rilasciano (vedi Problema dei 5 filosofi) 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 57 l'oggetto è stato aggiornato 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)