Mapping di Gerarchie con Hibernate (parte 1)

Nell’articolo Mapping delle Relazioni in Hibernate abbiamo visto come mappare le principali relazioni che possono esistere tra le tabelle (entità) che costituiscono la base dati dell’applicazione. Nella programmazione orientata agli oggetti c’è però un tipo di relazione tra classi che non trova una corrispondenza immediata con il mondo dei database. Nello specifico ci riferiamo alla gerarchia tra classi per la quale non esiste una soluzione di mapping univoca ma JPA fornisce diverse possibili strategie:

MappingSuperclass Implementa una gerarchia che è visibile esclusivamente nel modello delle classi ma non in quello delle entità
Single Table Tabella unica per tutte le entità della gerarchia.
Joined Table Ogni entità della gerarchia ha associata una tabella ciascuna con le proprie proprietà/colonne.
Table-Per-Class Ogni entità della gerarchia ha associata una tabella ciascuna con tutte le proprietà/colonne comuni.

Modello Dati

Prima di discutere le varie strategie descriviamo brevemente il modello dati che utilizzeremo nei vari esempi. Consideriamo quindi gli asset aziendali che per loro natura possono essere di diverso tipo ed avere caratteristiche specifiche. Nel nostro caso consideriamo esclusivamente Laptop e Cellulari, e implementiamo la gerarchia di esempio attraverso tre classi java: AssetLaptop e MobileNumber, la cui relazione gerarchica è mostrata nel seguente diagramma UML:

Configurazione del Progetto

Come al solito utilizziamo un progetto Maven ma in questo caso definiremo quattro moduli, uno per ogni strategia, ciascuno con le proprie classi Asset, Laptop e MobilePhone ma annotate in modo diverso in funzione della strategia. Nel pom.xml del progetto padre inseriamo le dipendenze ad Hibernate e al database H2.

In ogni cartella resources dei diversi moduli inseriamo, inoltre, il file hibernate.cfg.xml di configurazione per Hibernate, che sostanzialmente sarà uguale per tutti i moduli ad eccezione per la porzione associata mapping delle entità.

La prima parte definisce i parametri di connessione al database, il dialetto ed altre proprietà che non ci interessa descrivere. Si noti invece che la proprietà show_sql è valorizzata a TRUE al fine di visualizzare le query eseguite sul db, mentre la proprietà hbm2ddl.auto assume il valore CREATE al fine di rigenerare il db ad ogni avvio dell’applicazione.

Infine, in ogni modulo troverete una classe HibernateMain con il main() per l’esecuzione di un test di inserimento e recupero di tutte le entità dalla base dati. Si faccia attenzione a non utilizzare Java 9 per l’esecuzione degli esempi, al fine di non incorrere nell’eccezione java.lang.ClassNotFoundException: javax.xml.bind.JAXBException causato dal fatto che le API JaxB non sono più fornite automaticamente dal JDK.

MappingSuperclass

Nella strategia MappingSuperclass non a tutte le classi della gerarchia corrisponde una entità sulla base dati. Nel nostro caso, ad esempio, sarà la classe Asset a non avere una tabella corrispondente, quindi nella base dati troveremo esclusivamente le tabelle associate a Laptop e MobilePhone, ciascuna con un numero di colonne sufficiente a mappare sia le proprie proprietà sia quelle ereditate da Asset.

L’implementazione delle tre classi con tale strategia si realizza molto annotando Asset con @MappedSuperclass, mentre l’annotazione @Entity sarà utilizzata esclusivamente nelle classi derivate:

Eseguendo il main() associato a tale modulo troverete nell’output generato diverse informazioni:

La cancellazione della base dati e la generazione delle tabelle LAPTOP e MOBILE_PHONE (e relative sequence per gli indici).

L’inserimento di 4 record sulla base dati, due per ogni tipologia di asset.
La select di recupero dei record inseriti.

Single Table

Si tratta della strategia che Hibernate applica di default e consiste nel mappare tutte le classi della gerarchia in una unica entità nel database. Tale tabella conterrà quindi tante colonne quante sono le proprietà presenti in tutte le classi, più una colonna (DTYPE) utilizzata dal framework per discriminare l’appartenenza della riga ad una o all’altra entità.

La strategia che si intende utilizzare è dichiarata attraverso l’annotation @Inheritance con cui deve essere decorata cal classe genitrice (Asset).

Si noti inoltre che nelle classi Laptop e MobilePhone sono state eliminate le annotation @Table che comunque sarebbero state ignorate. Il framework infatti in questo caso genera una unica tabella Asset.

Eseguiamo il main() associato al modulo e nella consolle troveremo le seguenti informazioni:

Cancellazione della base dati e rigenerazione dell’unica tabella Asset e relativa sequence per l’indice.

Salvataggio di 4 record sulla base dati, tutti inseriti nell’unica tabella Asset con DTYPE valorizzato in funzione del tipo di asset.

Select di recupero dei record inseriti.

Discriminator

Come detto precedentemente la colonna DTYPE di tipo varchar è aggiunta dal framework ed è valorizzata con i nomi delle entità cui la riga si riferisce. E’ possibile modificare tale comportamento utilizzando una propria colonna di discriminazione con valori specifici. Tale personalizzazione si realizza attraverso l’annotazione @DiscriminatorColumn sulla classe Asset e l’annotazione @DiscriminatorValue su ciascuna delle entità derivate Laptop e MobilePhone.

Si noti che è stato necessario annotare anche la classe Asset con @DiscriminatorValue altrimenti sarebbe stata generata l’eccezione:

org.hibernate.AnnotationException: Using default @DiscriminatorValue for a discriminator of type CHAR is not safe

Per completezza riportiamo l’output dell’esecuzione della classe main() limitatamente alla fase di inserimento dei record in cui si evidenzia l’utilizzo della nuova colonna type automaticamente valorizzata da Hibernate in funzione del tipo di asset.

Codice Sorgente

Il codice sorgente completo degli esempi visti sino ad ora è scaricabile qui hibernate.