Curso Jakarta EE 9 (22). JPA con Hibernate (5): Identificadores

logo Jakarta EE

Las entidades deben tener un atributo identificador. Pese a que existen bases de datos relacionales que permiten la definición de tablas sin claves primarias, nosotros las vamos a necesitar, o bien una columna que cumpla con los mismos requisitos: única y no nula.

La elección y configuración de identificadores no es complicada y está condicionada por la base de datos que usemos. En este capítulo exploraremos todas las opciones.

>>>> ÍNDICE <<<<

Elección de identificador

El identificador de una entidad es un atributo, o su getter, anotado con @Id. Si Hibernate crea la tabla, la columna se declarará clave primaria.

@Entity
@Table(name = "expenses")
public class Expense {

    @Id
    private Long id;

Como cualquier atributo persistible, no puede ser final y admite la anotación @Column. No es necesario definir la columna como única y no nula, pues estas restricciones ya están implícitas. Se admiten los siguientes tipos.

  • Los tipos primitivos byte, int, short, long, float, double y char
  • Los wrappers en clases de los anteriores: Byte, Integer, Short, Long, FLoat, Double y Character.
  • BigInteger y BigDecimal.
  • Temporales de tipo Date y java.util.Date.
  • Cadenas.

Es posible que en una entidad tengamos varios atributos que puedan utilizarse como identificador, ya sea de forma conjunta o independiente (en el mundo relacional hablamos de claves candidatas). En el supuesto de una persona, podría ser su dirección de correo electrónico o el número de la seguridad social.

Sin embargo, y pese a sonar extraño, la mejor forma de seleccionar un identificador es no hacerlo, sino crear un atributo específico para este menester consistente en un número sin ningún significado en particular. Es lo que se ha hecho en Expense con Long id. Estas son las ventajas.

