Mapeo de herencia de Hibernate

1. Información general

Las bases de datos relacionales no tienen una forma sencilla de asignar jerarquías de clases a tablas de bases de datos.

Para abordar esto, la especificación JPA proporciona varias estrategias:

  • MappedSuperclass : las clases principales, no pueden ser entidades
  • Tabla única: las entidades de diferentes clases con un antepasado común se colocan en una sola tabla
  • Tabla unida: cada clase tiene su tabla y consultar una entidad de subclase requiere unirse a las tablas
  • Table-Per-Class: todas las propiedades de una clase están en su tabla, por lo que no se requiere combinación

Cada estrategia da como resultado una estructura de base de datos diferente.

La herencia de entidades significa que podemos usar consultas polimórficas para recuperar todas las entidades de la subclase al consultar una superclase.

Dado que Hibernate es una implementación de JPA, contiene todo lo anterior, así como algunas características específicas de Hibernate relacionadas con la herencia.

En las siguientes secciones, repasaremos las estrategias disponibles con más detalle.

2. MappedSuperclass

Usando la estrategia MappedSuperclass , la herencia solo es evidente en la clase, pero no en el modelo de entidad.

Comencemos creando una clase Person que representará una clase padre:

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

Tenga en cuenta que esta clase ya no tiene una anotación @Entity , ya que no se mantendrá en la base de datos por sí misma.

A continuación, agreguemos una subclase de empleado :

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

En la base de datos, esto corresponderá a una tabla "MyEmployee" con tres columnas para los campos declarados y heredados de la subclase.

Si usamos esta estrategia, los ancestros no pueden contener asociaciones con otras entidades.

3. Mesa única

La estrategia de tabla única crea una tabla para cada jerarquía de clases. Esta es también la estrategia predeterminada elegida por JPA si no especificamos una explícitamente.

Podemos definir la estrategia que queremos usar agregando la anotación @Inheritance a la superclase :

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

El identificador de las entidades también se define en la superclase.

Luego, podemos agregar las entidades de la subclase:

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

3.1. Valores discriminadores

Dado que los registros de todas las entidades estarán en la misma tabla, Hibernate necesita una forma de diferenciarlos.

De forma predeterminada, esto se hace a través de una columna discriminadora llamada DTYPE que tiene el nombre de la entidad como valor.

Para personalizar la columna discriminadora, podemos usar la anotación @DiscriminatorColumn :

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

Aquí hemos optado por diferenciar las entidades de la subclase MyProduct por una columna entera llamada product_type .

A continuación, necesitamos decirle a Hibernate qué valor tendrá cada registro de subclase para la columna product_type :

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

Hibernate agrega otros dos valores predefinidos que la anotación puede tomar: " nulo " y " no nulo ":

  • @DiscriminatorValue ("nulo") : significa que cualquier fila sin un valor discriminador se asignará a la clase de entidad con esta anotación; esto se puede aplicar a la clase raíz de la jerarquía
  • @DiscriminatorValue ("no nulo") : cualquier fila con un valor discriminador que no coincida con ninguno de los asociados con las definiciones de entidad se asignará a la clase con esta anotación

En lugar de una columna, también podemos usar la anotación @DiscriminatorFormula específica de Hibernate para determinar los valores diferenciadores:

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

Esta estrategia tiene la ventaja del rendimiento de las consultas polimórficas, ya que solo es necesario acceder a una tabla cuando se consultan las entidades principales. Por otro lado, esto también significa que ya no podemos usar restricciones NOT NULL en propiedades de entidad de subclase .

4. Mesa unida

Con esta estrategia, cada clase de la jerarquía se asigna a su tabla. La única columna que aparece repetidamente en todas las tablas es el identificador, que se utilizará para unirlas cuando sea necesario.

Creemos una superclase que use esta estrategia:

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

Entonces, simplemente podemos definir una subclase:

@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); }

Tenga en cuenta que esto también funciona para cualquier superclase o interfaz, ya sea @MappedSuperclass o no. La diferencia con una consulta HQL habitual es que tenemos que usar el nombre completo ya que no son entidades administradas por Hibernate.

Si no queremos que una subclase sea devuelta por este tipo de consulta, entonces solo necesitamos agregar la anotación Hibernate @Polymorphism a su definición, con tipo EXPLICIT :

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

En este caso, al consultar artículos, las bolsas no se devolverán registros.

7. Conclusión

En este artículo, mostramos las diferentes estrategias para mapear la herencia en Hibernate.

El código fuente completo de los ejemplos se puede encontrar en GitHub.