JPA e Hibernate: identificadores y claves primarias

Última actualización: 11/09/2021

logo java

En este tutorial veremos cómo definir las claves primarias de las entidades de JPA con Hibernate y las bases de datos MySQL, PostgreSQL, Oracle y SQL Server. En concreto, usaremos JPA 2.2, la última versión que utiliza como nombre de sus paquetes “javax“. A partir de JPA 3.0, los paquetes empiezan por jakarta, y deberemos usar una versión de Hibernate compatible con ella.

Importante. El contenido del presente artículo es válido, pero ha sido ampliado y mejorado en mi curso Jakarta EE 9.

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

Siempre que se habla de JPA, debemos tener claro que no es más que la especificación de una API estándar por lo que para utilizarla tendremos que recurrir a un producto que la implemente (proveedor o vendor de JPA). Este producto puede ofrecer funcionalidades propias fuera del estándar y si recurrimos a ellas, cambiar de implementación no será sencillo, aunque en la práctica este tipo de situaciones no suele darse.

Proyecto para pruebas

Se trata de un pequeño proyecto Maven para Java 11 cuya funcionalidad es la de crear y persistir unas entidades muy sencillas. He optado por una clase Main en lugar de escribir pruebas con JUnit con el objetivo de mantener el ejemplo lo más simple posible.

diagrama de clases

El pom.xml incluye las siguientes dependencias:

  • Hibernate, en concreto 5.5.4.Final (julio de 2021)
  • c3p0 – pool de conexiones integrado con la versión de Hibernate que usemos.
  • log4j2 – Sistema de bitácora o logging de Apache
  • El driver JDBC que necesitemos. Se incluyen los cuatro utilizados para las pruebas; los de Oracle y SQL Server hay que descargarlos e instalarlos manualmente en nuestro repositorio local (las instrucciones se incluyen en el pom) o, mejor todavía, en un servidor tipo Artifactory o Nexus).
