Hibernate Inheritance Mapping

1. Panoramica

I database relazionali non hanno un modo semplice per mappare le gerarchie di classi sulle tabelle di database.

Per risolvere questo problema, la specifica JPA fornisce diverse strategie:

  • MappedSuperclass : le classi padre, non possono essere entità
  • Tabella singola: le entità di classi diverse con un antenato comune vengono collocate in una singola tabella
  • Tabella unita: ogni classe ha la sua tabella e l'interrogazione di un'entità di sottoclasse richiede l'unione delle tabelle
  • Table-Per-Class: tutte le proprietà di una classe sono nella sua tabella, quindi non è richiesta alcuna unione

Ogni strategia risulta in una diversa struttura del database.

L'ereditarietà delle entità significa che possiamo utilizzare query polimorfiche per recuperare tutte le entità della sottoclasse quando si esegue una query per una superclasse.

Poiché Hibernate è un'implementazione JPA, contiene tutto quanto sopra e alcune funzionalità specifiche di Hibernate relative all'ereditarietà.

Nelle prossime sezioni, esamineremo le strategie disponibili in modo più dettagliato.

2. MappedSuperclass

Utilizzando la strategia MappedSuperclass , l'ereditarietà è evidente solo nella classe, ma non nel modello di entità.

Cominciamo creando una classe Person che rappresenterà una classe genitore:

