JPA + Hibernate: Claves primarias

logo java

JPA – Java Persistence API es una especificación (concretamente JSR-317 en su última versión) que debutó con JEE5 junto a EJB 3.0 y que intentó superar la complejidad que suponía hasta el momento utilizar EJB (en el caso de JPA hablaríamos de los EJB de Entidad usados para la persistencia) , además de estandarizar hasta cierto punto los ORM que como Hibernate o TopLink por aquel entonces ya eran usados masivamente, especialmente Hibernate que suponía casi un estándar de facto (de hecho JPA se “inspiró” bastante en este último). La versión 2.0 de la especificación amplió sus capacidades con nuevas opciones de mapeo y una API para la creación de consultas programáticamente, algo con lo que Hibernate ya contaba bastante antes de la aparición de JPA 1.0.La versión 2.1 está ya muy cerca de su publicación final

Siempre que se habla de JPA tenemos que tener muy claro que no es más que una especificación por lo que para utilizarla tendremos que recurrir a un producto que implemente este estándar, y que además este producto puede ofrecer soluciones y alternativas propias fuera del estándar pudiéndose combinar ambas (aunque en este caso sería complicado cambiar de implementación a posteriori). De todas implementaciones, voy a utilizar Hibernate como “vendor” por ser la más conocida y utilizada aunque existen otras alternativas excelentes como OpenJPA de Apache, ObjectDB o la ya mencionada TopLink.

El objetivo de este tutorial es doble:

  • Proporcionar un ejemplo listo para usar de JPA con la implementación de Hibernate configurado para las BBDD más populares. Este ejemplo forma parte de la colección de demos de danielme.com(aquí) y está disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.
  • Utilizar este ejemplo como base para estudiar las posibilidades que JPA con Hibernate ofrece para la gestión de claves primarias en varias bases de datos.

Entorno de pruebas:

Requisitos: Conocimientos básicos de Maven y JPA (sólo se estudiarán las claves primarias)

Proyecto para pruebas

Se trata de un simple proyecto Maven con un Main cuya funcionalidad es la de crear y persistir unas clases muy sencillas

diagrama de clases