<?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>
	<description>Maven + JPA 2 + Hibernate 4 + Log4j project just for playing around :). Includes configuration snippets for MySQL-MariaDB (default),
	PostgreSQL, SQLServer 2012 and Oracle.  </description>
	<url>https://danielme.com/maven-jpa-2-hibernate-4-log4j-demo/</url>
	<inceptionYear>2012</inceptionYear>


	<licenses>
		<license>
			<name>GNU GENERAL PUBLIC LICENSE, Version 3.0</name>
			<url>http://www.gnu.org/licenses/gpl.html</url>
		</license>
	</licenses>
	
	<developers>
		<developer>
			<id>danielme.com</id>
			<name>Daniel Medina</name>
		</developer>
	</developers>
	
	<scm>
		<url>https://github.com/danielme-com/Maven---JPA-2---Hibernate-4---Log4j</url>
	</scm>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.hibernate.version>5.5.4.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.14.0</log4j2.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-entitymanager</artifactId>
			<version>${project.hibernate.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 and MariaDB -->
		 <dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.49</version>
		</dependency>
		
		<!-- <dependency>
			<groupId>postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<version>9.1-901.jdbc4</version>
		</dependency> -->
		
		<!-- download from http://www.microsoft.com/en-us/download/details.aspx?displaylang=en&id=11774 -->
		<!-- install in a local repository -> mvn install:install-file -Dfile=sqljdbc4.jar -DgroupId=com.microsoft.sqlserver -DartifactId=sqljdbc4 -Dversion=4.0 -Dpackaging=jar -->
		
		<!-- <dependency>
		  <groupId>com.microsoft.sqlserver</groupId>
		  <artifactId>sqljdbc4</artifactId>
		  <version>4.0</version>
		</dependency> -->
		
		<!-- download ojdbc5.jar from oracle http://www.oracle.com/technetwork/database/enterprise-edition/jdbc-111060-084321.html-->
		<!-- install in a local repository ->  mvn install:install-file -Dfile=ojdbc5.jar -DgroupId=com.oracle -DartifactId=ojdbc5 -Dversion=11.2.0.3 -Dpackaging=jar -->
		
		 <!--  <dependency>
			<groupId>com.oracle</groupId>
			<artifactId>ojdbc5</artifactId>
			<version>11.2.0.3</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. Asimismo, está configurado de tal modo que al iniciarse Hibernate genere el esquema en el caso de que no exista (lo que implica que el usuario que usemos para conectarnos debe tener los permisos suficientes). Para más información consultar este tutorial.

<?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============ -->
		<!-- =============================== -->

		    <!-- MySQL -->
		    <property name="hibernate.connection.url" value="jdbc:mysql://localhost:3306/danielme" />
			<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
			<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect "/>
			
			<!-- MariaDB is fully compatible with MySQL, there isn't a custom dialect-->
			<!-- <property name="hibernate.connection.url" value="jdbc:mysql://192.168.0.103:3366/danielme" />
			<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
			<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect "/>-->
			
			<!-- 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" />
			<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>-->
			
			<!-- SQL SERVER 2012-->
			<!-- <property name="hibernate.connection.url" value="jdbc:sqlserver://192.168.2.102:1433;databaseName=danielme" />--><!-- http://msdn.microsoft.com/en-us/library/ms378428.aspx -->
			<!-- <property name="hibernate.connection.driver_class" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />
				 <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServer2008Dialect"/>-->
			
			
			<!-- 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" />
			<property name="hibernate.dialect" value="org.hibernate.dialect.Oracle10gDialect"/>--> <!-- compatible with 10g and 11g -->			
			
		<!-- =============================== -->
		<!-- =============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 ENVIROMENT. 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>

Estas son las clases que conforman el modelo.


package com.danielme.demo.model;

import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.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 javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.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


Se usa un fichero de configuración básico de log4j2 para mostrar en la salida estándar todos los mensajes a partir del nivel DEBUG y poder 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>

Ya solo nos queda nuestra clase Main, que creará un objeto de EntidadA y lo relacionará con dos instancias de EntidadB para que sean guardados en la base de datos. Con anterioridad habremos creado nuestro EntityManager mediante la factoría correspondiente para poder crear un gestor de entidades que nos permita trabajar con el contexto de persistencia.

package com.danielme.demo;

import java.util.LinkedList;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import com.danielme.demo.model.EntityA;
import com.danielme.demo.model.EntityB;

import org.apache.log4j.Logger;


public class Main
{
	
	
	private static final Logger logger = Logger.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(new LinkedList<EntityB>());
        entityA.getEntities().add(entityB1);
        entityA.getEntities().add(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
        {
        	//rememeber release always the connection
        	entityManager.close();
        } 
        
	}

}

El proyecto visto en Eclipse queda con la siguiente estructura.

MavenJpaHibernateLog4jDemo on Eclipse

Claves simples

Toda entidad debe poseer un atributo -o heredarlo- no final anotado como @Id que será el que se utilice para identificar de forma unívoca a cada objeto de esa entidad, lo que en la base de datos se traducirá en una clave primaria.

  • Primitivos: byte, int, long, short, float, double, char
  • Wrappers de los primitivos
  • String
  • Fecha: java.util.Date, java.sql.Date
  • Numéricos grandes:java.math.BigDecimal, java.math.BigInteger

La elección del identificador la trato en el tutorial mencionado anteriormente. En él explico por qué es preferible el uso de claves subrogadas: valores numéricos sin ningún significado en particular.

La anotación @Id no admite atributos, pero se puede acompañar de @Column para personalizar la columna. Recuérdese que por omisión se considera que el nombre del atributo y de su columna asociada en la tabla son el mismo.

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

Si ejecutamos nuestro proyecto de ejemplo, obtendremos el error:

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

Con la configuración actual, es responsabilidad de la aplicación generar un valor adecuado para el identificador\clave primaria antes de incluir una entidad en el contexto de persistencia con persist o merge. Sin embargo, cuando optamos por claves subrogadas la generación de identificador puede delegarse en JPA, aunque esto se verá condicionado por el proveedor que usemos (Hibernate) y la base de datos subyacente.

Esta funcionalidad se consigue complementando la anotación @Id con la anotación @GeneratedValue que configura la generación de forma totalmente automática y transparente de un identificador\clave primaria cada vez que sea necesario insertar una nueva entidad en las tablas correspondientes. Contamos con cuatro estrategias que vamos a analizar a continuación.

@GeneratedValue(strategy=GenerationType.IDENTITY)

MySQL/MariaDB y SQLServer tienen un tipo de columna autonumérica (AUTO_INCREMENT e IDENTITY respectivamente) ideal para ser usadas como clave primaria. Asimismo, PostgreSQL cuenta con el tipo SERIAL que simula esta funcionalidad creando secuencias.

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

Oracle incorporó el tipo IDENTITY en la versión 12c, y podemos utilizarlo con el dialecto org.hibernate.dialect.Oracle12cDialect disponible en 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

Esta estrategia es sencilla y práctica, pero no es oro todo lo que reluce: el identificador de un nuevo registro no se conocerá 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).

@GeneratedValue(strategy=GenerationType.SEQUENCE)

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')
	@Id
	@GeneratedValue(strategy=GenerationType.SEQUENCE)
	private Long id;

Si simplemente definimos la estrategia de generación como secuencia, Hibernate creará una única secuencia por esquema denominada “hibernate_sequence”. De nuestras bases de datos de ejemplo, las únicas que no tienen secuencias son MySQL y MariaDB pre 4.3. SQLServer 2012 sí que las tiene, pero es necesario utilizar el dialecto org.hibernate.dialect.SQLServer2012Dialect disponible a partir de Hibernate 4.2.

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

Es posible, y supone la praxis habitual, definir secuencias específicas para cada clave (o una común a varias de ellas) con la anotación @SequenceGenerator. Si las secuencias no existen, Hibernate las creará por nosotros. Esta anotación tiene como parámetros opcionales allocationSize, que indica el incremento numérico del avance de la secuencia y cuyo valor por omisión es 50, e initialValue cuyo valor predeterminado es 1 tal y como cabría esperar.