@MappedSuperclass public class Person { @Id private long personId; private String name; // constructor, getters, setters }

Si noti che questa classe non ha più un'annotazione @Entity , poiché non verrà mantenuta nel database da sola.

Successivamente, aggiungiamo una sottoclasse Employee :

@Entity public class MyEmployee extends Person { private String company; // constructor, getters, setters }

Nel database, questo corrisponderà a una tabella "MyEmployee" con tre colonne per i campi dichiarati ed ereditati della sottoclasse.

Se stiamo usando questa strategia, gli antenati non possono contenere associazioni con altre entità.

3. Tavolo unico

La strategia Tabella singola crea una tabella per ogni gerarchia di classi. Questa è anche la strategia predefinita scelta da JPA se non ne specifichiamo una esplicitamente.

Possiamo definire la strategia che vogliamo utilizzare aggiungendo l' annotazione @Inheritance alla superclasse :

@Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) public class MyProduct { @Id private long productId; private String name; // constructor, getters, setters }

Anche l'identificativo delle entità è definito nella superclasse.

Quindi, possiamo aggiungere le entità della sottoclasse:

@Entity public class Book extends MyProduct { private String author; }
@Entity public class Pen extends MyProduct { private String color; }

3.1. Valori discriminatori

Poiché i record per tutte le entità saranno nella stessa tabella, Hibernate necessita di un modo per differenziarli.

Per impostazione predefinita, questo viene fatto tramite una colonna discriminatore chiamata DTYPE che ha il nome dell'entità come valore.

Per personalizzare la colonna del discriminatore, possiamo utilizzare l' annotazione @DiscriminatorColumn :

@Entity(name="products") @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name="product_type", discriminatorType = DiscriminatorType.INTEGER) public class MyProduct { // ... }

Qui abbiamo scelto di differenziare le entità di sottoclasse MyProduct da una colonna intera chiamata product_type .

Successivamente, dobbiamo dire a Hibernate quale valore avrà ogni record di sottoclasse per la colonna product_type :

@Entity @DiscriminatorValue("1") public class Book extends MyProduct { // ... }
@Entity @DiscriminatorValue("2") public class Pen extends MyProduct { // ... }

Hibernate aggiunge altri due valori predefiniti che l'annotazione può assumere: " null " e " not null ":

  • @DiscriminatorValue ("null") - significa che qualsiasi riga senza un valore discriminatore verrà mappata alla classe entità con questa annotazione; questo può essere applicato alla classe radice della gerarchia
  • @DiscriminatorValue ("not null") - qualsiasi riga con un valore discriminatore che non corrisponde a nessuno di quelli associati alle definizioni di entità verrà mappata alla classe con questa annotazione

Invece di una colonna, possiamo anche utilizzare l' annotazione @DiscriminatorFormula specifica di Hibernate per determinare i valori di differenziazione:

@Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorFormula("case when author is not null then 1 else 2 end") public class MyProduct { ... }

Questa strategia ha il vantaggio di prestazioni di query polimorfiche poiché è necessario accedere a una sola tabella quando si interrogano le entità padre. D'altra parte, questo significa anche che non possiamo più usare i vincoli NOT NULL sulle proprietà dell'entità della sottoclasse .

4. Tabella unita

Utilizzando questa strategia, ogni classe nella gerarchia viene mappata alla relativa tabella. L'unica colonna che appare ripetutamente in tutte le tabelle è l'identificativo, che verrà utilizzato per unirle quando necessario.

Creiamo una superclasse che utilizzi questa strategia:

@Entity @Inheritance(strategy = InheritanceType.JOINED) public class Animal { @Id private long animalId; private String species; // constructor, getters, setters }

Quindi, possiamo semplicemente definire una sottoclasse:

@Entity public class Pet extends Animal { private String name; // constructor, getters, setters }

Both tables will have an animalId identifier column. The primary key of the Pet entity also has a foreign key constraint to the primary key of its parent entity. To customize this column, we can add the @PrimaryKeyJoinColumn annotation:

@Entity @PrimaryKeyJoinColumn(name = "petId") public class Pet extends Animal { // ... }

The disadvantage of this inheritance mapping method is that retrieving entities requires joins between tables, which can result in lower performance for large numbers of records.

The number of joins is higher when querying the parent class as it will join with every single related child – so performance is more likely to be affected the higher up the hierarchy we want to retrieve records.

5. Table per Class

The Table Per Class strategy maps each entity to its table which contains all the properties of the entity, including the ones inherited.

The resulting schema is similar to the one using @MappedSuperclass, but unlike it, table per class will indeed define entities for parent classes, allowing associations and polymorphic queries as a result.

To use this strategy, we only need to add the @Inheritance annotation to the base class:

@Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) public class Vehicle { @Id private long vehicleId; private String manufacturer; // standard constructor, getters, setters }

Then, we can create the sub-classes in the standard way.

This is not very different from merely mapping each entity without inheritance. The distinction is apparent when querying the base class, which will return all the sub-class records as well by using a UNION statement in the background.

The use of UNION can also lead to inferior performance when choosing this strategy. Another issue is that we can no longer use identity key generation.

6. Polymorphic Queries

As mentioned, querying a base class will retrieve all the sub-class entities as well.

Let's see this behavior in action with a JUnit test:

@Test public void givenSubclasses_whenQuerySuperclass_thenOk() { Book book = new Book(1, "1984", "George Orwell"); session.save(book); Pen pen = new Pen(2, "my pen", "blue"); session.save(pen); assertThat(session.createQuery("from MyProduct") .getResultList()).hasSize(2); }

In this example, we've created two Book and Pen objects, then queried their super-class MyProduct to verify that we'll retrieve two objects.

Hibernate can also query interfaces or base classes which are not entities but are extended or implemented by entity classes. Let's see a JUnit test using our @MappedSuperclass example:

@Test public void givenSubclasses_whenQueryMappedSuperclass_thenOk() { MyEmployee emp = new MyEmployee(1, "john", "baeldung"); session.save(emp); assertThat(session.createQuery( "from com.baeldung.hibernate.pojo.inheritance.Person") .getResultList()) .hasSize(1); }

Nota che questo funziona anche per qualsiasi superclasse o interfaccia, che si tratti di una @MappedSuperclass o meno. La differenza da una normale query HQL è che dobbiamo utilizzare il nome completo poiché non sono entità gestite da Hibernate.

Se non vogliamo che una sottoclasse venga restituita da questo tipo di query, dobbiamo solo aggiungere l' annotazione Hibernate @Polymorphism alla sua definizione, con il tipo EXPLICIT :

@Entity @Polymorphism(type = PolymorphismType.EXPLICIT) public class Bag implements Item { ...}

In questo caso, durante la ricerca di elementi, i Bag non saranno restituiti i record.

7. Conclusione

In questo articolo, abbiamo mostrato le diverse strategie per mappare l'ereditarietà in Hibernate.

Il codice sorgente completo degli esempi può essere trovato su GitHub.