  • Cualquier registro de cualquier tabla se identifica con un número único para esa tabla, o incluso toda la base de datos. Nos da igual el diseño de las entidades porque trabajaremos con ellas del mismo modo: identificándolas por un número.
  • A veces, un valor considerado como único no lo es o deja de serlo.
  • El identificador lo puede generar de forma automática, eficiente y segura la base de datos. De hecho, es la costumbre habitual.
  • Aunque se modifiquen los datos de un registro, siempre se identificará por el mismo número. Si en la tabla usuarios elegimos, por ejemplo, como clave primaria el atributo email, este valor es susceptible de cambiar. Pero si el usuario tiene el id 27, siempre será el usuario 27. Las bases de datos suelen permitir la modificación de las claves primarias, pero no es recomendable.
  • Un número es más práctico que una cadena, pues no contiene caracteres problemáticos, y, sobre todo, su columna consume poco espacio.
  • Evitamos la molestia, y hablo por experiencia propia, de tener claves primarias múltiples formadas por más de una columna. Téngase en cuenta que las claves primarias son referenciadas por las ajenas, lo que nos complicará además la declaración de estas últimas.

A este tipo de claves se les denomina subrogadas o, con menos frecuencia, artificiales. Por el contrario, las que sí lo tienen, son conocidas como claves naturales o de negocio. Si no estamos trabajando con una base preexistente a la que nos tenemos que adaptar, recomiendo optar siempre por las subrogadas debido a los beneficios que he desgranado.

Generación automática

Acabo de comentar que la mejor técnica para generar un identificador consiste en delegar esta acción en la base de datos, en función de sus características, en lugar de implementarla nosotros mismos. Esto se hace con la anotación @GeneratedValue, eligiendo una de las cuatro estrategias disponibles.

generatedvalue uml
Identidad

Esta generación confía en el tipo de dato identidad de la base de datos. Cuando se inserte un registro, recibirá un identificador único para la tabla. No hay nada más que hacer o configurar; sencillo y práctico.

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

En versiones antiguas de Oracle no contamos con el tipo identidad.

MySQL\MariaDBAUTO_INCREMENT
PostgreSQLserial (es un tipo identidad simulado con una secuencia)
Oracle pre 12cNo disponible
Oracle 12cIDENTITY
SQL ServerIDENTITY

El inconveniente de esta estrategia es que no se conoce el identificador de un nuevo registro hasta que se ejecute la sentencia INSERT. Esto supone un problema de eficiencia porque Hibernate tiene que insertar inmediatamente el registro para poder asignar el identificador a la entidad, lo que impide la realización de ciertas optimizaciones y las inserciones masivas en lotes (batch). Lo comprobaremos en el próximo capítulo.

Secuencia

Una secuencia es un objeto o tipo de datos (la nomenclatura depende de la base de datos) que proporciona valores numéricos únicos. Almacena un valor que aumenta según una cantidad de incremento cuando se solicita un nuevo valor. Esto último se realiza con una consulta SELECT.

select nextval('hibernate_sequence')

Con el siguiente código, Hibernate usará una única secuencia por esquema denominada hibernate_sequence. Los valores generados serán únicos para todas las entidades en las que establezcamos su identificador de la misma manera.

@Id
@GeneratedValue(strategy=GenerationType.SEQUENCE)
private Long id;

Las secuencias se declaran con @SequenceGenerator. Si no existen, Hibernate las creará si está configurado para actualizar el esquema. Contamos con los parámetros opcionales initialValue, que indica el primer número de la secuencia y de valor predeterminado 1, y allocationSize, con la cantidad a incrementar la secuencia cuando se devuelva un nuevo valor y cuyo valor predeterminado es 50.

Puede parecer raro que una secuencia no retorne números consecutivos (1, 2, 3, 4…) y vaya devolviendo 100, 150, 200, …, pero es una triquiñuela que permite a Hibernate generar internamente 49 identificadores y ahorrarse las llamadas a select nextval. Por tanto, estamos ante una medida de optimización de cara a la inserción masiva de registros. Eso sí, cuando echemos un vistazo a la tabla, veremos algunos huecos en las claves primarias; no debería importarnos, pues se tratan de claves subrogadas.

@Entity
public class EntityA {
	@Id
	@SequenceGenerator(name="entA", sequenceName="entityA_seq", allocationSize = 5, initialValue = 1)
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="entA")
	private Long id;

En el fragmento de código anterior se ha definido un generador llamado entA vinculado a la secuencia entityA_seq (lo normal es tener una secuencia para cada tabla). Se usa en los identificadores con la propiedad generator de @GeneratedValue. Si insertáramos diez instancias de EntityA, las trazas de Hibernate demostrarán cómo el marco de trabajo utiliza el valor de allocationSize para minimizar las llamadas a base de datos; los diez identificadores únicos se generan con solo dos SELECT.

select nextval ('entityA_seq')
	Sequence value obtained: 16
	Generated identifier: 12
	Generated identifier: 13
	Generated identifier: 14
	Generated identifier: 15
	Generated identifier: 16
select nextval ('entityA_seq')
	Sequence value obtained: 21
	Generated identifier: 17
	Generated identifier: 18
	Generated identifier: 19
	Generated identifier: 20
	Generated identifier: 21

En MySQL no hay secuencias.

MySQLNo disponible.
MariaDB pre 10.3No disponible
MariaDB 10.3
PostgreSQL
Oracle
SQL Server pre 2012No disponible
SQL Server 20102

Los identificadores basados en secuencias son los más recomendables porque Hibernate conoce el valor para un nuevo objeto\registro antes de su inserción, tan solo tiene que pedir a la base de datos el siguiente valor de la secuencia. Esto le permite retrasar el momento de ejecución de los INSERT para mejorar el rendimiento y también realizar inserciones por lotes.

Tabla

Esta estrategia es portable a cualquier base de datos porque emplea SQL estándar. Utiliza una tabla con dos columnas, una de tipo texto con el nombre que identifica al “generador” de claves primarias, y otra de tipo numérico con el último valor que fue proporcionado por ese generador. Cuando necesite una nueva clave primaria del generador, Hibernate la obtiene sumando uno a ese valor. En la práctica, es lo que parece: se están simulando secuencias.

@Entity
@Table(name="cities")
public class City {
    @Id
    @GeneratedValue(strategy = GenerationType.TABLE)
    private Long id;

En City se ha indicado que queremos el usar el modo tabla sin proporcionar ninguna configuración adicional, como por ejemplo el nombre del generador. Hibernate espera la existencia de la siguiente tabla con un único registro para calcular las claves primarias de aquellas entidades que usen el generador por omisión, denominado default.

Análogamente a lo que vimos para las secuencias, se pueden definir tantos generadores como queramos y aplicarlos a varias entidades. Por ejemplo, vamos a crear uno específico para cada una de las siguientes entidades.

@Entity
@Table(name="cities")
public class City {
    @Id
    @TableGenerator(name="city_generator")
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "city_generator")
    private Long id;

@Entity
@Table(name="users")
public class User {
    @Id
    @TableGenerator(name="user_generator")
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "user_generator")
    private Long id;

Tendremos estas tablas.

Otras propiedades interesantes de @TableGenerator.