En los fragmentos de código que se muestran a continuación, cada entidad cuenta con su propia secuencia en la base de datos.

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

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.

La siguiente traza demuestra el comportamiento que acabo de describir.

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 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.

@GeneratedValue(strategy=GenerationType.TABLE)

Esta estrategia es portable a cualquier base de datos porque emplea SQL estándar. Usa de una tabla con dos columnas, una de tipo texto con el nombre que identifica al generador de identificadores y otra de tipo numérico con el último valor que fue retornado. Cuando necesite un nuevo identificador del generador, Hibernate la obtiene sumando uno a 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 por nosotros una tabla “hibernate_sequences” con un registro en esa tabla para cada entidad cuyo identificador se obtenga mediante esta estrategia. Por ejemplo, en MySQL:

mysql table generator

Y del mismo modo con el resto de BD.

Podemos tener más control sobre este mapeo, y definir por ejemplo el nombre de la tabla con las claves con la anotación @TableGenerator:

	@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 generando, observaremos que son un tanto “particulares”. Por ejemplo, en PostgreSQL para EntidadB tras varias inserciones veremos lo siguiente:

postgresql table generator entityb

Esto es debido a que este generador también cuenta con la optimización que vimos para la secuencias basada en el valor de incremento del contador de identificadores (la propiedad allocationSize). El siguiente código muestra todos los parámetros configurables para la tabla generadora.

@Entity
public class EntityA
{
	@Id
	@TableGenerator(name="ent_generator",
			table="ent_generator",
			pkColumnName="ent_name",
			valueColumnName="ent_val",
			pkColumnValue="entityA",
			initialValue=1,
			allocationSize=1)
	@GeneratedValue(strategy = GenerationType.TABLE, generator = "ent_generator")
	private Long id;

...

@Entity
public class EntityB
{
	@Id
	@TableGenerator(name="ent_generator",
			table="ent_generator",
			pkColumnName="ent_name",
			valueColumnName="ent_val",
			pkColumnValue="entityB",
			initialValue=1,
			allocationSize=1)
	@GeneratedValue(strategy = GenerationType.TABLE,  generator = "ent_generator")
	private Long id;

La tabla de claves quedará así en Oracle:

oracle table generator

Y en la generación de claves se aplican incrementos de una unidad

oracle table generator entityb

La flexibilidad que aporta esta estrategia tiene un precio alto, hasta tal punto de no ser recomendable. Aún con la optimización derivada de allocationSize 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 veremos a continuación, aunque tampoco está exenta de inconvenientes.

@GeneratedValue(strategy=GenerationType.AUTO)

Es la opción predeterminada. Delega en la implementación de JPA la elección de la estrategia que considere oportuna para la base de datos que se emplee.

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

En Hibernate, si generamos el esquema y usamos esta estrategia, el criterio elegido para cada BD es el siguiente:

  • MySQL/MariaDB: BIGINT y AUTO_INCREMENT. La propia columna generará su valor secuencialmente. A partir de Hibernate 5.x se utiliza el generador de tipo tabla que veremos más adelante; si queremos que AUTO siga empleando claves de tipo IDENTITY (modo “nativo” de MySQL), podemos hacerlo así:
    @Id
    @GeneratedValue(
        strategy= GenerationType.AUTO, 
        generator="identity"
    )
    @GenericGenerator(
        name = "identity", 
        strategy = "native"
    )
    private Long id;
    
  • MariaDB a partir de la versión 10.3: secuencias.
  • PostgreSQL: BIGINT y una única secuencia denominada “hibernate_sequence” que se usará para obtener todas las claves. Si queremos una secuencia por cada tabla, ver la sección anterior.
  • SQL Server 2012: numeric(19,0) de tipo identidad, por lo que nos encontramos en un caso similar a MySQL.
  • Oracle: number(19,0) y, al igual que en PostgreSQL, una única secuencia llamada “hibernate_sequence”.

La generación AUTO parece la más adecuada si la aplicación que estamos desarrollando tiene que ser compatible con varias bases de datos, pero debemos conocer cómo la ha implementado el proveedor de JPA que usemos. Con Hibernate, 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 multiples

¿Qué pasaría 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 mapear en una entidad la tabla intermedia. También tendremos este problema si debemos adaptarnos a bases de datos ya existentes en las que no se haya optado por claves subrogadas.

Frente a esta situación, lo que haremos es crear una clase que contenga todos los atributos de nuestra clave múltiple. La marcaremos con la anotación @Embeddable que permite que una clase pueda ser “incrustada” en una entidad de tal modo que sus atributos sean columnas en la tabla asociada a la entidad. Por ejemplo:

package com.danielme.demo.model;

import javax.persistence.Embeddable;

@Embeddable
public class EntityPK implements Serializable
{
	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;
	}
}

Ahora nuestra clave primaria estará compuesta por los dos atributos de esta clase y 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));

Código de ejemplo

El proyecto completo se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar 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 e Hibernate: identificadores y claves primarias

  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.

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 .