Guía de JPA con Hibernate - Mapeo de herencia

En este artículo, nos sumergiremos en la asignación de herencia con JPA e Hibernate en Java. La API de persistencia de Java (JPA) es el estándar de persistencia de Java ec...

Introducción

En este artículo, nos sumergiremos en Asignación de herencia con JPA e Hibernate en Java.

La API de persistencia de Java (JPA) es el estándar de persistencia del ecosistema de Java. Nos permite mapear nuestro modelo de dominio directamente a la estructura de la base de datos y luego nos brinda la flexibilidad de manipular solo objetos en nuestro código. Esto nos permite no jugar con componentes JDBC engorrosos como Connection, ResultSet, etc.

Haremos una guía completa para usar JPA con Hibernate como su proveedor. En este artículo, exploraremos el mapeo de herencia en Hibernate.

  • Guía de JPA con Hibernate: Mapeo básico
  • Guide to JPA with Hibernate: Mapeo de relaciones
  • Guía de JPA con Hibernate: Mapeo de herencia (¡ya estás aquí!)
  • Guía de JPA con Hibernate: consulta (próximamente!)

Asignación de herencia

El mapeo básico, como el mapeo de campos de un objeto o el mapeo de relaciones, donde mapeamos la relación entre diferentes tablas, es muy común, y usará estas técnicas en casi todas las aplicaciones que está creando. Un poco más raramente, mapearás jerarquías de clases.

La idea aquí es manejar el mapeo de jerarquías de clases. JPA ofrece múltiples estrategias para lograrlo, y repasaremos cada una de ellas:

  • Superclase mapeada
  • Mesa Individual
  • Una mesa por clase (de hormigón)
  • Mesa unida

Modelo de dominio

En primer lugar, agreguemos algo de herencia en nuestro modelo de dominio:

domain-1

Como podemos ver, introdujimos la clase ‘Persona’, que es una superclase de ‘Profesor’ y ‘Estudiante’ y contiene nombres y fecha de nacimiento, así como dirección y sexo.

Además de eso, agregamos la jerarquía Vehicle para administrar los vehículos de los maestros para la administración del estacionamiento.

Pueden ser Coche o Moto. Cada vehículo tiene una matrícula, pero un automóvil puede funcionar con GLP (que está prohibido en ciertos niveles del estacionamiento) y las motocicletas pueden tener un sidecar (que requiere el espacio de estacionamiento de un automóvil).

Superclase asignada

Comencemos con uno simple, el enfoque de superclase mapeada. Una superclase asignada es una clase que no es una entidad sino una que contiene asignaciones. Es el mismo principio que las clases incrustadas, pero se aplica a la herencia.

Entonces, digamos que queremos mapear nuestras nuevas clases para manejar el estacionamiento de los maestros en la escuela, primero definiríamos la clase Vehicle, anotada con @MappedSuperclass:

1
2
3
4
5
@MappedSuperclass
public class Vehicle {
    @Id
    private String licensePlate;
}

Solo contiene el identificador, anotado con @Id, que es la matrícula del vehículo.

Ahora, queremos mapear nuestras dos entidades: Car y Motorcycle. Ambos se extenderán desde Vehicle y heredarán licensePlate:

1
2
3
4
5
6
7
8
9
@Entity
class Car extends Vehicle {
    private boolean runOnLpg;
}

@Entity
class Motorcycle extends Vehicle {
    private boolean hasSideCar;
}

Bien, hemos definido las entidades ahora, y heredan de Vehicle. Sin embargo, ¿qué sucede en el lado de la base de datos? JPA genera estas definiciones de tabla:

1
2
create table Car (licensePlate varchar(255) not null, runOnLpg boolean not null, primary key (licensePlate))
create table Motorcycle (licensePlate varchar(255) not null, hasSideCar boolean not null, primary key (licensePlate))

Cada entidad tiene su propia tabla, ambas con una columna licensePlate, que también es la clave principal de estas tablas. No hay tabla Vehículo. @MappedSuperclass no es una entidad. De hecho, una clase no puede tener aplicadas las anotaciones @Entity y @MappedSuperclass.

¿Cuáles son las consecuencias de que Vehicle no sea una entidad? Bueno, no podemos buscar un Vehículo usando EntityManager.

Agreguemos algunos autos y una motocicleta:

1
2
3
insert into CAR(LICENSEPLATE, RUNONLPG) values('1 - ABC - 123', '1');
insert into CAR(LICENSEPLATE, RUNONLPG) values('2 - BCD - 234', '0');
insert into MOTORCYCLE(LICENSEPLATE, HASSIDECAR) values('M - ABC - 123', '0');

Intuitivamente, es posible que desee buscar un ‘Vehículo’ con la matrícula ‘1 - ABC - 123’:

