JPA con Hibernate: identificadores y claves primarias. Generación automática. Claves múltiples.

Última actualización: 19/02/2023
logo java

En este artículo veremos cómo funcionan los identificadores (claves primarias) de las entidades de JPA en Hibernate con las bases de datos MySQL, MariaDB, PostgreSQL, Oracle y SQL Server. En concreto, usaremos JPA 3.1 e Hibernate 6.1, las últimas versiones estables en febrero de 2023. Comentaré algunos cambios con respecto a otras versiones para que el artículo te sea útil incluso si eres un arqueólogo que trabaja con reliquias tales como Hibernate 4 y JPA 2.

IMPORTANTE. Aunque el contenido del presente artículo es válido, encontrarás una versión alternativa en el curso Jakarta EE 9:

Siempre que se habla de JPA, debemos tener claro que no es más que la especificación de una API estándar. Por consiguiente, para utilizarla necesitamos un producto que la implemente (proveedor o vendor de JPA). El proveedor ofrece funcionalidades propias fuera del estándar; si recurrimos a ellas, cambiar de implementación no será sencillo, aunque en la práctica este tipo de situaciones no suele darse.

Índice

  1. Proyecto para pruebas
  2. Nota sobre versiones
  3. Declaración del identificador
  4. Generación automática de identificadores
    1. Tipo identidad
    2. Tipo secuencia
    3. Tipo tabla
    4. Selección automática del tipo
    5. Tipo UUID
  5. Claves múltiples con @EmbeddedId
  6. Código de ejemplo

Proyecto para pruebas

Consiste en un pequeño proyecto Maven para Java 11 cuya única funcionalidad es la de crear y persistir dos entidades muy sencillas en un método main.

diagrama de clases

Como puedes ver, los nombres de las clases de tipo entidad son todo un ejercicio de originalidad.

El pom.xml incluye estas dependencias:

  • Hibernate, en concreto 6.1.7.Final (febrero de 2023). Tiene como dependencia JPA 3.0, por lo que agregaremos JPA 3.1.
  • c3p0 – pool de conexiones integrado con Hibernate.
  • log4j2 – Sistema de bitácora o logging de Apache.
  • El controlador JDBC de la base de datos. En la actualidad, tanto los de las bases de datos de código abierto como los de Oracle (ojdbc) y SQL Server se publican en los repositorios centrales de Maven.
<?xml version="1.0" encoding="UTF-8" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0    	http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.danielme.demo</groupId>
	<artifactId>MavenJpaHibernateLog4jDemo</artifactId>
	<version>1.0</version>
	<packaging>jar</packaging>	

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.hibernate.version>6.1.7.Final</project.hibernate.version>
        <project.build.mainClass>com.danielme.demo.Main</project.build.mainClass>	
		<maven.compiler.release>11</maven.compiler.release>
        <maven.compiler.plugin>3.8.0</maven.compiler.plugin>
        <maven.jar.plugin>3.2.0</maven.jar.plugin>
        <log4j2.version>2.19.0</log4j2.version>
        <jpa.version>3.1.0</jpa.version>
	</properties>
	
	
	<build>
	
		<pluginManagement>
		
			<plugins>
				
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-compiler-plugin</artifactId>
					<version>${maven.compiler.plugin}</version>					
				</plugin>
        
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-jar-plugin</artifactId>
                     <version>${maven.jar.plugin}</version>
                     <configuration>
                        <archive>
                            <manifest>
                                <addClasspath>true</addClasspath>
                                <mainClass>${project.build.mainClass}</mainClass>
                            </manifest>
                            </archive>
                      </configuration>
                  </plugin>
				
			</plugins>
			
		</pluginManagement>
		
	</build>

	<dependencies>
	    
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-core</artifactId>
			<version>${project.hibernate.version}</version>
		</dependency>

       <dependency>
			<groupId>jakarta.persistence</groupId>
			<artifactId>jakarta.persistence-api</artifactId>
			<version>${jpa.version}</version>
		</dependency>
		
		<!-- connection pooling with c3p0 -->
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-c3p0</artifactId>
			<version>${project.hibernate.version}</version>
		</dependency>
		
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>${log4j2.version}</version>
        </dependency>
        
        <dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-core</artifactId>
          <version>${log4j2.version}</version>
        </dependency>

		
		<!-- =================================== -->
		<!-- =========== JDBC DRIVERS ========== -->
		<!-- =================================== -->

		<!-- Mysql -->
		 <dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.32</version>
		</dependency>

     <!-- MariaDB -->
		<!--<dependency>
			<groupId>org.mariadb.jdbc</groupId>
			<artifactId>mariadb-java-client</artifactId>
			<version>3.1.2</version>
		</dependency>-->
		
		<!-- <dependency>
         <groupId>org.postgresql</groupId>
         <artifactId>postgresql</artifactId>
         <version>42.5.4</version>
        </dependency>-->		
		
		<!-- <dependency>
           <groupId>com.microsoft.sqlserver</groupId>
           <artifactId>mssql-jdbc</artifactId>
           <version>12.2.0.jre11</version>
        </dependency> -->	
		
		 <!-- <dependency>
               <groupId>com.oracle.database.jdbc</groupId>
               <artifactId>ojdbc8</artifactId>
               <version>21.9.0.0</version>
               </dependency> -->           

	</dependencies>