  • table. El nombre de la tabla que contiene el generador. Permite usar siempre la misma tabla y que cada generador sea un registro de la misma. La idea es centralizar todos los generadores y evitar tener muchas tablas de este tipo (user_generator, city_generator…) .
  @Id
  @TableGenerator(name="user_generator", table = "table_generator")
  @GeneratedValue(strategy = GenerationType.TABLE, generator = "user_generator")
  private Long id;
  • initialValue. El número en el que comienzan los identificadores. Por omisión es cero para que el primer valor sea uno.
  • allocationSize. El incremento de la “secuencia” después de asignarse un nuevo valor. Funciona igual que el allocationSize de @SequenceGenerator, así que no repetiré mis observaciones.
  • pkColumnName. Nombre de la columna que almacena el nombre del generador. Por omisión, Hibernate usa sequence_name.
  • pkColumnValue. El nombre del generador dentro de la columna pkColumnName. Hibernate usa el nombre de la tabla de la entidad.
  • valueColumnName. El nombre de la columna con la última clave asignada para el generador. Hibernate usa next_val.

La flexibilidad que aporta esta estrategia tiene un precio alto, hasta tal punto de no ser recomendable: resulta ineficiente porque hay que consultar y actualizar una tabla para obtener la clave, además de gestionar los posibles problemas de concurrencia de estas operaciones. Esto da lugar a un pequeño “cuello de botella” a la hora de insertar los registros. Si buscamos la máxima compatibilidad de nuestros identificadores con diversas bases de datos, es aconsejable recurrir a la estrategia que estudiaremos a continuación, aunque tampoco está exenta de inconvenientes.

Auto

Es la estrategia predeterminada y, al igual que la anterior, portable. Deja en manos de la implementación de JPA la selección de una estrategia de generación dependiendo de la base de datos (el dialecto configurado en Hibernate).

@Entity
public class IdAuto {
    @Id
    @GeneratedValue
    private Long id;

Para usarla es imprescindible conocer cómo la ha implementado nuestro proveedor de JPA. En Hibernate este es el comportamiento.

MySQL\MariaDB pre 10.3AUTO_INCREMENT con Hibernate 4, TABLE a partir de Hibernate 5.
MariaDB 10.3 una única secuencia (hibernate_sequence)
PostgreSQLuna única secuencia (hibernate_sequence)
Oracleuna única secuencia (hibernate_sequence)
SQL ServerIDENTITY

Obsérvese que es una mala idea usar este generador si nuestro software se utiliza con MySQL o en versiones antiguas de MariaDB porque Hibernate aplica el tipo TABLE que, tal y como hemos visto, debemos evitar. Tampoco es una elección afortunada en el caso de SQL Server: si bien IDENTITY es preferible a TABLE, en SQLServer 2012 tenemos disponible SEQUENCE que es mejor todavía.

Claves múltiples

A todas luces, resulta más cómodo trabajar con claves primarias simples que con aquellas conformadas por más de un atributo\columna. No obstante, ya mencioné que en ocasiones no nos quedará más remedio que configurar claves múltiples para adaptarnos a bases de datos ya existentes. Por fortuna, JPA contempla este escenario.

La manera de hacerlo nos sonará del capítulo anterior: la clave se modela con una clase incrustable que además debe ser Serializable e implementar los métodos equals y hashCode. Recordemos que en una clase incrustable es posible añadir atributos pertenecientes a otras clases incrustables y relaciones con entidades, de tal modo que la clave primaria podría incluir columnas que también sean claves ajenas o foráneas.

He aquí una clave primaria múltiple para la entidad “ciudad” con dos atributos: su nombre y la región administrativa a la que pertenece.

@Embeddable
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class CityPK implements Serializable {

    private String name;

    @Column(name = "region_code")
    private String regionCode;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        CityPK cityPK = (CityPK) o;
        return name.equals(cityPK.name) && regionCode.equals(cityPK.regionCode);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, regionCode);
    }
}

Ahora, la usamos con la anotación @EmbeddedId.

package com.danielme.jakartaee.jpa.entities;

import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;

@Entity
@Table(name="cities")
public class City {