1
assertThrows(Exception.class, () -> entityManager.find(Vehicle.class, "1 - ABC - 123"));

Y esto lanzará una excepción. No hay entidades ‘Vehículo’ persistentes. Sin embargo, hay entidades Car persistentes. Busquemos un Auto con esa matrícula:

1
2
3
4
5
Car foundCar = entityManager.find(Car.class, "1 - ABC - 123");

assertThat(foundCar).isNotNull();
assertThat(foundCar.licensePlate()).isEqualTo("1 - ABC - 123");
assertThat(foundCar.runOnLpg()).isTrue();

Estrategia de mesa única

Pasemos ahora a la estrategia de mesa única. Esta estrategia nos permite mapear todas las entidades de una jerarquía de clases a la misma tabla de base de datos.

Si reutilizamos nuestro ejemplo de estacionamiento, eso significaría que los autos y las motocicletas se guardarían en una tabla VEHÍCULO.

Para configurar esta estrategia, necesitaremos algunas anotaciones nuevas que nos ayuden a definir esta relación:

  • @Inheritance - que define la estrategia de herencia y se usa para todas las estrategias excepto para las superclases mapeadas.
  • @DiscriminatorColumn - que define una columna cuyo propósito será determinar qué entidad se guarda en una fila de base de datos determinada. Marcaremos esto como TYPE, indicando el tipo de vehículo.
  • @DiscriminatorValue - que define el valor de la columna del discriminador para una entidad dada - así, si esta entidad dada es un Automóvil o Moto.

Esta vez, el Vehículo es un JPA @Entity administrado, ya que lo estamos guardando en una tabla. Agreguemos también las anotaciones @Inheritance y @DiscriminatorColumn:

1
2
3
4
5
6
7
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "TYPE")
public class Vehicle {
    @Id
    private String licensePlate;
}

La anotación @Inheritance acepta un indicador strategy, que hemos establecido en InheritanceType.SINGLE_TABLE. Esto le permite a JPA saber que hemos optado por el enfoque de tabla única. Este tipo también es el tipo predeterminado, por lo que incluso si no hubiéramos especificado ninguna estrategia, aún sería SINGLE_TABLE.

También configuramos el nombre de nuestra columna discriminadora para que sea TYPE (el valor predeterminado es DTYPE). Ahora, cuando JPA genere tablas, se verá así:

1
create table Vehicle (TYPE varchar(31) not null, licensePlate varchar(255) not null, hasSideCar boolean, runOnLpg boolean, primary key (licensePlate))

Eso tiene algunas consecuencias:

  • Los campos tanto para ‘Automóvil’ como para ‘Motocicleta’ se almacenan en la misma tabla, lo que puede complicarse si tenemos muchos campos.
  • Todos los campos de la subclase tienen que ser anulables (porque un ‘Automóvil’ no puede tener valores para los campos de una ‘Motocicleta’, y viceversa), lo que significa menos validación en el nivel de la base de datos.

Dicho esto, vamos a mapear nuestro ‘Auto’ y ‘Moto’ ahora:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Entity
@DiscriminatorValue("C")
class Car extends Vehicle {
    private boolean runOnLpg;
}

@Entity
@DiscriminatorValue("M")
class Motorcycle extends Vehicle {
    private boolean hasSideCar;
}

Aquí, estamos definiendo los valores de la columna discriminadora para nuestras entidades. Elegimos C para automóviles y M para motocicletas. Por defecto, JPA usa el nombre de las entidades. En nuestro caso, Coche y Moto, respectivamente.

Ahora, agreguemos algunos vehículos y veamos cómo el EntityManager los trata:

1
2
3
insert into VEHICLE(LICENSEPLATE, TYPE, RUNONLPG, HASSIDECAR) values('1 - ABC - 123', 'C', '1', null);
insert into VEHICLE(LICENSEPLATE, TYPE, RUNONLPG, HASSIDECAR) values('2 - BCD - 234', 'C', '0', null);
insert into VEHICLE(LICENSEPLATE, TYPE, RUNONLPG, HASSIDECAR) values('M - ABC - 123', 'M', null, '0');

Por un lado, podemos recuperar cada entidad Coche o Moto:

1
2
3
4
5
Car foundCar = entityManager.find(Car.class, "1 - ABC - 123");

assertThat(foundCar).isNotNull();
assertThat(foundCar.licensePlate()).isEqualTo("1 - ABC - 123");
assertThat(foundCar.runOnLpg()).isTrue();

Pero, dado que Vehicle también es una entidad, también podemos recuperar entidades como su superclase - Vehicle:

1
2
3
4
Vehicle foundCar = entityManager.find(Vehicle.class, "1 - ABC - 123");

