Curso Jakarta EE 9 (26). JPA con Hibernate (9): Relaciones de herencia

logo Jakarta EE

La herencia es una de las características definitorias de la programación orientada a objetos, aunque en la actualidad se le tenga cierta aversión debido a su empleo abusivo (me incluyo). Es una técnica fundamental en la definición de modelos de datos y en la aplicación de algunos patrones de diseño.

>>>> ÍNDICE <<<<

Dado que el modelo relacional no contempla el mecanismo de herencia, JPA define cuatro estrategias fáciles de utilizar para vincular jerarquías de entidades con tablas. Ninguna de ellas se puede considerar como solución «universal». La elección se basará en nuestras necesidades, valorando en todo momento los beneficios e inconvenientes de cada opción.

He aquí el modelo de ejemplo. Hay dos tipos de dispositivos: cámara y sensor de temperatura. No está de más recordar que las entidades pueden heredar de clases que no lo sean y viceversa, así como ser abstractas.

Sea cual sea la estrategia elegida, siempre se configura en la clase raíz de toda la jerarquía, que en nuestro ejemplo es Device.

Ignorar la jerarquía (mapped superclass)

Lo más fácil es no trasladar la jerarquía a la base de datos y declarar como entidades solo las «hojas» del árbol. En el ejemplo, tendríamos dos entidades, TempSensor y Camera, con sus respectivas tablas. En ambas encontraremos los atributos de la clase padre Device.

@Getter
@Setter
@Entity
@Table(name = "cameras")
public class Camera extends Device {

    private BigDecimal resolution;

    private Integer minIso;

    private Integer maxIso;

}
@Getter
@Setter
@Entity
@Table(name = "temp_sensors")
public class TempSensor extends Device {

    private BigDecimal minTemp;

    private BigDecimal maxTemp;
}

Se trata de dos entidades «normales». No tienen identificador porque es uno de los atributos que heredan de Device.

@Getter
@Setter
@MappedSuperclass
public class Device {

    @Id
    private Long id;

    private String manufacturer;

    private String name;

    private BigDecimal weight;
}

Device se ha marcado con @MappedSuperclass para que Hibernate aplique a las clases hijas las configuraciones de persistencia que contiene. En el ejemplo, se trata del identificador.

La base de datos queda como sigue.

El inconveniente de esta configuración es, precisamente, que Device no es una entidad. No podremos usarla en consultas para obtener todos los dispositivos de forma genérica, ni tampoco en relaciones; por ejemplo, una entidad llamada Almacen que contenga un listado con los dispositivos que alberga, independientemente de su tipo. Las jerarquías en las que la superclase deba ser una entidad con relaciones son muy habituales.

Una tabla para todo (single table)

La estrategia predeterminada consiste en guardar todas las entidades en una única tabla.