    @EmbeddedId
    private CityPK id;

Se puede utilizar @AttributeOverrides como con cualquier clase incrustada.

Podemos comprobar con un pequeño test que no hay diferencias entre usar una clave primaria simple y una compuesta. Siempre se utiliza la clase que la modela.

    @Test
    @DataSet(value = "/datasets/cities.yml")
    void testEmbeddableId() {
        City city = em.find(City.class, new CityPK("Seville", "SE"));

        assertNotNull(city);
    }

Existe otra forma menos habitual de definir claves múltiples. Consiste en declarar los atributos del identificador directamente en la entidad con los mismos nombres que tienen en una clase que modela todo el conjunto que compone la clave. Esta clase la indicamos con @IdClass

@Entity
@Table(name="cities")
@IdClass(CityPK.class)
public class City {

    @Id
    private String name;

    @Id
    @Column(name = "region_code")
    private String regionCode;

Con la configuración anterior, la prueba testEmbeddableId sigue siendo válida.

Claves naturales

Examinemos una funcionalidad propia de Hibernate poco conocida. Aunque usemos claves subrogadas, en algunas entidades tendremos claves naturales de gran importancia desde el punto de vista de la lógica de negocio. Vuelvo al ejemplo del número de seguridad social o equivalente según el país. Su atributo se declararía así.

@Entity
@Table(name = "users_naturalid")
@Getter
@Setter
public class UserNaturalId {
    @Id
    private Long id;
    @Column(unique = true, nullable = false, updatable = false)
    private Integer number;

En number tenemos la misma configuración que querríamos para una clave primaria: única, no nula y no actualizable. Podemos cambiar esta declaración indicando de forma explícita que se trata de una clave natural que usaremos como tal.

@NaturalId
@Column(nullable = false)
private Integer numer;

@NaturalId aporta semántica al código, pero también permite recuperar objetos de UserNaturalId según el valor de number como si se tratara del identificador de la entidad. Veámoslo con un ejemplo.

    @Test
    @DataSet(value = "/datasets/users_naturalid.yml")
    void testNaturalId() {
        UserNaturalId userId = em.find(UserNaturalId.class, 1L);
        UserNaturalId userNaturalId = em.unwrap(Session.class).bySimpleNaturalId(UserNaturalId.class).load(234);

        assertEquals(userId.getId(), userNaturalId.getId());
    }

En la prueba vemos cómo recuperar un usuario por sus dos claves. Se verifica que en ambos casos se devuelve la misma entidad (el usuario con id 1 y número 234). El método bySimpleNaturalId pertenece a la API de Hibernate, así que la obtenemos con el método unwrap.

Si vemos las consultas generadas, observamos algo curioso.

select
	usernatura_.id AS id1_0_
from
	users_naturalid usernatura_
where
	usernatura_.number =?;

select
	usernatura0_.id AS id1_0_0_,
	usernatura0_.number AS number2_0_0_
from
	users_naturalid usernatura0_
where
	usernatura0_.id =?

bySimpleNaturalId ejecuta dos sentencias SELECT. Con la primera, averigua el identificador de la entidad porque Hibernate necesita esta información. La segunda obtiene la entidad, siempre y cuando todavía no forme parte del contexto de persistencia. Podemos mejorar el rendimiento indicando que la relación entre la clave primaria y la natural se almacene en la caché de segundo de nivel de Hibernate, de nuevo un concepto que veremos más adelante.

@Entity
@Table(name = "users_naturalid")
@NaturalIdCache
@Getter
@Setter
public class UserNaturalId {

También se admiten claves naturales múltiples.

@NaturalId
private String name;
@NaturalId
private String country;

Se usan así.

em.unwrap(Session.class).byNaturalId(City.class).using("CADIZ", "SPAIN");

Código de ejemplo

El código de ejemplo del capítulo 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 <<<<

Responder

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 .