</project>

La configuración de JPA e Hibernate se encuentra en el fichero /META-INF/persistence.xml. Se incluye también la configuración del pool de conexiones con C3P0.

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
             http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
             version="2.2">

	<persistence-unit name="persistence-unit_demo" transaction-type="RESOURCE_LOCAL">		

		<properties>
		
		<!-- =============================== -->
		<!-- ===========DATABASE============ -->
		<!-- =============================== -->

			<!-- backwards compatibility with Hibernate 5-->
			<!--<property name="hibernate.id.db_structure_naming_strategy" value="legacy" />-->

		    <!-- MySQL -->
		    <property name="hibernate.connection.url" value="jdbc:mysql://localhost:3306/danielme" />
			<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
			
			<!-- MariaDB -->
			<!--
			<property name="hibernate.connection.url" value="jdbc:mariadb://localhost:3310/danielme" />
			-->
			
			<!-- PostgreSQL -->
			<!-- <property name="hibernate.connection.url" value="jdbc:postgresql://192.168.2.102:5432/danielme" />
			<property name="hibernate.connection.driver_class" value="org.postgresql.Driver" />
			-->
			
			<!-- SQL SERVER 2012-->
			<!-- <property name="hibernate.connection.url" value="jdbc:sqlserver://192.168.2.102:1433;databaseName=danielme" />-->
			<!-- <property name="hibernate.connection.driver_class" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />
				 -->
			
			
			<!-- ORACLE -->
			<!-- <property name="hibernate.connection.url" value="jdbc:oracle:thin:@192.168.2.102:1521:XE" />
			<property name="hibernate.connection.driver_class" value="oracle.jdbc.driver.OracleDriver" />
			-->
			
		<!-- =============================== -->
		<!-- =============LOGIN============= -->
		<!-- =============================== -->
			<property name="hibernate.connection.username" value="danielme" />
			<property name="hibernate.connection.password" value="danielmepass" />	
		
		<!-- =============================== -->
		<!-- ===========OTHER PROPS========== -->
		<!-- =============================== -->				
			<!-- automatically updates the schema, NOT RECOMENDED IN A PRODUCTION ENVIRONMENT. Check user's grant permissions -->
			<property name="hibernate.hbm2ddl.auto" value="update"/>		
			<property name="hibernate.format_sql" value="true"/>
			<!-- Enables autocommit for JDBC pooled connections (it is not recommended) -->
			<property name="hibernate.connection.autocommit" value="false" />			
			
		<!-- =============================== -->
		<!-- ==============POOL============= -->
		<!-- =============================== -->
			<!-- Connection pool with Hibernate-C3P0 integration-->
			<property name="hibernate.connection.provider_class" value="org.hibernate.connection.C3P0ConnectionProvider" />
			<property name="hibernate.c3p0.acquire_increment" value="5"/>
			<property name="hibernate.c3p0.idle_test_period" value="100"/>
			<property name="hibernate.c3p0.max_size" value="50"/>
			<property name="hibernate.c3p0.max_statements" value="0" />
			<property name="hibernate.c3p0.min_size" value ="5" />
			<property name="hibernate.c3p0.timeout" value="100" /><!-- for idle connections, in seconds -->
		
		</properties>

	</persistence-unit>

</persistence>

Nota. En Hibernate 6 no suele ser necesario indicar ni el dialecto de la base de datos ni la clase de su controlador JDBC.

Se espera la existencia de una base de datos llamada danielme con las credenciales de acceso danielme\danielmepass. Asimismo, Hibernate al arrancar crea el esquema si no existe; si existe, intenta actualizarlo cuando sea imprescindible para que resulte coherente con la configuración del modelo de clases de las entidades (propiedad hibernate.hbm2ddl.auto con valor update). Más información aquí.