assertThat(foundCar).isNotNull();
assertThat(foundCar.licensePlate()).isEqualTo("1 - ABC - 123");

De hecho, incluso podemos guardar una entidad Vehículo que no sea ni un Coche ni una Moto:

1
2
3
4
Vehicle vehicle = new Vehicle();
vehicle.setLicensePlate("T - ABC - 123");

entityManager.persist(vehicle);

Lo que se traduce en la siguiente consulta SQL:

1
insert into Vehicle (TYPE, licensePlate) values ('Vehicle', ?)

Aunque es posible que no queramos que eso suceda, debemos usar la anotación @Entity en Vehicle con esta estrategia.

Si desea deshabilitar esta función, una opción simple es hacer que la clase Vehicle sea abstracta, evitando que alguien pueda instanciarla. Si no es instanciable, no se puede guardar como una entidad, aunque esté anotado como tal.

Estrategia de una mesa por clase

La siguiente estrategia se llama Una tabla por clase, que, como su nombre lo indica, crea una tabla por clase en la jerarquía.

Sin embargo, podríamos haber usado el término "Clase concreta" en su lugar, ya que no crea tablas para clases abstractas.

Este enfoque se parece mucho al enfoque de la superclase mapeada; la única diferencia es que la superclase también es una entidad.

Para que JPA sepa que nos gustaría aplicar esta estrategia, estableceremos InheritanceType en TABLE_PER_CLASS en nuestra anotación @Inheritance:

1
2
3
4
5
6
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Vehicle {
    @Id
    private String licensePlate;
}

Nuestras clases Car y Motorcycle solo tienen que mapearse usando @Entity y listo. Las definiciones de la tabla son las mismas que con la superclase mapeada, más una tabla VEHÍCULO (porque es una clase concreta).

Pero, lo que difiere de superlcass mapeado es que podemos buscar una entidad Vehicle, así como una entidad Car o Motorcycle:

1
2
3
4
5
6
7
8
Vehicle foundVehicle = entityManager.find(Vehicle.class, "1 - ABC - 123");
Car foundCar = entityManager.find(Car.class, "1 - ABC - 123");

assertThat(foundVehicle).isNotNull();
assertThat(foundVehicle.licensePlate()).isEqualTo("1 - ABC - 123");
assertThat(foundCar).isNotNull();
assertThat(foundCar.licensePlate()).isEqualTo("1 - ABC - 123");
assertThat(foundCar.runOnLpg()).isTrue();

Estrategia de mesa unida

Por último, está la estrategia Joined Table. Crea una tabla por entidad y mantiene cada columna donde pertenece naturalmente.

Tomemos nuestra jerarquía Persona/Estudiante/Profesor. Si lo implementamos usando la estrategia de tablas unidas, terminaremos con tres tablas:

1
2
3
create table Person (id bigint not null, city varchar(255), number varchar(255), street varchar(255), birthDate date, FIRST_NAME varchar(255), gender varchar(255), lastName varchar(255), primary key (id))
create table STUD (wantsNewsletter boolean not null, id bigint not null, primary key (id))
create table Teacher (id bigint not null, primary key (id))

El primero, PERSON, obtiene las columnas de todos los campos de la entidad Person, mientras que los demás solo obtienen columnas para sus propios campos, más el id que vincula las tablas.

Al buscar un estudiante, JPA emitirá una consulta SQL con una combinación entre las tablas STUD y PERSON para recuperar todos los datos del estudiante.

Para mapear esta jerarquía, usaremos la estrategia InheritanceType.JOINED, en la anotación @Inheritance:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    private String lastName;

    @Column(name = "FIRST_NAME")
    private String firstName;

    private LocalDate birthDate;

    @Enumerated(EnumType.STRING)
    private Student.Gender gender;

    @Embedded
    private Address address;
}

Nuestras otras entidades simplemente se mapean usando @Entity:

1
2
3
4
5
6
7
@Entity
public class Student extends Person {
    @Id
    private Long id;
    private boolean wantsNewsletter;
    private Gender gender;
}

Y:

1
2
3
4
@Entity
public class Teacher extends Person {
    @Id
    private Long id;

Definamos también el ENUM que hemos usado en la clase Student:

1
2
3
enum GENDER {
MALE, FEMALE
}

Ahí vamos, podemos obtener las entidades ‘Persona’, ‘Estudiante’ y ‘Profesor’, así como guardarlas usando ‘EntityManager.persist()’.

De nuevo, si queremos evitar la creación de entidades Persona debemos hacerla abstracta.

Conclusión

En este artículo, nos sumergimos en el mapeo de herencia usando JPA e Hibernate y hemos abordado un par de situaciones diferentes que podría encontrar.

El código de esta serie se puede encontrar en GitHub.