Object-Relational Mapping (ORM) in JPA
Il passaggio dal modello object-oriented a quello relazionale è reso particolarmente complicato dai seguenti aspetti:- un oggetto può mantenere riferimenti ad altri oggetti
- un oggetto può estendere altri oggetti (ereditarietà)
- un oggetto include oltre ad uno stato anche dei comportamenti
Il termine object-relational mapping (ORM) fa riferimento al processo mediante il quale lo stato di un oggetto viene salvato all’interno di un database relazionale.
Per stabilire come questo mapping debba avvenire ORM fa uso di metadati di configurazione siano questi sottoforma di annotazioni o inseriti all’interno di deployment descriptor.
Il problema ORM riguarda essenzialmente tre aspetti:
- il mapping delle entità
- il mapping delle relazioni
- il mapping dell’ereditarietà
Mapping delle entità
Lo stato di un’entità in generale viene mappato in una singola tabella anche se Java Persistence Api (JPA) offre la possibilità di suddividere lo stato in parti e mappare ciascuna parte in una tabella differente.La prima cosa da fare quando si definisce un’entity bean () consiste quindi nell’indicare la tabella nella quale l’entity bean verrà mappato.
Per definire questo aspetto si fa uso dell’annotazione così definita
(TYPE) (RUNTIME) public Table { String name() default ""; String catalog() default ""; String schema() default ""; UniqueConstraint[] uniqueConstraints() default {}; }Tale annotazione è del tutto opzionale, se non viene specificata infatti il persistence provider effettua il mapping nella tabella avente lo stesso nome dell’entità.
Il parametro name serve ad indicare appunto il nome della tabella nella quale verrà effettuato il mapping dell’entità.
I parametri catalog e schema vengono usati raramente e servono a qualificare completamente la tabella mappata: per esempio è possibile specificare che la tabella UTENTI appartiene allo schema MIOSCHEMA nel seguente modo:
(name="UTENTI", schema="MIOSCHEMA") public class UtenteIl parametro uniqueConstraints serve invece a specificare dei vincoli sulle colonne della tabella.
Ad esempio è possibile inserire un vincolo univoco sulla colonna ID quando questa viene creata a tempo di deployment nel seguente modo:
(name="UTENTI",uniqueConstraints={t(columnNames={"ID"})})Occorre comunque considerare che l’autogenerazione delle tabelle non è un’operazione raccomandata.
L’annotazione mappa un campo persistente o proprietà nella colonna specificata dall’attributo name.
Per esempio se si vuole mappare il campo userId nella colonna ID si può scrivere:
(name=" ID") protected Long userId;In questo caso si assume che la colonna ID appartenga alla tabella UTENTI specificata dall’annotazione .
Se si vuole esplicitamente indicare la tabella nella quale il campo deve essere memorizzato si può usare l’attributo table:
(name="NOME", table="TABELLANOMI") protected String nome;Come è possibile osservare dalla definizione dell’annotazione, questa supporta anche altri attributi:
({METHOD, FIELD}) (RUNTIME) public Column { String name() default ""; boolean unique() default false; boolean nullable() default true; boolean insertable() default true; boolean updatable() default true; String columnDefinition() default ""; String table() default ""; int length() default 255; int precision() default 0; int scale() default 0; }In particolare parametri insertable e updatable sono usati per gestire la persistenza.
Se il parametro insertable è posto a false allora il campo o la proprietà non verrà incluso nello statement INSERT generato dal persistence provider per creare un nuovo record corrispondente all’entità.
Analogamente, il parametro updatable posto a false allora il corrispondente campo viene escluso dall’istruzione di UPDATE.
(name="ID", insertable=false, updatable=false) protected Long userId;Gli altri parametri dell’annotazione sono utilizzati soltanto nella generazione automatica delle tabelle: ad esempio il parametro nullable specifica se una colonna supporta valori null, il parametro unique specifica che i valori memorizzati nella colonna devono essere tutti diversi fra loro, il parametro length specifica la dimensione della colonna, precision la precisione di un campo decimale, scale la scala della colonna decimale e columnDefinition consente di specificare il codice SQL esatto da utilizzare nella creazione della colonna.
L’annotazione è stata introdotta per il mapping dei tipi enumeration.
Supponiamo di aver definito un tipo enumeration Statura nel seguente modo:
public enum Statura {MOLTO BASSO, BASSO, NORMALE, ALTO, MOLTO ALTO};Ad ogni elemento dell’enumeration, come è noto, è associato un indice chiamato ordinale: per esempio MOLTO BASSO ha ordinale 0, NORMALE 2 e così via.
Java Persistence API supporta due distinti modi per effettuare il mapping di un tipo enumeration, entrambi specificati con l’annotazione .
Il primo modo, specificato da EnumType.ORDINAL, è quello di default e memorizza l’ordinale del tipo enumeration:
(EnumType.ORDINAL) protected Statura statura;Il secondo, specificato da EnumType.STRING, memorizza invece l’enumeration come una stringa:
(EnumType.STRING) protected Statura statura;Una caratteristica molto potente dei database relazionali è rappresentata dalla possibilità di memorizzare binary large object (BLOB) o character large object (CLOB).
In entrambi i casi si fa uso dell’annotazione @Lob, il tipo di LOB, ovvero CLOB o BLOB, viene determinato in base al tipo del campo: se questo è un char[] allora il persistence provider mapperà il dato in un CLOB altrimenti in un BLOB.
@Lob protected byte[] immagine;Molti database supportano diversi tipi di dati temporali di differente granularità come DATE, TIME e TIMESTAMP.
L’annotazione specifica in quale di questi tre tipi vogliamo mappare un campo persistente di tipo java.util.Date o java.util.Calendar.
(TemporalType.DATE) protected Date dataCreazione;Per i tipi java.sql.Date, java.sql.Time e java.sql.Timestamp questa annotazione è ridondante.
Gli eventuali oggetti annotati con vengono mappati nella stessa tabella utilizzata per il mapping dell’entity bean includente pertanto in questo caso non è necessario specificare alcuna annotazione ma soltanto il mapping delle colonne mediante l’annotazione .
Mapping su tabelle multiple
A volte si ha la necessità di mappare le entità su più tabelle, l’annotazione consente di raggiungere questo scopo:({TYPE}) (RUNTIME) public SecondaryTable { String name(); String catalog() default ""; String schema() default ""; PrimaryKeyJoinColumn[] pkJoinColumns() default {}; UniqueConstraint[] uniqueConstraints() default {}; }Come vediamo, a parte la presenza dell’elemento pkJoinColumns, la definizione dell’annotazione è identica a quella dell’annotazione .
Le due tabelle ovviamente devono essere collegate così da rappresentare insieme l’intera entità, il collegamento viene implementato creando una foreign key nella seconda tabella che referenzia la primary key della prima.
La foreign key risulta essere anche la primary key della seconda tabella.
Supponiamo ad esempio di voler memorizzare un campo fotografia dell’entità Utente su una tabella differente dalla tabella UTENTI chiamata FOTO_UTENTI.
Per fare ciò possiamo usare l’annotazione precisando mediante l’attributo pkJoinColumns il campo ID che è primary key della prima tabella:
(name="UTENTI") (name="FOTO_UTENTI",pkJoinColumns=olumn(name=" ID")) public class Utente implements Serializable { ... }Nel caso di dati distribuiti su più tabelle è possibile usare l’annotazione .
Generazione di chiavi primarie
Quando identifichiamo una colonna come primary key essenzialmente chiediamo al database di garantirne l’univocità.Le primary key che consistono di dati business sono dette chiavi naturali, in alternativa è possibile ricorrere a chiavi surrogate che sono utilizzate tipicamente al posto di primary key composte.
Esistono tre differenti strategie per generare i valori di una primary key, tutte e tre i modi sono supportati mediante l’annotazione .
Nella strategia GenerationType.IDENTITY, il valore del campo annotato con @Id non è disponibile prima che l’entità venga salvata nel database perché generato al momento dell’inserimento.
@Id (strategy=GenerationType.IDENTITY) (name="ID") protected Long userId;Le strategie SEQUENCE e TABLE invece uso di un generatore esterno: in particolare devono essere creati un SequenceGenerator o un TableGenerator con i quali viene configurata l’annotazione GeneratedValue.
Per usare la stragegia GenerationType.SEQUENCE è necessario dapprima definire una sequenza nel database.
La definizione di una sequenza varia a seconda del tipo di DBMS utilizzato, su Oracle ad esempio è possibile scrivere:
CREATE SEQUENCE MIASEQUENZA START WITH 1 INCREMENT BY 1;Una volta definita la sequenza è possibile creare un sequence generator che faccia riferimento a tale sequenza:
or(name="MIOGENERATORES",sequenceName="MIASEQUENZA", initialValue=1, allocationSize=1)L’annoatazione or crea un sequence generator nominato MIOGENERATORES che fa riferimento alla sequenza creata sul DBMS.
Il nome della sequenza è fondamentale dal momento che faremo riferimento a questa mediante l’annotazione ; i valori di default per initialValue e allocationSize sono 0 e 50.
Il sequence generator non deve essere necessariamente creato nella stessa entità nella quale è usato, inoltre è condiviso fra tutte le entità del modulo di persistenza pertanto deve avere un nome univoco all’interno del modulo.
Per utilizzare il generatore di sequenza si può usare il seguente codice:
@Id (strategy=GenerationType.SEQUENCE, generator="MIOGENERATORE") (name=”ID") protected Long userId;La strategia GenerationType.TABLE utilizza un table generator in modo del tutto equivalente all’utilizzo di un sequence generator.
Il primo passo consiste nel creare una tabella da utilizzare per la generazione dei valori, il cui formato tipico è il seguente:
CREATE TABLE TABELLA_GENERATORE (NOME_SEQUENZA VARCHAR2(80) NOT NULL, VALORE_SEQUENZA NUMBER(15) NOT NULL, PRIMARY KEY (NOME_SEQUENZA));dove la colonna NOME_SEQUENZA memorizza il nome della sequenza, mentre la colonna VALORE_SEQUENZA memorizza il valore corrente della sequenza.
Quindi si ineriscono i valori iniziali:
INSERT INTO TABELLA_GENERATORE (NOME_SEQUENZA, VALORE_SEQUENZA) VALUES ('MIASEQUENZA', 1);Il TableGenerator è definito da:
(name="MIOGENERATORET", table="TABELLA_GENERATORE", pkColumnName="NOME_SEQUENZA", valueColumnName="VALORE_SEQUENZA", pkColumnValue="MIASEQUENZA")Se necessario si possono specificare initialValue e allocationSize.
Infine il TableGenerator viene usato nel seguente modo:
@Id (strategy=GenerationType.TABLE, generator="MIOGENERATORET") (name="ID") protected Long userId;Il vantaggio di questo approccio è rappresentato però dal fatto che nella stessa tabella è possibile definire sequenze multiple.
L’ultima opzione per la generazione automatica di primary key consiste nel lasciare al provider la decisione circa la strategia da utilizzare:
@Id (strategy=GenerationType.AUTO) (name="ID") protected Long userId;
Mapping di relazioni uno a uno
Anche se la normalizzazione suggerisce di fondere le tabelle che partecipano ad una relazione uno ad uno in una sola tabella, tali relazioni sono tipicamente mappate mediante associazioni primary key/foreign key.Se la tabella corrispondente all’entità referenziante è l’unica contenente la foreign key alla tabella corrispondente all’entità referenziata allora si usa l’annotazione .
Nel seguente esempio l’entità Utente contiene la foreign key ID_INFO_UTENTE che fa riferimento alla primary key ID della tabella INFO_UTENTi:
(name="UTENTI") public class Utente { @Id (name="ID") protected String userId; ... (name="ID_INFO_UTENTE", referencedColumnName="ID", updatable=false) protected InfoUtente info; } (name="INFO_UTENTI") public class InfoUtente { @Id (name="ID") protected Long infoId; ... }L’elemento name dell’annotazione fa riferimento al nome della foreign key nella tabella UTENTI.
L’elemento referencedColumnName specifica il nome della primary key della tabella INFO_UTENTI alla quale la foreign key si riferisce.
Come l’annotazione l’annotazione contiene gli elementi updatable, insertable, table e unique.
Se si ha più di una colonna nella foreign key si può usare l’annotazione (anche se tale situazione è poco probabile o comunque facilmente evitabile mediante chiavi surrogate).
Se la relazione è bidirezionale la parte opposta della relazione conterrà l’elemento mappedBy
public class InfoUtente { (mappedBy="info") protected Utente utente; ... }Nel caso in cui la foreign key appartiene alla tabella nella quale viene mappata l’entità referenziata, occorre utilizzare l’annotazione olumn.
Tale annotazione viene usata nelle relazioni uno ad uno dove entrambe le tabelle hanno come chiave primaria la primary key della tabella referenziante, il che significa che la foreign key della tabella corrispondente all’entità referenziata è anche primary key:
(name="UTENTI") public class Utente { @Id (name="ID") protected Long userId; ... olumn(name="ID", referencedColumnName="ID") protected InfoUtente info; } (name="INFO_UTENTI") public class InfoUtente { @Id (name="ID") protected Long userId; ... }L’elemento name dell’annotazione olumn fa riferimento alla primary key della tabella in cui viene memorizzata l’entità referenziante.
L’elemento referencedColumnName invece fa riferimento alla foreign key dell’entità referenziata.
Se i nomi delle chiavi sono gli stessi l’elemento referencedColumnName può essere omesso (nell’esempio poteva essere omesso).
Se si hanno chiavi composte si può usare olumns (ma è sempre bene evitare usando chiavi surrogate).
Mapping di relazioni uno a molti e molti a uno
Le relazioni uno a molti e molti a uno sono implementate mediante le annotazioni e .Nel seguente esempio, dal momento che più istanze di Articolo possono fare riferimento alla stessa istanza di Categoria, la tabella ARTICOLI conterrà una foreign key CATEGORIA_ID che fa riferimento alla primary key ID della tabella CATEGORIE.
(name="CATEGORIE") public class Categoria { @Id (name="ID") protected Long id; ... (mappedBy="categoria") protected SetLa relazione molti a uno è espressa in ORM mediante l’annotazione : l’elemento name specifica la foreign key, CATEGORIA_ID, e l’elemento referencedColumnName specifica la primary key ID.articoli; ... } (name="ARTICOLI") public class Articolo { @Id (name="ID") protected Long idArt; ... (name="CATEGORIA_ID", referencedColumnName="ID") protected Categoria categoria; ... }
Per specificare il fatto che la relazione è bidirezionale occorre usare l’elemento mappedBy dalla parte opposta della relazione.
Le foreign key possono fare riferimento alla primary key della stessa tabella nella quale risiedono come avviene ad esempio nel caso di categorie organizzate gerarchicamente:
(name="CATEGORIE") public class Categoria implements Serializable { @Id (name="ID") protected Long id; ... (name="PARENT_ID",referencedColumnName="ID") Category categoriaSuperiore; }
Mapping di relazioni molti a molti
Una relazione molti a molti viene implementata mediante una terza tabella che memorizza i riferimenti alle primary key delle due tabelle in relazione fra loro.(name="ASSOCIAZIONI") public class Associazione implements Serializable { @Id (name="ID") protected Long id; (name="ASSOCIAZIONI_PERSONE",joinColumns=(name="ID_ASSOCIAZIONE", referencedColumnName="ID"),inverseJoinColumns=(name="ID_PERSONA", referencedColumnName="ID")) protected SetL’attributo name dell’annotazione specifica la tabella che memorizza i riferimenti alle primary key delle tabelle PERSONE e ASSOCIAZIONI.persone; ... } (name="PERSONE") public class Persona implements Serializable { @Id (name="ID") protected Long id; ... (mappedBy="persone") protected Set associazioni; ... }
Questa tabella contiene solo due sole colonne ID_ASSOCIAZIONE e ID_PERSONA che sono le foreign key che fanno riferimento alle primary key delle due tabelle.
Gli elementi joinColumns e inverseJoinColumns fanno riferimento alle primary key, il primo descrive il proprietario della relazione (tale distinzione nel caso di una relazione molti a molti è arbitraria).
L’elemento mappedBy viene usato esclusivamente nel caso di una relazione bidirezionale.
Mapping dell'ereditarietà
Esistono sostanzialmente tre strategie per mappare le relazioni di ereditarietà fra le entità all’interno di un database relazionale:- mappare tutte le classi della gerarchia nella stessa tabella
- usare tabelle distinte per la superclasse e le sottoclassi e collegarle fra loro
- usare tabelle completamente separate per ciascun tipo di classe
I differenti tipi di oggetti sono identificati usando una particolare colonna detta colonna discriminatrice.
Supponiamo ad esempio di avere un’entità Utente dalla quale ereditano le entità Giocatore e Amministratore, e di voler mappare tutte le entità nella tabella UTENTI usando un campo TIPO_UTENTE come discriminatore.
La tebella UTENTI conterrà quindi tutti i dati comuni a tutti gli utenti e una colonna TIPO_UTENTE che conterrà una G per “giocatore” o una “A” per amministratore.
Il persistence provider mapperà per ciascun utente soltanto i dati relativi alla classe di appartenenza ponendo a null le altre colonne e settando opportunamente il valore del compo discriminatore.
Il seguente codice mostra invece come viene effettuato il mapping:
(name="UTENTI") (strategy=InheritanceType.SINGLE_TABLE) lumn(name="TIPO_UTENTE", discriminatorType=DiscriminatorType.STRING, length=1) public abstract class Utente ... lue(value="G") public class Giocatore extends Utente ... lue(value="A") public class Amministratore extends Utente ...Come vediamo la strategia di ereditarietà e la colonna discriminatrice sono specificate alla radice dell’entità della gerarchia mediante l’annotazione e lumn: nel nostro caso si utilizza la strategia SINGLE_TABLE con discriminatore TIPO_UTENTE.
In particolare l’annotazione lumn consente di specificare il nome della colonna (name), il tipo (discriminatorType) e la lunghezza (length).
Entrambe le sottoclassi quindi specificano il valore del discriminatore mediante l’annotazione lue e l’elemento value.
Questa strategia è quella impiegata di default in EJB 3, è semplice da usare ma ha un grande svantaggio: non usa la potenza dei database relazionali in termini di primary e foreign key.
La strategia joined-tables usa relazioni uno a uno per modellare l’ereditarietà.
Questa strategia comporta la creazione di tabelle separate per ogni entità della gerarchia e collega i discendenti con relazioni uno a uno.
La tabella relativa alla superclasse contiene soltanto le colonne comuni ai figli, mentre le tabelle figlie contengono esclusivamente le colonne specifiche del sottotipo.
L’ereditarietà viene implementata mediante una relazione uno a uno fra padre e figlio.
(name="UTENTI") (strategy=InheritanceType.JOINED) lumn(name="TIPO_UTENTE", discriminatorType=STRING, length=1) public abstract class Utente ... (name="GIOCATORI") lue(value="G") olumn(name="ID") public class Giocatore extends Utente ... (name="AMMINISTRATORI") lue(value="A") olumn(name=" ID") public class Amministratore extends Utente ...Le annotazioni lumn e lue sono usate esattamente come nella strategia con singola tabella.
L’annotazione imposta la strategia a JOINED.
Inoltre viene implementata una relazione uno a uno tra tabella madre e tabella figlia mediante l’annotazione olumn.
Dal punto di vista della progettazione, questa strategia costituisce la scelta migliore, ma risulta computazionalmente più onerosa della prima (perché richiede operazioni di join su più tabelle).
L’ultima strategia prevede che ogni classe venga memorizzata nella propria tabella.
I dati delle entità sono memorizzate nelle corrispondenti tabelle anche se sono ereditati dalla superclasse.
Le primary key in tutte le tabelle devono essere mutuamente esclusive.
(name="UTENTI") (strategy=InheritanceType.TABLE_PER_CLASS) public class Utente ... (name="GIOCATORI") public class Giocatore extends Utente ... (name="AMMINISTRATORI") public class Amministratore extends Utente ...La strategia di ereditarietà TABLE_PER_CLASS è specificata nell’entità superclasse User, però tutti i dati nella gerarchia sono mappati in tabelle separate (ognuno fa uso di ).
A standardized object-relational mapping mechanism for the Java platform
Object-Relational Mapping (ORM) standard nella piattaforma Java
Object-Relational Mapping (ORM) standard nella piattaforma Java
Defining your Object Model with JPA
Esempio di utilizzo di JPA e di applicazione dell'Object-Relational Mapping (ORM)
Esempio di utilizzo di JPA e di applicazione dell'Object-Relational Mapping (ORM)