Estas son las clases de tipo entidad:

package com.danielme.demo.model;
 
import java.util.List;
 
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
 
 
@Entity
public class EntityA
{
    @Id
    private Long id;
     
    @Column(nullable=false, length=50)
    private String name;
     
    @OneToMany(cascade=CascadeType.ALL, mappedBy = "entityA")
    private List<EntityB> entities;
 
       //getters y setters
package com.danielme.demo.model;
 
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
 
@Entity
public class EntityB
{
 
    @Id
    private Long id;
     
    @Column(nullable=false, length=50)
    private String name;
     
    @ManyToOne(optional=false)
    private EntityA entityA;
 
    //getters y setters


El sistema de log se configura en el fichero /src/main/resources/log4j2.xml. Mostrará en la salida estándar todos los mensajes a partir del nivel DEBUG. Así, podremos saber qué está pasando entre bambalinas…

<?xml version="1.0" encoding="UTF-8"?>
 
<Configuration status="INFO">
   
  <Appenders>
    <Console name="ConsoleAppender" target="SYSTEM_OUT">
      <PatternLayout
        pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
    </Console>   
  </Appenders>
   
  <Loggers>     
    <Root level="debug">
      <AppenderRef ref="ConsoleAppender" />
    </Root>
  </Loggers>
   
</Configuration>

He optado por una clase con un método main en lugar de escribir pruebas con JUnit con el objetivo de mantener el ejemplo lo más simple posible. Creará un objeto de EntityA, lo relacionará con dos nuevas instancias de EntityB y lo introducirá en el contexto de persistencia, lo que a la postre implica el guardado en las tablas. Con anterioridad habremos obtenido un EntityManager mediante la factoría correspondiente que nos permita trabajar con el contexto de persistencia de JPA.

package com.danielme.demo;

import com.danielme.demo.model.EntityA;
import com.danielme.demo.model.EntityB;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.List;

public class Main {

    private static final Logger logger = LogManager.getLogger(Main.class);

    public static void main(String[] args) {
        logger.info("demo begins...");

        EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("persistence-unit_demo");
        EntityManager entityManager = entityManagerFactory.createEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();

        transaction.begin();

        EntityA entityA = new EntityA();
        entityA.setName("parent");

        EntityB entityB1 = new EntityB();
        entityB1.setName("child 1");
        entityB1.setEntityA(entityA);

        EntityB entityB2 = new EntityB();
        entityB2.setName("child 2");
        entityB2.setEntityA(entityA);

        entityA.setEntities(List.of(entityB1, entityB2));

        try {
            //children will be persisted on cascade
            entityManager.persist(entityA);
            transaction.commit();

            logger.info("===================");
            logger.info("{} {} :  ", entityA.getId(), entityA.getName());
            for (EntityB entityB : entityA.getEntities()) {
                logger.info("  {} {}",entityB.getId(), entityB.getName());
            }
        } catch (Exception ex) {
            logger.error(ex.getMessage(), ex);
            transaction.rollback();
        } finally {
            entityManager.close();
        }

    }

}

Nota sobre versiones

A partir de JPA 3.0, los paquetes que empezaban por javax lo hacen con jakarta, lo que requiere emplear una versión de Hibernate compatible con estos nombres (la serie 6). Aquí explico la razón de este nuevo nombre.

Declaración del identificador

Toda clase de tipo entidad debe poseer un atributo anotado con @Id (también se puede anotar el getter de ese atributo) que representa a la clave primaria de la tabla modelada por la clase. Así pues, la función del identificador es identificar de forma unívoca a las entidades.

Ese atributo puede ser de los siguientes tipos:

  • Primitivos: byte, int, long, short, float, double, char
  • Wrappers de los primitivos anteriores (Long, Integer…)
  • String
  • Fecha: java.util.Date, java.sql.Date
  • Numéricos enormes: java.math.BigDecimal, java.math.BigInteger
  • java.util.uuid. Desde JPA 3.1. Lo veremos en una sección específica.

Naturalmente, el tipo elegido debe ser compatible con el tipo de la columna en la tabla.

La elección del identificador se aborda en el tutorial que mencioné al principio del artículo. En él explico por qué es preferible el uso de claves subrogadas (valores numéricos sin ningún significado en particular).

Para almacenar esos números, elige el tipo de atributo de menor tamaño que necesites. En general, Long es una buena opción, ya que su tamaño de 8 bytes (64 bits) te permitirá tener hasta 263-1 números, cantidad equivalente en granos de trigo a mil años de la producción mundial. ¡Eso es mucho pan! El Long de Java se suele corresponden con el tipo BIGINT que poseen la mayoría de bases de datos.

La anotación @Id no admite atributos. Puede acompañarse con @Column para configurar la columna asociada en la tabla. Por omisión, se considera que el nombre del atributo y de su columna son el mismo.

@Id
@Column(name="_ID")
private Long id;

Una práctica que he observado en numerosos proyectos consiste en crear una clase padre para todas las clases de tipo entidad que contenga el identificador. Sería algo así:

import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;

@MappedSuperclass
public class BaseEntity {
    
