Nell’articolo Primi Passi con Hibernate sono stati introdotti i concetti che sono alla base dell’utilizzo del framework ORM Hibernate, mostrando un semplice esempio di mapping di una singola entità. In questo post affrontiamo il problema del mapping delle diverse possibili relazioni tra persisted object.
Associazioni in Java
Hibernate è un framework di persistenza per plain java object (o pojo) e quindi le tipologie di associazioni che sono prese in considerazione sono prevalentemente quelle unidirezionali, tipiche di java. Tali associazioni sono tipicamente espresse attraverso riferimenti o collezioni in funzione della molteplicità dell’associazione. La specifica JPA prevede le seguenti tipologie di associazioni:
ONE-TO-ONE | Ogni istanza di una entità è legata ad una singola istanza di un’altra entità. Sono espresse attraverso l’annotazione javax.persistence.OneToOne posizionata in corrispondenza della proprietà o campo dell’oggetto persistito.ESEMPIO: relazione tra una persona ed il proprio passaporto. |
ONE-TO-MANY | Un’istanza di una entità può essere correlata a più istanze di altre entità. Sono espresse utilizzando l’annotazione javax.persistence.OneToMany posizionata in corrispondenza della proprietà o campo dell’oggetto persistito.ESEMPIO: relazione esistente tra un ordine di acquisto e gli elementi che lo compongono. |
MANY-TO-ONE | Più istanze di una entità possono essere correlati a una singola istanza dell’altra entità. Questa molteplicità è l’opposto di una relazione one-to-many. Sono espresse utilizzando l’annotazione javax.persistence.ManyToOne posizionata in corrispondenza della proprietà o campo dell’oggetto persistito.ESEMPIO: relazione esistente tra una persona ed uno dei suoi genitori, che ovviamente può avere più figli. |
MANY-TO-MANY | Le istanze di una entità possono essere correlate a più istanze di ogni altro entità. Sono espresse attraverso l’annotazione javax.persistence.ManyToMany posizionata in corrispondenza della proprietà o campo dell’oggetto persistito.ESEMPIO: la relazione che college ogni corso ha molti studenti, e ogni studente hai diversi corsi che frequenta. |
Le diverse associazioni sono caratterizzate da informazioni che sono caratteristiche della specifica relazione, ma tutte hanno in comune la modalità con cui sono recuperate dal database (fetching). L’enum FetchType elenca le due strategie possibili di fetching, che sono EAGER o LAZY. La prima indica al runtime di persistenza che la relazione deve essere recuperata immediatamente insieme all’oggetto che la utilizza. La seconda è invece un suggerimento per cui la specifica implementazione può decidere di seguirlo, recuperando la relazione solamente quando viene acceduta, o meno, di fatto comportandosi come nel caso EAGER.
Relazione One-To-One
Consideriamo la relazione esistente tra uno studente e l’indirizzo della sua abitazione, rappresentato dal modello dati mostrato in figura, dove un indirizzo è associato univocamente ad uno studente.
Lo script di creazione delle due tabelle e della relazione in una base dati MySQL sarà:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
CREATE TABLE INDIRIZZI ( ID INT NOT NULL AUTO_INCREMENT, STRADA VARCHAR(200) NOT NULL, CIVICO VARCHAR(20) NOT NULL, CAP VARCHAR(5) NOT NULL, CITTA VARCHAR(45) NOT NULL, PRIMARY KEY (ID) ); CREATE TABLE STUDENTI ( MATRICOLA VARCHAR(10) NOT NULL, NOME VARCHAR(45) NOT NULL, COGNOME VARCHAR(45) NOT NULL, INDIRIZZO INT NULL, PRIMARY KEY (MATRICOLA), INDEX FK_STUD_IND_IDX (INDIRIZZO ASC), CONSTRAINT FK_STUD_IND FOREIGN KEY (INDIRIZZO) REFERENCES INDIRIZZI (ID) ON DELETE NO ACTION ON UPDATE NO ACTION ); |
Studente
ed Indirizzo
sarà del tipo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@Entity @Table( name = "STUDENTI" ) public class Studente { private String matricola; private String nome; private String cognome; private Indirizzo indirizzo; @Id public String getMatricola() { return matricola; } // Altri getter e setter @OneToOne( fetch= FetchType.EAGER ) @JoinColumn( name="INDIRIZZO" ) public Indirizzo getIndirizzo() { return indirizzo; } public void setIndirizzo(Indirizzo indirizzo) { this.indirizzo = indirizzo; } } |
STUDENTI
che contiene il valore della primary key referenziata nella tabella INDIRIZZI
.
Recuperare lo studente ed il relativo indirizzo è quindi possibile semplicemente con una unica istruzione:
1 2 |
Studente studente = session.find( Studente.class, "RM123MR"); System.out.println( studente.toString() + ", " + studente.getIndirizzo().toString() ); |
Sebbene non necessario è anche possibile mappare la relazione inversa tra l’indirizzo e lo studente cui è associato, senza necessità di modificare il database ma semplicemente utilizzando la stessa annotation sulla classe Indirizzo
nel modo seguente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
@Entity @Table( name = "INDIRIZZI" ) public class Indirizzo { private Integer id; private String strada; private String civico; private String cap; private String citta; private Studente studente; @Id public Integer getId() { return id; } // Altri getter e setter @OneToOne( mappedBy = "indirizzo" ) public Studente getStudente() { return studente; } public void setStudente(Studente studente) { this.studente = studente; } } |
In questo caso è sufficiente indicare la proprietà della classe Studente
che mappa la relazione ed Hibernate provvederà ad invertirla per eseguire la join necessaria a recuperala. Il codice seguente produce esattamente lo stesso output del precedente:
1 2 |
Indirizzo indirizzo = session.find( Indirizzo.class, 1); System.out.println( indirizzo.getStudente().toString() + ", " + indirizzo.toString() ); |
Relazione One-To-Many e Many-To-One
Consideriamo la relazione esistente tra uno studente ed il relativo corso di laurea, mostrata nel modello dati seguente, dove un ad un corso possono essere iscritti più studenti ma uno studente può appartenere ad uno ed un solo corso di laurea.
Si noti che la struttura della tabella CORSI_LAUREA
è similare a quella INDIRIZZI, quindi
la differenziazione del tipo di relazione deve essere fatta a livello di mapping JPA. Modifichiamo il database con il seguente script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
CREATE TABLE CORSI_LAUREA ( ID INT NOT NULL AUTO_INCREMENT, ANNO INT NOT NULL, FACOLTA VARCHAR(200) NOT NULL, PRIMARY KEY (ID) ); ALTER TABLE STUDENTI ADD COLUMN CORSO INT NULL AFTER INDIRIZZO, ADD INDEX FK_STUD_CORSO_idx (CORSO ASC); ALTER TABLE javaboss.STUDENTI ADD CONSTRAINT FK_STUD_CORSO FOREIGN KEY (CORSO) REFERENCES CORSI_LAUREA (ID) ON DELETE NO ACTION ON UPDATE NO ACTION; |
Utilizzando le annotazioni JPA il mapping tra le classi Studente
e CorsoLaurea
si realizzeranno attraverso una annotazione @ManyToOne
sulla prima classe:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@Entity @Table( name = "STUDENTI" ) public class Studente { .... private CorsoLaurea corsoLaurea; // getter e setter @ManyToOne @JoinColumn( name="CORSO" ) public CorsoLaurea getCorsoLaurea() { return corsoLaurea; } public void setCorsoLaurea(CorsoLaurea corsoLaurea) { this.corsoLaurea = corsoLaurea; } } |
@OneToMany
sulla seconda:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@Entity @Table( name = "CORSI_LAUREA" ) public class CorsoLaurea { private Integer id; private Integer anno; private String facolta; private Set<Studente> studenti; @Id public Integer getId() { return id; } // altri getter e setter @OneToMany( mappedBy = "corsoLaurea" ) public Set<Studente> getStudenti() { return studenti; } public void setStudenti(Set<Studente> studenti) { this.studenti = studenti; } } |
Anche in questo caso è necessario, quindi, specificare la colonna di join nella relazione @ManyToOne
presente nella classe Studente
e la proprietà di mapping nella @OneToMany
nella classe CorsoLaurea
.
Il seguente codice mostra come recuperare Il corso di laurea di uno studente e tutti gli studenti appartenenti ad uno specifico corso di laurea:
1 2 3 4 5 6 7 |
Studente studente = session.find( Studente.class, "RM123MR"); System.out.println( studente.toString() + ", " + studente.getCorsoLaurea().toString() ); CorsoLaurea corso = session.find( CorsoLaurea.class, 1 ); for ( Studente corsista : corso.getStudenti() ) { System.out.println( corsista.toString() ); } |
Relazione Many-To-Many
Questa volta prendiamo in consideriamo la relazione esistente tra uno studente ed i corsi di studio seguiti. In questo caso uno studente può seguire più corsi ed analogamente ad un corso possono essere iscritti più studenti. Una relazione di questo tipo può essere catturata esclusivamente utilizzando una tabella di relazione tra le due entità, come mostrato nel diagramma seguente:
Aggiorniamo il nostro database con lo script seguente di creazione delle due tabelle CORSI_STUDIO
e STUDENTI_CORSO
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
CREATE TABLE CORSI_STUDIO ( ID INT NOT NULL AUTO_INCREMENT, NOME VARCHAR(45) NOT NULL, CORSO_LAUREA INT NOT NULL, PRIMARY KEY (ID), INDEX FK_CORSO_STUD_LAUREA_idx (CORSO_LAUREA ASC), CONSTRAINT FK_CORSO_STUD_LAUREA FOREIGN KEY (CORSO_LAUREA) REFERENCES javaboss.CORSI_LAUREA (ID) ON DELETE NO ACTION ON UPDATE NO ACTION); CREATE TABLE STUDENTI_CORSO ( STUDENTE VARCHAR(10) NOT NULL, CORSO INT NOT NULL, INDEX FK_STUDENTE_idx (STUDENTE ASC), INDEX FK_CORSO_idx (CORSO ASC), CONSTRAINT FK_STUDENTE FOREIGN KEY (STUDENTE) REFERENCES javaboss.STUDENTI (MATRICOLA) ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT FK_CORSO FOREIGN KEY (CORSO) REFERENCES javaboss.CORSI_STUDIO (ID) ON DELETE NO ACTION ON UPDATE NO ACTION ); |
Supponiamo ora di voler mappare nella classe Studente
tutti i corsi di studio seguiti. Per farlo utilizziamo l’annotazione @ManyToMany
nel modo seguente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@Entity @Table( name = "STUDENTI" ) public class Studente { ... private Set<CorsoStudio> corsiStudio; // altri getter e setter @ManyToMany @JoinTable( name="STUDENTI_CORSO", joinColumns=@JoinColumn(name="STUDENTE", referencedColumnName="MATRICOLA"), inverseJoinColumns=@JoinColumn(name="CORSO", referencedColumnName="ID")) public Set<CorsoStudio> getCorsiStudio() { return corsiStudio; } public void setCorsiStudio(Set<CorsoStudio> corsiStudio) { this.corsiStudio = corsiStudio; } } |
Si noti che la tabella STUDENTI_CORSO
non ha un corrispondente model nel progetto ma è semplicemente referenziata dall’annotazione @JoinTable
che serve a dichiarare il mapping tra le entità Studente
e CorsoStudio
.
Il codice seguente recupera uno studente e stampa a video i corsi seguiti:
1 2 3 4 5 6 |
Studente studente = session.find( Studente.class, "RM123MR"); System.out.println( studente.toString() + ", " + studente.getIndirizzo().toString() ); for ( CorsoStudio corsoStudio : studente.getCorsiStudio() ) { System.out.println( corsoStudio.getNome() ); } |
Codice Sorgente
Il codice sorgente comprensivo dello script SQL di creazione del database è disponibile qui hb-user.
Ciao, come è possibile invece mappare le gerarchie?
Grazie.
Buongiorno.
Mi rendo conto che il mapping delle gerarchie è un argomento altrettanto interessante. Preparerò quindi un post a breve.
Grazie.