El pom.xml incluye los plugins para ejecutar directamente el proyecto (exec-maven-plugin) o empaquetarlo todo en un jar (maven-shade-plugin). Tendremos las siguientes dependencias (últimas versiones en el momento de redactar este artículo):

  • Hibernate
  • c3p0 – pool de conexiones integrado con la versión de Hibernate que usemos.
  • log4j
  • 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>
	<inceptionYear>2013</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>
	

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.build.mainClass>com.danielme.demo.Main</project.build.mainClass>
		<project.hibernate.version>4.1.7.Final</project.hibernate.version>	
		<project.java.version>1.5</project.java.version>			
	</properties>
	
	
	<build>
	
		<pluginManagement>
		
			<plugins>
				
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-compiler-plugin</artifactId>
					<version>3.0</version>
					<configuration>
						<source>${project.java.version}</source>
						<target>${project.java.version}</target>
					</configuration>
				</plugin>
				
				<!-- standard runnable jar -->
				 <plugin>
	                    <groupId>org.apache.maven.plugins</groupId>
	                    <artifactId>maven-jar-plugin</artifactId>
	                    <version>2.4</version>
	                    <configuration>
	                        <archive>
	                            <manifest>
	                                <addClasspath>true</addClasspath>
	                                <mainClass>${project.build.mainClass}</mainClass>
                            </manifest>
	                        </archive>
	                    </configuration>
	                </plugin>

				<!-- run project mvn clean package exec:java-->
				<plugin>
					<groupId>org.codehaus.mojo</groupId>
					<artifactId>exec-maven-plugin</artifactId>
					<version>1.2.1</version>
					<executions>
						<execution>
							<goals>
								<goal>java</goal>
							</goals>
						</execution>
					</executions>
					<configuration>
						<mainClass>${project.build.mainClass}</mainClass>
					</configuration>
				</plugin>
				
				<!-- packages all dependencies in one big jar mvn clean package shade:shade-->
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-shade-plugin</artifactId>
					<executions>
						<execution>
							<phase>package</phase>
							<goals>
								<goal>shade</goal>
							</goals>
						</execution>
					</executions>
					<configuration>
						<finalName>${artifactId}-${version}-all-deps</finalName>	
						<transformers>
	                		<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
	                 		 <mainClass>${project.build.mainClass}</mainClass>
	               		 	</transformer>
	              		</transformers>		
	              		<!-- http://zhentao-li.blogspot.com.es/2012/06/maven-shade-plugin-invalid-signature.html -->
						 <filters>
				            <filter>
				              <artifact>*:*</artifact>
				              <excludes>
				                <exclude>META-INF/*.SF</exclude>
				                <exclude>META-INF/*.DSA</exclude>
				                <exclude>META-INF/*.RSA</exclude>
				              </excludes>
				            </filter>
          				</filters>
	              				
					</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>log4j</groupId>
			<artifactId>log4j</artifactId>
			<version>1.2.17</version>
		</dependency>
		
		<!-- =================================== -->
		<!-- =========== JDBC DRIVERS ========== -->
		<!-- =================================== -->

		<!-- Mysql and MariaDB -->
		 <dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.21</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 para que al iniciarse Hibernate genere el esquema en el caso de que no exista (lo que implica que el usuario que usemos para conectarnos tenga los permisos suficientes).

<?xml version="1.0" encoding="UTF-8" ?>

<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">

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

		<provider>org.hibernate.ejb.HibernatePersistence</provider>

		<class>com.danielme.demo.model.EntityA</class>
		<class>com.danielme.demo.model.EntityB</class>

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

		    <!-- MySQL -->
		    <property name="hibernate.connection.url" value="jdbc:mysql://192.168.0.103:3306/danielme" />
			<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
			<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect"/>
			
			<!-- 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.MySQL5Dialect"/>-->
			
			<!-- 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========== -->
		<!-- =============================== -->				
			<!-- actualiza automáticamente el esquema, NO RECOMENDADO EN PRODUCCIÓN. Comprobar los grants del usuario -->
			<property name="hibernate.hbm2ddl.auto" value="update"/>		
			<property name="hibernate.show_sql" value="true"/>
			<!-- autocommit no recomendado por Hibernate -->
			<property name="hibernate.connection.autocommit" value="false" />			
			
		<!-- =============================== -->
		<!-- ==============POOL============= -->
		<!-- =============================== -->
			<!-- Pool de conexiones con la integración Hibernate-C3P0-->
			<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" />
		
		</properties>

	</persistence-unit>

</persistence>

Las clases que conforman nuestro modelo y que serán entidades persistibles (recordad siempre que el estándar nos obliga a definirlas en el persistence.xml)


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;


	public List<EntityB> getEntities()
	{
		return entities;
	}

	public void setEntities(List<EntityB> entities)
	{
		this.entities = entities;
	}

	public String getName()
	{
		return name;
	}

	public void setName(String name)
	{
		this.name = name;
	}

	public Long getId()
	{
		return id;
	}

	public void setId(Long id)
	{
		this.id = id;
	}
	
	
	
	
}

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;

	
	public Long getId()
	{
		return id;
	}

	public void setId(Long id)
	{
		this.id = id;
	}

	public String getName()
	{
		return name;
	}

	public void setName(String name)
	{
		this.name = name;
	}

	public EntityA getEntityA()
	{
		return entityA;
	}

	public void setEntityA(EntityA entityA)
	{
		this.entityA = entityA;
	}
	
	
}


Se usa un fichero de configuración básico de log4j 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" ?>

<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">

<!-- http://wiki.apache.org/logging-log4j/Log4jXmlFormat -->

<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
    
	<appender name="console" class="org.apache.log4j.ConsoleAppender">
		<param name="Target" value="System.out" />
		<layout class="org.apache.log4j.PatternLayout">
			<param name="ConversionPattern" value="%d{MM-dd-yyyy hh:mm:ss,SSS a} %5p %c{1}:%L - %m%n" />
		</layout>
	</appender>

	<root>
		<priority value="debug" />
		<appender-ref ref="console" />
	</root>

</log4j:configuration>

Ya sólo nos queda nuestra clase Main, que creará una objeto de la clase EntidadA y los relacionará con dos objetos de la clase EntidadB para que sean guardados en la base de datos. Previamente habremos creado nuestro EntityManager con la factoría correspondiente para poder utilizar el contexto de persitencia que hemos definido.

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 (mvn clean eclipse:eclipse) queda con la siguiente estructura.

MavenJpaHibernateLog4jDemo on Eclipse

Independientemente del proyecto de ejemplo disponible en GitHub, se puede descargar aquí el proyecto “limpio” con el código tal y como se ha mostrado hasta ahora para poder seguir el artículo. Obviamente habrá que configurar en cada caso la conexión a la base de datos con un usuario que tenga los permisos adecuados (en este artículo se va permitir que Hibernate genere automáticamente los esquemas).

Claves simples

Toda entidad debe tener un atributo (o heredarlo) anotado como @Id (aunque también se podría anotar el getter) 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. Aunque lo habitual sea utilizar como clave primaria un número entero sin ningún significado desde el punto de vista de la lógica de negocio de la aplicación, la especificación JPA es muy flexible y admite los siguientes tipos de atributos:

  • 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

Esta anotación no admite atributos, pero se puede acompañar de @Column para definir el nombre de la columna que , por lógica, deberá ser insertable, no updatable y no nullable. Recuérdese que por defecto 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

ya que no hemos definido el id de cada objeto. Con el mapeo actual que hemos definido, es responsabilidad de la aplicación generar un valor adecuado para la clave primaria y que además garantice su unicidad para toda la tabla. Sin embargo, en la mayor parte de las ocasiones podemos evitar tener que implementar la gestión de las claves primarias y hacer que esta recaiga en el vendor de JPA que usemos que a su vez se apoyará en la base de datos que utilicemos (por ejemplo usando una secuencia).

Esta funcionalidad se consigue complementando la anotación @Id con la anotación @GeneratedValue que permitirá la generación de forma totalmente automática y transparente de una clave primaria cada vez que sea necesario insertar un nuevo objeto/registro. Para esta generación JPA define 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. Podemos usar esta característica mediante este tipo de generador:

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

En el caso de Oracle, al no tener esta funcionalidad obtendremos el correspondiente error:

org.hibernate.MappingException: org.hibernate.dialect.Oracle10gDialect does not support identity key generation
@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. Ese valor se define dentro de un rango, y se va incrementando según un valor de incremento a medida que se vayan solicitando nuevos valores (por ejemplo en Oracle con la sentencia NOMBRE_SEQ.nextval).

	@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 ejemplo, la única que no soporta secuencias es MySQL. SQLServer 2012 sí que las tiene, pero de momento no hay un dialecto de Hibernate que soporte las nuevas funcionalidades de esta versión por lo que en ambos casos obtendremos un error:

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

Se pueden 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 (por lo que probablemente queramos definirlo a un valor que nos resulte más conveniente como por ejemplo 1) y initialValue cuyo valor por defecto es 1 tal y como cabría esperar.

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

....

@Entity
public class EntityB
{

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

Delega en el “vendor” la elección de la estrategia que considere oportuna en función de la BD subyacente.

	@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.
  • PostgreSQL: BIGINT y una única secuencia denominada “hibernate_sequence” que se usará para obtener todas las claves, por lo que las claves serán únicas para toda la BD y en cada tabla podríamos ver valores no consecutivos. Si queremos una secuencia por cada tabla, ver la sección de secuencias.
  • SQL Server 2012: numeric(19,0) de tipo identidad, por lo que nos encontramnos en un caso similar a MySQL.
  • Oracle: number(19,0) y, al igual que en PostgreSQL, una única secuencia llamada “hibernate_sequence” para todo el esquema para obtener la clave.

Es la estrategia más cómoda, pero debemos conocer qué hace nuestro framework ya que el estándar le da vía libre y es posible que el resultado no sea el deseado.

@GeneratedValue(strategy=GenerationType.TABLE)

Por último, veremos la estrategia más portable de todas ya que se puede emplear de igual manera en cualquier BD. Se basa en utilizar una tabla con dos columnas, una de tipo texto con el nombre que identifica al generador de claves primarias (tendremos uno por cada tabla) y otra de tipo numérico con el último valor que fue asignado como PK para un registro de esa tabla.

Lo más sencillo si generamos el esquema con Hibernate es simplemente utilizar el generador sin ningún otro atributo.

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

Hibernate crea por nosotros una tabla “hibernate_sequences” sin PK y con un registro en esa tabla para cada entidad cuya PK 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 id que se van generando, veremos que tiene unos valores un tanto “particulares” (Hibernate lo hace así por razones de eficiencia). Por ejemplo, en PostgreSQL para EntidadB tras varias inserciones veremos lo siguiente:

postgresql table generator entityb

Si no queremos este comportamiento y preferimos que las claves se incrementen de uno en uno podemos configurarlo a nuestro gusto. El siguiente snippet 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

Claves multiples

Con la anotación @Id indicamos un atributo que puede ser de cierto tipo como clave primaria, pero ¿qué pasaría si nuestra clave primaria es múltiple? Es el caso en que nos encontraremos si tenemos una relación m:n y queremos mapear en una entidad la tabla intermedia.

Frente a esta situación, lo que haremos es crear una clase que contenga todos los atributos de nuestra clave múltiple, y la marcaremos con la anotación @Embeddable que permite que una clase pueda ser incluída en una entidad y sus atributos formen parte de su mapeo a la BD.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.

3 Responses to JPA + Hibernate: Claves primarias

  1. David dice:

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

    • danielme.com dice:

      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.

  2. David dice:

    Muy buena explicación de la problemática.

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. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: