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: Asset
, Laptop
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
, Lapto
p 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.
1 2 3 4 5 6 7 8 9 10 11 12 |
<dependencies> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>5.2.16.Final</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.196</version> </dependency> </dependencies> |
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à.
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 29 30 31 32 33 34 |
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <!-- Database connection settings --> <property name="connection.driver_class">org.h2.Driver</property> <property name="connection.url">jdbc:h2:mem:javaboss</property> <property name="connection.username">sa</property> <property name="connection.password"/> <property name="hibernate.default_schema">PUBLIC</property> <!-- JDBC connection pool (use the built-in) --> <property name="connection.pool_size">1</property> <!-- SQL dialect --> <property name="dialect">org.hibernate.dialect.H2Dialect</property> <!-- Disable the second-level cache --> <property name="cache.provider_class">org.hibernate.cache.internal.NoCacheProvider</property> <!-- Echo all executed SQL to stdout --> <property name="show_sql">true</property> <!-- Drop and re-create the database schema on startup --> <property name="hbm2ddl.auto">create</property> <!-- Mapping --> <mapping .../> </session-factory> </hibernate-configuration> |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@MappedSuperclass public class Asset { @Id @GeneratedValue private Long id; ... } @Entity @Table( name = "LAPTOP" ) public class Laptop extends Asset { private String model; ... } @Entity @Table( name = "MOBILE_PHONE" ) public class MobilePhone extends Asset { private String number; ... } |
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).
1 2 3 4 5 6 |
Hibernate: drop table PUBLIC.LAPTOP if exists Hibernate: drop table PUBLIC.MOBILE_PHONE if exists Hibernate: drop sequence if exists PUBLIC.hibernate_sequence Hibernate: create sequence PUBLIC.hibernate_sequence start with 1 increment by 1 Hibernate: create table PUBLIC.LAPTOP (id bigint not null, model varchar(255), primary key (id)) Hibernate: create table PUBLIC.MOBILE_PHONE (id bigint not null, number varchar(255), primary key (id)) |
1 2 3 4 5 6 7 8 |
Hibernate: call next value for PUBLIC.hibernate_sequence Hibernate: call next value for PUBLIC.hibernate_sequence Hibernate: call next value for PUBLIC.hibernate_sequence Hibernate: call next value for PUBLIC.hibernate_sequence Hibernate: insert into PUBLIC.LAPTOP (model, id) values (?, ?) Hibernate: insert into PUBLIC.MOBILE_PHONE (number, id) values (?, ?) Hibernate: insert into PUBLIC.LAPTOP (model, id) values (?, ?) Hibernate: insert into PUBLIC.MOBILE_PHONE (number, id) values (?, ?) |
1 2 3 4 5 6 7 8 |
Hibernate: select laptop0_.id as id1_0_, laptop0_.model as model2_0_ from PUBLIC.LAPTOP laptop0_ 2 asset(s) found: Asset id 1 is a laptop model Acer Asset id 3 is a laptop model Compaq Hibernate: select mobilephon0_.id as id1_1_, mobilephon0_.number as number2_1_ from PUBLIC.MOBILE_PHONE mobilephon0_ 2 asset(s) found: Asset id 2 is a mobile phone number 555-060606 Asset id 4 is a mobile phone number 555-090909 |
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
).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) public class Asset { @Id @GeneratedValue private Long id; .. } @Entity public class Laptop extends Asset { private String model; .. } @Entity public class MobilePhone extends Asset { private String number; ... } |
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.
1 2 3 4 |
Hibernate: drop table PUBLIC.Asset if exists Hibernate: drop sequence if exists PUBLIC.hibernate_sequence Hibernate: create sequence PUBLIC.hibernate_sequence start with 1 increment by 1 Hibernate: create table PUBLIC.Asset (DTYPE varchar(31) not null, id bigint not null, model varchar(255), number varchar(255), primary key (id)) |
Salvataggio di 4 record sulla base dati, tutti inseriti nell’unica tabella Asset
con DTYPE
valorizzato in funzione del tipo di asset.
1 2 3 4 5 6 7 8 |
Hibernate: call next value for PUBLIC.hibernate_sequence Hibernate: call next value for PUBLIC.hibernate_sequence Hibernate: call next value for PUBLIC.hibernate_sequence Hibernate: call next value for PUBLIC.hibernate_sequence Hibernate: insert into PUBLIC.Asset (model, DTYPE, id) values (?, 'Laptop', ?) Hibernate: insert into PUBLIC.Asset (number, DTYPE, id) values (?, 'MobilePhone', ?) Hibernate: insert into PUBLIC.Asset (model, DTYPE, id) values (?, 'Laptop', ?) Hibernate: insert into PUBLIC.Asset (number, DTYPE, id) values (?, 'MobilePhone', ?) |
1 2 3 4 5 6 7 8 |
Hibernate: select laptop0_.id as id2_0_, laptop0_.model as model3_0_ from PUBLIC.Asset laptop0_ where laptop0_.DTYPE='Laptop' 2 asset(s) found: Asset id 1 is a laptop model Acer Asset id 3 is a laptop model Compaq Hibernate: select mobilephon0_.id as id2_0_, mobilephon0_.number as number4_0_ from PUBLIC.Asset mobilephon0_ where mobilephon0_.DTYPE='MobilePhone' 2 asset(s) found: Asset id 2 is a mobile phone number 555-060606 Asset id 4 is a mobile phone number 555-090909 |
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
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name="type", discriminatorType = DiscriminatorType.CHAR) @DiscriminatorValue("null") public class Asset { ... } @Entity @DiscriminatorValue("L") public class Laptop extends Asset { ... } @Entity @DiscriminatorValue("M") public class MobilePhone extends Asset { ... } |
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.
1 2 3 4 5 6 7 8 |
Hibernate: call next value for PUBLIC.hibernate_sequence Hibernate: call next value for PUBLIC.hibernate_sequence Hibernate: call next value for PUBLIC.hibernate_sequence Hibernate: call next value for PUBLIC.hibernate_sequence Hibernate: insert into PUBLIC.Asset (model, type, id) values (?, 'L', ?) Hibernate: insert into PUBLIC.Asset (number, type, id) values (?, 'M', ?) Hibernate: insert into PUBLIC.Asset (model, type, id) values (?, 'L', ?) Hibernate: insert into PUBLIC.Asset (number, type, id) values (?, 'M', ?) |
Codice Sorgente
Il codice sorgente completo degli esempi visti sino ad ora è scaricabile qui hibernate.