    @Id
    private Long id;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}
@Entity
public class EntityA extends BaseEntity
{

BaseEntity no es una clase de tipo entidad. @MappedSuperclass informa que la configuración de JPA declarada en la clase debe heredarse por las clases hijas. En una jerarquía de clases de entidad el identificador solo se define una vez.

No obstante, si lo que quieres es que todas las entidades puedan ser tratadas como clases con un identificador, mejor considera la creación de una interfaz. Así, te librarás de la rigidez de la herencia.

public interface Identifier {

    Long getId();
    void setId(Long id);
}
@Entity
public class EntityA implements Identifier {

    @Id
    private Long id;

    @Override
    public Long getId() {
        return id;
    }
    
    @Override
    public void setId(Long id) {
        this.id = id;
    }

Sea como fuere, si ejecutamos el proyecto de ejemplo en el estado actual, obtendremos este error:

 org.hibernate.id.IdentifierGenerationException: ids for this class must be manually assigned before calling save(): com.danielme.demo.model.EntityA

Informa que los identificadores de los objetos de EntityA deben asignarse manualmente. Por tanto, con la configuración actual es responsabilidad nuestra generar un valor adecuado para el identificador\clave primaria.

Por fortuna, cuando optamos por claves subrogadas la generación de los identificadores puede delegarse en JPA. Esto se verá condicionado por el proveedor que usemos (Hibernate) y la base de datos subyacente. Lo estudiaremos en la próxima sección.

Generación automática de identificadores

Esta funcionalidad se consigue complementando @Id con la anotación @GeneratedValue. Su cometido es configurar la generación automática de un identificador\clave primaria cuando se incorpore una nueva entidad a un contexto de persistencia con los métodos merge o persist del gestor de entidades. A continuación explico las cinco estrategias disponibles para conseguir ese identificador.

Nota. Si quieres que Hibernate modifique la estructura de la base de datos para crear las secuencias, configuraciones, tablas, etcétera, que voy a comentar asegúrate de configurar la propiedad hibernate.hbm2ddl.auto que comenté unos párrafos atrás.

Tipo identidad

Se basa en un tipo de columna especial consistente en un número cuyo valor se asigna automáticamente en el momento de insertarse el registro.

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

La inmensa mayoría de bases de datos ofrecen este tipo de columna. En MySQL/MariaDB se denomina AUTO_INCREMENT y en SQLServer IDENTITY. En ambos casos, más que un tipo de dato como tal, es un atributo que acompaña a la declaración de una columna numérica.

id BIGINT AUTO_INCREMENT  PRIMARY KEY

id BIGINT IDENTITY PRIMARY KEY

Por su parte, PostgreSQL cuenta con el tipo SERIAL. Simula la funcionalidad de IDENTITY con secuencias.

id SERIAL PRIMARY KEY

Oracle incorporó el tipo IDENTITY en la versión 12c. Como en los casos de MySQL y SQL Server, de nuevo se trata de una propiedad de las columnas numéricas.

id BIGINT GENERATED ALWAYS AS IDENTITY NOT NULL,
CONSTRAINT entity_pk PRIMARY KEY (id)

Podemos utilizarla con el dialecto org.hibernate.dialect.Oracle12cDialect disponible desde Hibernate 5. En dialectos de Oracle más antiguos obtendremos el siguiente error:

org.hibernate.MappingException: org.hibernate.dialect.Oracle10gDialect does not support identity key generation

Permíteme que sea pesado: en Hibernate 6 ya no se requiere declarar el dialecto; la detección es automática.

Esta estrategia es sencilla y práctica, pero no es oro todo lo que reluce. El identificador de una nueva entidad no se conocerá hasta que se ejecute la sentencia INSERT en la base de datos. Esto ocasiona que al llamarse al método persist o merge del EntityManager Hibernate inserte de inmediato en la tabla el registro correspondiente a la entidad para averiguar el identificador asignado. Y es una mala noticia porque impide que Hibernate efectúe ciertas optimizaciones mediante el retraso de la inserción, así como la realización de inserciones masivas en lotes (batch).

Tipo secuencia

Una secuencia es un objeto o tipo de datos (la nomenclatura depende de la BD de la que hablemos) que genera valores numéricos únicos dentro de esa secuencia. El valor de la secuencia va aumentando según una cantidad de incremento a medida que se vayan solicitando nuevos valores con una sentencia SQL parecida a la que sigue:

select nextval('hibernate_sequence')

Por tanto, las secuencias resultan perfectas para producir identificadores, de ahí que JPA contemple su empleo.

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

Si definimos la estrategia de generación secuencia sin más configuración, Hibernate 4\5 espera -o crea- una única secuencia por esquema denominada «hibernate_sequence». En Hibernate 6 este comportamiento cambia: se crea \ espera una secuencia para cada clase de tipo entidad que use el generador secuencia. Estará nombrada con el nombre de la clase más el sufijo _SEQ. En nuestro ejemplo, tendríamos EntityA_SEQ y EntityB_SEQ.

Se puede volver al funcionamiento anterior activando el modo legacy, pensado para facilitar la actualización desde versiones previas:

<property name="hibernate.id.db_structure_naming_strategy" value="legacy" />

De las bases de datos contempladas por este artículo, las únicas que carecen de secuencias son MySQL y las versiones de MariaDB anteriores a las 10.3 (mayo 2018). ¿Qué sucede si empleamos con ellas el generador secuencia? Pues depende de la versión de Hibernate.

Hibernate 4 lanza una excepción:

org.hibernate.MappingException: org.hibernate.dialect.MySQL5Dialect does not support sequences

Hibernate 5 prefiere simular todas las secuencias con una tabla llamada «hibernate_sequence». Contiene una única columna (next_val) que almacena el último valor entregado para la secuencia emulada. Este «apaño» es una mala idea; en breve explicaré el motivo cuando hable del generador tabla.

Hibernate 6 hace lo mismo, solo que crea una tabla \ secuencia para cada clase de tipo entidad siguiendo el criterio que ya indiqué: el nombre de la clase más la cadena «_SEQ».

SQLServer 2012 sí cuenta con secuencias. Es necesario utilizar el dialecto org.hibernate.dialect.SQLServer2012Dialect disponible a partir de Hibernate 4.2. De lo contrario, verás este error:

org.hibernate.MappingException: org.hibernate.dialect.SQLServer2008Dialect does not support sequences

Es posible configurar secuencias con @SequenceGenerator. Si no existen, Hibernate las creará por nosotros. La anotación posee cinco propiedades:

PropiedadDescripciónValor predeterminado
(Hibernate)
nameEl nombre único del generador (no confundir con el de la secuencia).Ninguno; este campo es obligatorio.
sequenceNameEl nombre de la secuencia de la base de datos que producirá los valores para el identificador.El nombre (name) del generador.
catalogEl catálogo de la base de datos con la secuencia. Este concepto no existe en todas las bases de datos.Ninguno.
schemaEl esquema de la base de datos con la secuencia. Este concepto no existe en todas las bases de datos.Ninguno.
initialValueEl primer valor que devolverá la secuencia justo en el momento en que empiece a dar los valores.1
allocationSizeEl incremento o salto numérico del avance de la secuencia cada vez que proporcione un nuevo valor.50

Reparemos en allocationSize. Resulta extraño que una secuencia no retorne números consecutivos (1, 2, 3, 4…) y vaya devolviendo 100, 150, 200, etcétera, en función del valor de allocationSize. Se trata de una técnica de optimización que permite a Hibernate generar internamente la cantidad de allocationSize en identificadores y así ahorrarse llamadas a select nextval. Eso sí, cuando echemos un vistazo a la tabla, veremos algunos huecos en las claves primarias. No debería importar, pues son claves subrogadas sin ningún significado particular. Y cuando importe, tan fácil como establecer allocationSize a 1.

Conviene advertir que el valor de allocationSize debe coincidir con el de la propiedad equivalente que tenga la secuencia en la base de datos. Cuando no sea así se lanzará una excepción, lo que evita males mayores:

org.hibernate.MappingException: The increment size of the [EntityA_SEQ] sequence is set to [50] in the entity mapping while the associated database sequence increment size is [20].

El anterior error explica que la secuencia EntityA_SEQ tiene un valor allocationSize de 50, mientras que esa secuencia en la base de datos tiene la cantidad de incremento 20.

A continuación, configuro nuestras clases de ejemplo de tal modo que cada una use su propia secuencia personalizada:

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

Fíjate que la propiedad generator de @GeneratedValue contiene el nombre (name) de un generador declarado con @SequenceGenerator. Será el generador que se utilizará para el identificador. Desde Hibernate 5.3, si el valor de generator no coincide con el nombre de un @SequenceGenerator, se asume que es el nombre de una secuencia de la base de datos.

Con estos cambios, la siguiente traza demuestra que Hibernate consigue cinco (allocationSize) identificadores con una sola llamada a select nextval:

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

Los identificadores generados con secuencias son los más recomendables porque Hibernate puede conocer el identificador de una nueva entidad sin tener que guardarla. Solo tiene que pedir a la base de datos el siguiente valor de la secuencia, y a veces ni eso, tal y como acabamos de comprobar en la traza. Esto le permite retrasar el momento de ejecución de los INSERT para mejorar el rendimiento y también realizar inserciones por lotes (batch). En resumidas cuentas, se superan las limitaciones de los identificadores basados en columnas IDENTITY.

Tipo tabla

Esta estrategia es portable a cualquier base de datos porque se basa en SQL estándar. Usa una tabla con dos columnas, una de tipo texto con el nombre del generador de identificadores y otra de tipo numérico con el último valor que fue retornado. Cuando necesite un nuevo identificador del generador, Hibernate lo obtiene a partir de ese valor. En la práctica, es lo que parece: se están simulando secuencias.

Lo más sencillo si creamos el esquema con Hibernate es utilizar el generador sin ningún otro atributo:

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

Hibernate crea la tabla «hibernate_sequences» con un registro para cada clase de tipo entidad cuyo identificador se obtenga mediante esta estrategia. Por ejemplo, en MySQL:

mysql table generator

Podemos tener más control sobre este comportamiento y definir, por ejemplo, el nombre de la tabla con las claves con la anotación @TableGenerator, parecida a @SequenceGenerator. Aquí tienes los atributos:

PropiedadDescripciónValor predeterminado (Hibernate)
nameEl nombre único del generador (no confundir con el de la tabla).Ninguno; este campo es obligatorio.
tableEl nombre de la tabla de la base de datos que usa el generador.El nombre (name) del generador.
catalogEl catálogo de la base de datos con la tabla. Este concepto no existe en todas las bases de datos.Ninguno.
schemaEl esquema de la base de datos con la tabla. Este concepto no existe en todas las bases de datos.Ninguno.
pkColumnNameEl nombre de la columna con la clave primaria de los registros de la tabla.sequence_name
valueColumnNameEl nombre de la columna que contiene el último valor generado.next_val
pkColumnValueEl valor de la clave primaria que distingue cada conjunto de valores generados de otros que puedan estar almacenados en la misma tabla. Ten presente que una tabla puede contener varios generadores.El nombre de la clase.
initialValueEl valor inicial de la columna con el último identificador generado.0
allocationSizeEl incremento del último valor devuelto cuando se asigna un nuevo identificador.50
uniqueConstraintsPermite añadir constraints.Ninguno. La tabla ya tiene una clave primaria.
indexesPermite añadir índices.Ninguno.
@Id
@TableGenerator(name="ent_generator", table="ent_generator")
@GeneratedValue(strategy = GenerationType.TABLE, generator = "ent_generator")
private Long id;

Si nos fijamos en los valores que se van calculando, observaremos que son un tanto peculiares. Por ejemplo, tras varias ejecuciones de la clase Main veremos lo siguiente:

postgresql table generator entityb

Esto es debido a que este generador también cuenta con la optimización que vimos para las secuencias basada en el valor de incremento del contador de identificadores (la propiedad allocationSize, por omisión es 50).

Pese a que con esta estrategia aunamos las ventajas de las secuencias con una compatibilidad absoluta entre bases de datos distintas, hay que pagar un precio demasiado elevado. Incluso con la mejora derivada de allocationSize, el generador TABLE es ineficiente porque hay que consultar y, el verdadero problema, actualizar una tabla para obtener el identificador. El resultado es un pequeño cuello de botella a la hora de insertar los registros que no padecemos con las secuencias.

Si buscamos la máxima compatibilidad de nuestros identificadores con diversas bases de datos, es aconsejable recurrir a la estrategia que veremos a continuación, aunque tampoco está exenta de inconvenientes…

Selección automática del tipo

Nos encontramos ante la opción predeterminada. Delega en el proveedor de JPA la elección de la estrategia que considere oportuna.

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

La generación AUTO parece la más adecuada si la aplicación que estamos desarrollando precisa funcionar con diferentes bases de datos. No obstante, debemos conocer cómo la ha implementado nuestro proveedor de JPA para evitar sorpresas desagradables. En Hibernate el criterio elegido es el siguiente:

MySQL/MariaDB pre 10.3: Hibernate 4 aplica la generación de tipo identidad. A partir de Hibernate 5 se utiliza el pernicioso generador de tipo tabla que acabamos de ver. Si queremos que AUTO siga configurando el generador identidad, recurriremos a la anotación @GenericGenerator propia de Hibernate:

@Id
@GeneratedValue(
    strategy= GenerationType.AUTO, 
    generator="identity"
)
@GenericGenerator(
    name = "identity", 
    strategy = "native"
)
private Long id;

Estamos pidiendo a Hibernate que utilice un generador llamado «identity» del tipo «nativo». En realidad, este generador nativo no existe como tal, sino que aplica el generador identidad si usamos MySQL, MariaDB (todas las versiones) y SQLServer, y el secuencia en los casos de Oracle y PostgreSQL. Cuando se aplique el generador secuencia, el nombre del generador debe coincidir con el de la secuencia de la base de datos a emplear.

MariaDB 10.3, PostgreSQL, Oracle: AUTO equivale al generador secuencia.

SQL Server 2012: generador identidad.

Tipo UUID

La versión 3.1 de JPA incorpora a la especificación varias novedades. La que nos interesa en este artículo es la posibilidad de usar la clase java.util.UUID para declarar cualquier atributo persistible de una clase de tipo entidad, incluyendo el identificador.

UUID significa «identificador único universal», definición estupenda porque no deja mucho a la imaginación. Un UUID pretende identificar un mismo elemento de forma única en sistemas distintos y, quizás, en cualquier sistema existente (la probabilidad de duplicidad es baja). Otra de las fortalezas reseñables de los UUID es que pueden ser generados de manera independiente por cualquier sistema sin requerirse de un servicio central que los proporcione. En la era de los microservicios \ sistemas distribuidos esta característica es sensacional.

El concepto UUID toma cuerpo en un valor de 128 bits representado visualmente como 36 caracteres alfanuméricos separados en cinco grupos. Aquí tienes una muestra:

dd180102-b094-11ed-afa1-0242ac120002

El ejemplo expone el principal punto débil de los UUID: su exagerado tamaño para tratarse de una clave primaria subrogada. Un UUID consume un gran espacio en la base de datos, en concreto el doble de los 64 bits de los enteros grandes que vimos. Esto no solo afecta a la tabla con la clave, sino también a aquellas que la referencien como clave ajena. Otro inconveniente a considerar, de suma importancia, es que el índice para la clave puede tener un rendimiento pobre en ciertas bases de datos (artículo en inglés) debido al caracter pseudoaleatorio y desordenado de los valores de las claves. Por estos motivos, solo se recomienda utilizar UUIDs cuando sea imprescindible.

Hibernate admite UUID como identificadores desde hace años y ahora, como dije, esta capacidad ha sido estandarizada por JPA 3.1. Lo siguiente es lícito:

@Id
private UUID uuid;

¿Qué columna requiere uuid? Podemos guardar la representación visual en un CHAR (36) o equivalente, pero lo eficiente es guardarlo en un formato binario o bien un tipo uuid si la base de datos ofrece esta opción. Esta tabla recoge el tipo idóneo en las bases de datos que contemplamos en el artículo:

MySQL \MariaDBBINARY(16)
PostgreSQLuuid
SQL Serveruniqueidentifier
OracleRAW(16)
La pregunta del millón: ¿cómo creamos un UUID válido? Java cuenta con el método UUID#randomUUID y algunas bases de datos proveen funciones generadoras de UUIDs. Pero para nosotros lo más conveniente es utilizar el generador GenerationType.UUID y olvidarnos del asunto:
@Id
@GeneratedValue(strategy= GenerationType.UUID)
private UUID id;

Listo. Ejecutando la clase Main verás algo como lo que sigue:

12:58:53.856 [main] INFO  com.danielme.demo.Main - 25912bec-4daa-4249-9086-d1e8d0605c00 parent :  
12:58:53.856 [main] INFO  com.danielme.demo.Main -   bea998db-611a-46dc-b361-57630d3cc9db child 1
12:58:53.856 [main] INFO  com.danielme.demo.Main -   99aba102-b27a-4d94-ad50-35621ea5829e child 2

JPA exige a su proveedor calcular un UUID que cumpla con el estándar RFC 4122. Pero hay un problemilla: no indica cuál de las cinco técnicas de cálculo propuestas por el RFC debe aplicarse. Hibernate opta por la denominada versión 4, fundamentada en números pseudoaleatorios. Como alternativa, puedes solicitar a Hibernate que elija la versión 1 del RFC basada en un timestamp y la dirección MAC, aunque Hibernate usa la IP en lugar de la MAC:

@Id
@GeneratedValue
@UuidGenerator(style = UuidGenerator.Style.TIME)
private UUID id;

Claves múltiples con @EmbeddedId

Lo más práctico es usar una clave subrogada. De acuerdo. Con todo, ¿qué pasaría en JPA si nuestra clave primaria fuera múltiple (más de una columna)? Es el caso en que nos encontraremos si tenemos una relación m:n y queremos (o necesitamos) modelar con una clase la tabla intermedia con una clase asociación (lo explico aquí). También tendremos este problema si debemos adaptarnos a bases de datos ya existentes que empleen este tipo de claves.

Resolvemos esta situación con la creación de una clase que contenga todos los campos de la clave múltiple. La marcaremos con @Embeddable, anotación que permite que una clase pueda ser incrustada en otra de tipo entidad de tal modo que sus atributos sean considerados como columnas de la tabla (lo explico aquí). Por ejemplo:

package com.danielme.demo.model;
 
import java.io.Serializable;
 
import jakarta.persistence.Embeddable;
 
@Embeddable
public class EntityPK implements Serializable {
 