@Entity
@Table(name = "devices")
@Inheritance
public class Device {

Tendremos una tabla devices que almacena todos los registros.

La columna DTYPE permite saber la entidad a la que corresponde el registro. Si el nombre DTYPE no nos parece adecuado, es posible personalizarlo con la anotación DiscriminatorColumn . El nombre identificativo de cada entidad es personalizable gracias a la propiedad DiscriminatorValue.

@Table(name = "devices")
@Inheritance
@DiscriminatorColumn(name = "type")
public class Device {

Fácil y eficiente. Es la estrategia que ofrece el mejor rendimiento: las operaciones CRUD sobre las entidades son directas y no requieren join. Lo que no es tan magnífica es la definición de la tabla debido a que tendremos numerosos valores nulos. Para las cámaras, las columnas propias de los sensores de temperatura no tendrán valor y viceversa. Además, las columnas que no pertenezcan a la clase padre no pueden marcarse como no nulas porque las entidades que no las tuvieran no se podrían insertar, lo que comprometerá la integridad de los datos dentro del modelo relacional. Esto último se puede corregir implementando triggers en la propia base de datos.

Otro problema de esta tabla única es que tiende a ser muy grande tanto a lo ancho (muchas columnas) como a lo largo (muchos registros). Con el tiempo, puede llegar a ser enorme.

Una tabla por clase (table per class)

Esta es la traslación directa de las entidades a la base de datos ignorando que mantienen una relación de herencia. Cada entidad no abstracta tiene su propia tabla con todos sus atributos, incluyendo los heredados. No hay claves ajenas que relacionen estas tablas

@Entity
@Table(name = "devices")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Device {

Simple y eficiente…mientras obtengamos las entidades situadas en las hojas del árbol de la jerarquía. Ejecutemos este código tan inocente.

em.find(Device.class, 1L);

Un Device es un Device (obvio), pero las cámaras y sensores también lo son. Esto obliga a Hibernate a realizar una unión entre todas las tablas para averiguar y obtener el dispositivo deseado (puede ser una cámara o un sensor).

select
	device0_.id as id1_5_0_,
	device0_.manufacturer as manufact2_5_0_,
	device0_.name as name3_5_0_,
	device0_.weight as weight4_5_0_,
	device0_.maxIso as maxIso1_2_0_,
	device0_.minIso as minIso2_2_0_,
	device0_.resolution as resoluti3_2_0_,
	device0_.maxTemp as maxTemp1_8_0_,
	device0_.minTemp as minTemp2_8_0_,
	device0_.clazz_ as clazz_0_
from
	(
	select
		id,
		manufacturer,
		name,
		weight,
		null as maxIso,
		null as minIso,
		null as resolution,
		null as maxTemp,
		null as minTemp,
		0 as clazz_
	from
		devices
union
	select
		id,
		manufacturer,
		name,
		weight,
		maxIso,
		minIso,
		resolution,
		null as maxTemp,
		null as minTemp,
		1 as clazz_
	from
		cameras
union
	select
		id,
		manufacturer,
		name,
		weight,
		null as maxIso,
		null as minIso,
		null as resolution,
		maxTemp,
		minTemp,
		2 as clazz_
	from
		temp_sensors ) device0_
where
	device0_.id =?

Así pues, esta estrategia no es la más aconsejable si vamos a trabajar intensamente con las entidades de la parte superior de la jerarquía porque penaliza el rendimiento de su explotación.

Viendo la consulta anterior, cabe plantearse una cuestión interesante: si tuviéramos una cámara 1 y un sensor 1, ¿cuál de los dos es el dispositivo 1? Si se diera esta situación, find provocaría una excepción.

org.hibernate.HibernateException: More than one row with the given identifier was found: 1, for class: com.danielme.jakartaee.jpa.entities.inheritance.Device

En consecuencia, las claves primarias deben ser únicas para todas las tablas de la jerarquía, de lo contrario no podremos trabajar con las entidades que no sean «hojas». Este requerimiento nos obliga a descartar el uso de identificadores de tipo identidad porque son únicos solo para cada tabla y pueden repetirse entre varias de ellas. Hibernate asegura que esta configuración no se produzca porque cuando la detecte lanzará esta excepción.

org.hibernate.MappingException: Cannot use identity column key generation with <union-subclass> mapping

Si con TABLE_PER_CLASS queremos que los identificadores sean generados por la base de datos (muy recomendable), lo mejor es recurrir a una secuencia para obtener los identificadores de todos los registros de las tablas correspondientes a la jerarquía. La configuración resulta sencilla, pues, tal y como hemos visto, el identificador solo se declara en la entidad raíz. Eso sí, en MySQL no tenemos está opción, pues no cuenta con secuencias.

@Entity
@Table(name = "devices")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Device {

    @Id
    @SequenceGenerator(name="SEQ_DEVICE", sequenceName="seq_device", allocationSize = 1)
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SEQ_DEVICE")
    private Long id;

Tablas unidas (joined table)

La cuarta y última estrategia crea una tabla por entidad, incluso para las abstractas, con los atributos que define cada una. Cada tabla está relacionada con su tabla\entidad padre con una clave ajena, también usada como clave primaria, para poder recuperar los datos de una entidad uniendo las tablas. La estructura que tenemos en la base de datos es la más parecida a una jerarquía de clases que el modelo relacional permite. En la siguiente imagen, las vemos frente a frente.

La configuración, como siempre, se realiza definiendo el tipo de herencia en la clase raíz.

@Table(name = "devices")
@Inheritance(strategy = InheritanceType.JOINED)
public class Device {

Con joined se optimiza el espacio requerido en la base de datos: no hay columnas duplicadas y las tablas son más pequeñas, tanto a lo ancho como a lo alto, que las empleadas por las otras estrategias. El diseño final resulta natural desde el punto de vista del modelo de entidades JPA, pero también resulta «ortodoxo» a los ojos de un DBA. El precio a pagar es el (inevitable) aumento de las sentencias SQL ejecutadas por Hibernate. Por ejemplo, probemos a guardar una nueva cámara.

Camera camera = new Camera();
camera.setId(1L);
camera.setName("camera test");
camera.setResolution(new BigDecimal("50"));
camera.setMinIso(50);
camera.setMaxIso(3000);

em.persist(camera);
em.flush();

Esto implica la inserción de dos registros para una sola entidad.

insert into devices (manufacturer, name, weight, id) values (?, ?, ?, ?)
into cameras (maxIso, minIso, resolution, id) values (?, ?, ?, ?)

La obtención de la cámara requiere el join entre cameras y devices.

 select
	camera0_.id as id1_5_0_,
	camera0_1_.manufacturer as manufact2_5_0_,
	camera0_1_.name as name3_5_0_,
	camera0_1_.weight as weight4_5_0_,
	camera0_.maxIso as maxIso1_2_0_,
	camera0_.minIso as minIso2_2_0_,
	camera0_.resolution as resoluti3_2_0_
from
	cameras camera0_
inner join devices camera0_1_ on
	camera0_.id = camera0_1_.id
where
	camera0_.id =?

Por tanto, cuantas más clases conformen la jerarquía, más operaciones serán necesarias para explotarla. Si es grande y con muchos niveles, la pérdida de rendimiento puede ser apreciable y será recomendable analizar cuidadosamente si nos conviene seguir otra estrategia.

Código de ejemplo

El código de ejemplo de los capítulos dedicados a las relaciones se encuentra en GitHub (todos los proyectos son independientes pero están en un único repositorio). Para más información sobre cómo utilizar GitHub, consultar este artículo.

>>>> ÍNDICE <<<<

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.