    private static final long serialVersionUID = 4451186209733902128L;
 
    private Long idRef1;
 
    private Long idRef2;
 
    public Long getIdRef1() {
        return idRef1;
    }
 
    public void setIdRef1(Long idRef1) {
        this.idRef1 = idRef1;
    }
 
    public Long getIdRef2() {
        return idRef2;
    }
 
    public void setIdRef2(Long idRef2) {
        this.idRef2 = idRef2;
    }
 
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((idRef1 == null) ? 0 : idRef1.hashCode());
        result = prime * result + ((idRef2 == null) ? 0 : idRef2.hashCode());
        return result;
    }
 
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        EntityPK other = (EntityPK) obj;
        if (idRef1 == null) {
            if (other.idRef1 != null)
                return false;
        } else if (!idRef1.equals(other.idRef1))
            return false;
        if (idRef2 == null) {
            if (other.idRef2 != null)
                return false;
        } else if (!idRef2.equals(other.idRef2))
            return false;
        return true;
    }
 

EntityPK debe implementar Serializable y los métodos equals y hashCode. El código mostrado ha sido generado por Eclipse.

Ahora nuestra clave primaria estará compuesta por los dos atributos de esta clase. La usaremos con la anotación @EmbeddedId:

@Entity
public class EntityA
{
    @EmbeddedId
    EntityPK entityPK;

En este caso deberemos crear un objeto EntityPK y definir sus atributos antes de realizar una inserción. El «objeto clave primaria» se utiliza de igual forma que el Long que hemos empleado en los ejemplos anteriores.

em.find(EntityA.class, new EntityPK(1L, 7L));

Si algún atributo de la clave múltiple es autogenerado (@GeneratorValue), consulta la versión mejorada del artículo. Avance: hay que usar @IdClass.

Código de ejemplo

El proyecto está en GitHub (el nombre del repositorio se ha quedado obsoleto). Para más información sobre cómo utilizar GitHub consulta este artículo.

Otros tutoriales relacionados con Hibernate

Persistencia en BD con Spring: Integrando JPA, c3p0, Hibernate y EHCache
Persistencia en BD con Spring Data JPA

3 comentarios sobre “JPA con Hibernate: identificadores y claves primarias. Generación automática. Claves múltiples.

  1. Hola.
    Sabes si realmente es posible generar de forma automática uno de los elementos de una clave compuesta.

    Por ejemplo en tu caso idRef1 se genere de forma automática.

    Gracias por el post!!.

    1. Puede hacerse, al menos con Hibernate y MySQL, utilizando la anotación @GeneratedValue sin @Id

      @Embeddable
      public class EntityPK implements Serializable {

      @GeneratedValue(strategy = GenerationType.AUTO)
      private Long idRef1;

      private Long idRef2;

      Asimismo deberás definir en la base de datos el campo idRef1 de forma adecuada, por ejemplo como AUTO_INCREMENT en MySQL.

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 )

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.