6. Spring Data JPA Hibernate
6.1. Introducción
Vamos a retomar la base [dbproduitscategories] gestionada por el proyecto [spring-jdbc-04] e implementaremos las dos interfaces [IDao<Categorie>, IDao<Produit>] definidas en este proyecto. Esto nos permitirá varias cosas:
- comparar los códigos de implementación;
- utilizar la misma capa de pruebas;
- comparar el rendimiento de las dos implementaciones;
![]() |
- la capa [JDBC] está implementada por el proyecto [mysql-config-jdbc] estudiado en el apartado 3.3;
Pasamos ahora a las demás capas.
6.2. Configuración del entorno de trabajo
Con STS, importe el proyecto [mysl-config-jpa-hibernate] [1] que se encuentra en la carpeta [<exemples>/spring-database-config/mysql/eclipse] [2]:
![]() |
Este proyecto configura la capa [Spring JPA Hibernate] del proyecto. Cada implementación JPA tiene su propio proyecto de configuración.
A continuación, importe el proyecto [spring-jpa-generic] [1] que se encuentra en la carpeta [<exemples>/spring-database-generic/spring-jpa] [2]:
![]() |
Una vez hecho esto, reinicie el entorno Maven (Alt-F5) de todos los proyectos presentes en [Package Explorer]:
![]() |
A continuación, para comprobar el entorno de trabajo, ejecute la configuración de ejecución denominada [spring-jpa-generic-JUnitTestDao-hibernate]:
![]() |
Esta configuración ejecuta la prueba [JUnitTestDao]. Esta prueba debe completarse con éxito:
![]() |
6.3. El proyecto de configuración de la capa JPA
![]() |
Este proyecto tiene como función configurar la capa JPA de la arquitectura que se muestra a continuación:
![]() |
6.3.1. Configuración de Maven
El proyecto es un proyecto Maven y está configurado mediante el siguiente archivo [pom.xml]:
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jpa</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>configuration mysql openjpa</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- dependencias variables ********************************************** -->
<!-- Proveedor JPA -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
</dependency>
<!-- dependencias constantes ********************************************** -->
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<!-- Contexto de Spring -->
<!-- configuración heredada JDBC -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- líneas 5-7: el artefacto Maven generado por este proyecto. Los proyectos de configuración de las otras implementaciones JPA (Eclipselink y OpenJpa) utilizarán este mismo artefacto. Esto significa que solo uno de estos proyectos puede estar activo en un momento dado. Por lo tanto, hay que evitar tenerlos todos presentes en [Package Explorer]. Solo se necesita uno;
- líneas 10-14: el proyecto Maven principal que define version para la mayoría de las dependencias necesarias del proyecto;
- líneas 19-22: la biblioteca Hibernate;
- líneas 25-28: la biblioteca Spring Data;
- líneas 32-34: el proyecto de configuración de la capa JPA se basa en el de configuración de la capa JDBC, que define, entre otras cosas, el controlador JDBC del SGBD utilizado y las coordenadas de la base de datos que se va a utilizar;
- líneas 35-39: el proyecto de configuración de la capa JDBC incluye la biblioteca [Spring JDBC], que aquí se sustituye por la biblioteca [Spring Data JPA]. Por lo tanto, se indica que no se incluya en las dependencias del proyecto. Sin embargo, si se mantiene, no provoca errores;
Al final, las dependencias del proyecto son las siguientes:
![]() |
6.3.2. Configuración de Spring
![]() |
La clase [ConfigJpa] configura el proyecto Spring:
package generic.jpa.config;
import javax.persistence.EntityManagerFactory;
import generic.jdbc.config.ConfigJdbc;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
@Import({ ConfigJdbc.class })
public class ConfigJpa {
// el proveedor JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
hibernateJpaVendorAdapter.setGenerateDdl(true);
return hibernateJpaVendorAdapter;
}
// paquetes de entidades JPA
public final static String[] ENTITIES_PACKAGES = { "generic.jpa.entities.dbproduitscategories" };
// fuente de datos
@Bean
public DataSource dataSource() {
// fuente de datos TomcatJdbc
DataSource dataSource = new DataSource();
// configuración de acceso JDBC
dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
// conexiones abiertas inicialmente
dataSource.setInitialSize(5);
// resultado
return dataSource;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan(ENTITIES_PACKAGES);
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// Gestor de transacciones
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- línea 18: la clase es una clase de configuración de Spring;
- línea 19: importa los beans definidos por la clase de configuración [ConfigJdbc], que se utilizó para configurar el proyecto Spring [mysql-config-jdbc]. Se trata de los filtros jSON;
- líneas 23-30: definen la implementación JPA utilizada, en este caso la implementación de Hibernate (línea 25);
- línea 26: se puede elegir si se muestran o no las operaciones SQL ejecutadas por la implementación de Hibernate;
- línea 27: se indica a Hibernate el SGBD conectado. Esta configuración es importante. Permite a Hibernate utilizar el dialecto SQL de SGBD MySQL, incluida su parte propietaria. Además, le proporciona información sobre los tipos SQL y los objetos de SGBD que podrá utilizar. Es esta capacidad de la implementación JPA para adaptarse a un SGBD concreto lo que le confiere una gran portabilidad entre SGBD;
- línea 28: Hibernate puede generar o no las tablas de la base de datos de destino a partir de las entidades JPA que encuentre. Esta generación solo tiene lugar si las tablas están ausentes. Si ya están presentes, no se hace nada. Utilizaremos esta capacidad de generar las tablas cuando presentemos cómo se han generado los scripts SQL de generación de las diferentes bases de datos utilizadas en este documento;
- línea 33: el paquete en el que se encuentran las entidades JPA de la base [dbproduitscategories];
- líneas 36-49: la fuente de datos [tomcat-jdbc] vinculada a la base [dbproduitscategories];
- líneas 52-60: el bean denominado [entityManagerFactory] (debe llamarse así) es el bean que creará el objeto [EntityManager] que gestiona el contexto de persistencia JPA. Todas las operaciones JPA pasan por él. El uso de [Spring Data JPA] hace que nunca vayamos a utilizar este objeto nosotros mismos. Sin embargo, debemos configurarlo. Necesita conocer lo siguiente:
- la implementación JPA utilizada (línea 55);
- la fuente de datos utilizada (línea 57);
- las entidades JPA de esta fuente (línea 56);
- línea 58: inicializa EntityManager con esta información;
- línea 59: devuelve el singleton [entityManagerFactory];
- líneas 63-68: definen el gestor de transacciones. Debe llamarse [transactionManager];
- línea 65: se crea un gestor de transacciones JPA;
- línea 66: se conecta a la fuente de datos de la línea 37 a través del bean [entityManagerFactory] (líneas 53 y 57);
Solo el bean de las líneas 23-30 depende de la implementación JPA utilizada. Los demás beans se basan en él.
6.3.3. Las entidades de la capa [JPA]
![]() |
![]() |
La base de datos de destino es la base [dbproduitscategories] con sus dos tablas [CATEGORIES] y [PRODUITS]. Hemos visto que también tiene otras tres tablas, [USERS, ROLES, USERS_ROLES], que se utilizarán para proteger el servicio web que se implementará en la web. Por ahora, ignoraremos estas tablas. Recordemos la estructura de las tablas [CATEGORIES] y [PRODUITS]:
La tabla [PRODUITS] es la siguiente:
![]() |
- [ID]: la clave primaria autoincrementada de la tabla [2];
- [NOM]: el nombre único del producto [4];
- [PRIX]: el precio del producto;
- [DESCRIPTION]: la descripción del producto;
- [VERSIONING] es el n.º de version del producto. Su version inicial es 1 [3]. Cada vez que se modifique el producto, su n.º version se incrementará mediante el código que gestiona la tabla;
- [CATEGORIE_ID]: la clave externa de la tabla [CATEGORIES] para designar la categoría a la que pertenece el producto;
![]() |
- en [1-3], la clave externa [CATEGORIE_ID] de la tabla [PRODUITS]. Se dirige a la columna [ID] de la tabla [CATEGORIES] [4-5];
- cuando se elimina una categoría, también se eliminan todos los productos vinculados a ella [6]. Es importante señalar este punto, ya que se utiliza en la construcción de la capa [DAO] que explota la base [dbproduitscategories];
La tabla [CATEGORIES] de categorías es la siguiente:
![]() |
- [ID]: clave primaria autoincrementada;
- [VERSIONING]: n.º de version de la categoría;
- [NOM]: nombre único de la categoría;
A continuación describiremos las entidades JPA, [Produit] y [Categorie], imágenes de las tablas [PRODUITS] y [CATEGORIES].
![]() |
6.3.3.1. La interfaz [AbstractCoreEntity]
La interfaz [AbstractCoreEntity] está implementada por las entidades JPA, [Categorie] y [Produit]:
package generic.jpa.entities.dbproduitscategories;
public interface AbstractCoreEntity {
// getter y setter de los campos [id], [version], [entityType]
public Long getId();
public void setId(Long id);
public Long getVersion();
public void setVersion(Long version);
public enum EntityType {
PROXY, POJO
}
public EntityType getEntityType();
public void setEntityType(EntityType entityType);
}
Esta interfaz, implementada por las dos entidades JPA, sirve simplemente para enumerar los métodos para leer/escribir los campos [id], [version] y [entityType] de estas entidades. La función del campo [entityType] se explicará más adelante;
6.3.3.2. La entidad JPA [Produit]
La clase [Produit] es la entidad JPA asociada a una línea de la tabla [PRODUITS]:
![]() |
package generic.jpa.entities.dbproduitscategories;
import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = ConfigJdbc.TAB_PRODUITS)
@JsonFilter("jsonFilterProduit")
public class Produit implements AbstractCoreEntity {
// Propiedades
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = ConfigJdbc.TAB_JPA_ID)
protected Long id;
@Version
@Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
protected Long version;
@Transient
protected EntityType entityType = EntityType.POJO;
@Transient
@JsonIgnore
protected String simpleClassName = getClass().getSimpleName();
// propiedades
@Column(name = ConfigJdbc.TAB_PRODUITS_NOM, unique = true, length = 30, nullable = false)
private String nom;
@Column(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID, insertable = false, updatable = false, nullable = false)
private Long idCategorie;
@Column(name = ConfigJdbc.TAB_PRODUITS_PRIX, nullable = false)
private double prix;
@Column(name = ConfigJdbc.TAB_PRODUITS_DESCRIPTION, length = 100)
private String description;
// la categoría
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID)
private Categorie categorie;
// constructores
public Produit() {
}
public Produit(Long id, Long version, String nom, Long idCategorie, double prix, String description,
Categorie categorie) {
this.id = id;
this.version = version;
this.nom = nom;
this.idCategorie = idCategorie;
this.prix = prix;
this.description = description;
this.categorie = categorie;
}
// firma
public String toString() {
return String.format("[id=%s, version=%s, nom=%s, prix=10.2f, desc=%s, idCategorie=%s]", id, version, nom, prix,
description, idCategorie);
}
// ------------------------------------------------------------
// redefinición [equals] y [hashcode]
@Override
public int hashCode() {
Long id = getId();
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractCoreEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractCoreEntity other = (AbstractCoreEntity) entity;
Long id = getId();
Long otherId = other.getId();
return id != null && otherId != null && id.equals(otherId);
}
// getters y setters
...
public void setCategorie(Categorie categorie) {
// tipo de entidad
if (entityType == EntityType.PROXY) {
throw new ProxyException(1005, new RuntimeException(
"On ne peut changer la catégorie d'un produit de type [PROXY]"), simpleClassName);
}
this.categorie = categorie;
}
}
- línea 21: la anotación [@Entity] convierte la clase [Produit] en una entidad gestionada por la capa [JPA]. También se puede escribir [@Entity(name="MonProduit")], lo que da a la entidad el nombre [MonProduit]. A falta de esta información, el nombre de la entidad es el nombre de la clase, en este caso [Produit]. Esta nomenclatura se hace necesaria cuando entre las entidades hay dos clases de paquetes diferentes que llevan el mismo nombre;
- línea 22: la anotación [@Table(name = "PRODUITS")] indica que la clase [Produit] es la imagen objeto de una línea de la tabla [PRODUITS] de la base de datos;
- línea 23: el nombre del filtro jSON que se debe aplicar a la entidad. Veremos que la propiedad [categorie] de la línea 58 no siempre está disponible. Por lo tanto, hay que excluirla de la representación jSON del objeto. Para ello necesitamos un filtro. Así pues, en un filtro denominado [jsonFilterCategorie] indicaremos si queremos o no la propiedad [categorie];
- línea 26: la anotación [@Id] convierte el campo anotado en el campo asociado a la clave primaria de la tabla de la línea 19;
- línea 27: la anotación [@GeneratedValue(strategy = GenerationType.IDENTITY)] establece el modo de generación automática de la clave primaria en la tabla [PRODUITS]. Es el atributo [strategy] el que lo determina. Existen diferentes modos:

La estrategia [IDENTITY] no está disponible para todos los SGBD. De los seis SGBD probados, estuvo disponible para los SGBD y [MySQL 5, PostgreSQL 9.4, SQL Server 2014, DB2 Express-C10.5]. Para los otros dos [Oracle Express 11g Release 2, Firebird 2.5.4], se tuvo que utilizar la estrategia [SEQUENCE]. Para la portabilidad entre implementaciones JPA, no se debe utilizar la estrategia [AUTO], que deja a discreción de la implementación JPA la elección de la estrategia de generación de la clave primaria. Así, con MySQL 5 y la estrategia [AUTO]:
- Hibernate elige la estrategia [IDENTITY] con el modo [AUTO_INCREMENT] de la clave primaria;
- EclipseLink elige la estrategia [TABLE], que crea una tabla llamada por defecto [SEQUENCE] a la que hay que consultar para obtener las claves primarias.
Al final, la estructura de la base de datos gestionada por estas dos implementaciones JPA no es la misma. Si ha sido generada por Hibernate, no será utilizable por EclipseLink y viceversa.
- línea 28: la anotación [@Column(name="ID"] establece el nombre de la columna de la tabla [PRODUITS] que se asociará al campo [id];
- línea 29: se utiliza el tipo [Long] en lugar de [long] para la clave primaria. De hecho, las claves primarias [null] tienen un significado particular para JPA. Por lo tanto, es preferible utilizar aquí un tipo objeto en lugar de un tipo simple;
- línea 31: la anotación [@Version] indica que el campo [version] está asociado a una columna de control de versiones. La implementación JPA incrementará este número de version cada vez que se modifique la entidad. Este número sirve para impedir la actualización simultánea de la entidad por parte de dos usuarios diferentes: dos usuarios, U1 y U2, leen la entidad E con un número de version igual a V1. U1 modifica E y persiste esta modificación en la base de datos: el n.º de version pasa entonces a V1+1. U2 modifica a su vez E y persiste esta modificación en la base: recibirá una excepción porque tiene un version (V1) diferente al de la base (V1+1);
- línea 36: el tipo de la entidad. Tendremos dos: POJO y PROXY. Por defecto, la instancia generada será un POJO (Plain Old Java Object). En algunos casos, las instancias [Produit] recuperadas de la base de datos serán de tipo [PROXY]. Este será el caso en el que la propiedad [Categorie categorie] de la línea 58 no se haya inicializado con una categoría debido al atributo [fetch = FetchType.LAZY] de la línea 56. En este caso, las implementaciones JPA que se van a probar difieren:
- [Hibernate, OpenJPA]: acceder a la categoría de un producto de tipo [PROXY] provoca una excepción. Hibernate utiliza el término «proxy» para referirse a una instancia JPA obtenida en modo [LAZY]. Por eso he utilizado este término para designar este tipo de entidad;
- [EclipseLink]: acceder a la categoría de un producto de tipo [PROXY] provoca la búsqueda de dicha categoría en la base de datos y no se produce ninguna excepción;
Como quería disponer de una capa de pruebas independiente de la implementación JPA utilizada, deseaba conocer el tipo de cada entidad: POJO o PROXY. Por eso he añadido el campo [entityType] a las entidades JPA;
- línea 35: la anotación [@Transient] indica que la implementación JPA debe ignorar este campo. De hecho, no existe en las tablas de SGBD;
- línea 40: la clase [Produit] lanza una excepción de tipo [ProxyException] que necesita el nombre de la clase;
- línea 38: al igual que antes, se indica que la implementación JPA debe ignorar este campo;
- línea 39: la anotación [@JsonIgnore] indica que el serializador/deserializador jSON de una instancia [Produit] debe ignorar este campo;
- línea 43: la anotación [@Column] asocia el campo [nom] a la columna [NOM] de la tabla [PRODUITS]. Cuando el campo tiene el mismo nombre que la columna asociada (sin distinción entre mayúsculas y minúsculas), se puede omitir la anotación [@Column]. Este sería el caso aquí. Los atributos [unique = true, length = 30, nullable = false] solo se utilizan cuando la implementación JPA debe generar la tabla [CATEGORIES] a partir de la entidad [Produit]. Se traducirán mediante los atributos SQL y [UNIQUE, VARCHAR(30), NOT NULL], lo que hace que la columna [NOM] tenga como máximo 30 caracteres, sea única en la tabla y no pueda tener el valor NULL;
- líneas 46-47: el campo [idCategorie] vinculado a la columna [CATEGORIE_ID]. Volveremos sobre sus atributos más adelante;
- líneas 49-50: el campo [prix] está asociado a la columna [PRIX];
- líneas 52-53: el campo [description] está asociado a la columna [DESCRIPTION];
- líneas 56-58: la categoría del producto;
- línea 56: la anotación [@ManyToOne] indica que la columna de la anotación de la línea 57, [@JoinColumn(name = "CATEGORIE_ID")], es una clave externa de la tabla [PRODUITS] de laentidad [Produit] en la tabla [CATEGORIES] asociada a la entidad de la línea 58. Esta anotación debe anotar una entidad JPA. Por lo tanto, la clase de la línea 58 debe ser una entidad JPA;
- línea 56: la anotación [fetch = FetchType.LAZY] solicita que, cuando se recupere un producto de la tabla [PRODUITS], su categoría (línea 58) no se recupere inmediatamente (carga diferida). Esta se obtiene entonces durante la primera llamada al método [getCategorie]. Para ello, en la ejecución, la capa JPA amplía el método [getCategorie] inicial (que se limita a devolver el campo «categoría») mediante una llamada a SGBD para recuperar la categoría, una técnica denominada «proxying». Las implementaciones JPA difieren en su implementación de esta característica, tal y como hemos mencionado anteriormente. Este atributo no es obligatorio. La implementación JPA utilizada tiene derecho a ignorarlo. Es precisamente porque la propiedad [categorie] puede estar presente o no por lo que hemos introducido el filtro jSON de la línea 23. La columna de unión [CATEGORIE_ID] de la tabla [PRODUITS] se actualiza automáticamente al insertar o actualizar el producto. Recibe el valor de [categorie.getId()], donde [categorie] es el campo de la línea 58. La especificación JPA impone que esta columna de unión no pueda actualizarse por ningún otro medio. Asimismo, impone los atributos [insertable = false, updatable = false] de la línea 46, que hacen que la columna [CATEGORIE_ID] (la columna de unión, por tanto) asociada al campo [idCategorie] no pueda ser modificada por el campo [idCategorie]. Solo será posible la transferencia de la columna [CATEGORIE_ID] al campo [idCategorie];
- líneas 91-104: la igualdad entre las entidades [Produit] se define como la igualdad entre sus claves primarias [id];
- líneas 108-115: para que nuestra capa de pruebas sea portátil, gestionaremos de manera uniforme las entidades [PROXY] de las tres implementaciones JPA [Hibernate, EclipseLink, OpenJpa]. Para un tipo [Produit] del tipo [PROXY], prohibiremos cambiar el valor del campo [categorie]. La clase [ProxyException] es la siguiente:
![]() |
package generic.jpa.infrastructure;
import generic.jdbc.infrastructure.UncheckedException;
public class ProxyException extends UncheckedException {
private static final long serialVersionUID = 7278276670314994574L;
public ProxyException() {
}
public ProxyException(int code, Throwable e, String simpleClassName) {
super(code, e, simpleClassName);
}
}
Para concluir el análisis de esta entidad, cabe señalar que las anotaciones y sus atributos se utilizan en dos casos bien diferenciados:
- para crear las tablas de la base de datos;
- para utilizarlas. En este caso, la implementación JPA espera encontrar las tablas tal y como las habría generado ella misma. Por lo tanto, no se puede asociar a la entidad [Produit] anterior cualquier tabla [PRODUITS]. Esta debe tener al menos (puede tener otras) las características de la tabla [PRODUITS] que habría generado. Cuando se trabaja con JPA, lo ideal es partir de una base vacía en la que se deje que JPA genere las tablas. Abordaremos esta generación un poco más adelante. El script SQL proporcionado para SGBD MySQL se ha generado a partir de las tablas generadas por JPA.
Todos los atributos de la entidad [Produit] se utilizan para la generación de la tabla [PRODUITS]. Una vez hecho esto, los atributos de generación como [unique = true, length = 30, nullable = false] ya no se utilizan al procesar las tablas.
6.3.3.3. La entidad JPA [Categorie]
La clase [Categorie] es una entidad JPA asociada a una línea de la tabla [CATEGORIES]:
![]() |
Su código es el siguiente:
package generic.jpa.entities.dbproduitscategories;
import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = ConfigJdbc.TAB_CATEGORIES)
@JsonFilter("jsonFilterCategorie")
public class Categorie implements AbstractCoreEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = ConfigJdbc.TAB_JPA_ID)
protected Long id;
@Version
@Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
protected Long version;
@Transient
protected EntityType entityType = EntityType.POJO;
@Transient
@JsonIgnore
protected String simpleClassName = getClass().getSimpleName();
// propiedades
@Column(name = ConfigJdbc.TAB_CATEGORIES_NOM, unique = true, length = 30, nullable = false)
private String nom;
// productos asociados
@OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
private List<Produit> produits;
// constructores
public Categorie() {
}
public Categorie(Long id, Long version, String nom, List<Produit> produits) {
this.id = id;
this.version = version;
this.nom = nom;
this.produits = produits;
}
// firma
public String toString() {
return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
}
// métodos
public void addProduit(Produit produit) {
// tipo de entidad
if (entityType == EntityType.PROXY) {
throw new ProxyException(1004, new RuntimeException(
"On ne peut ajouter de produits à une catégorie de type [PROXY]"), simpleClassName);
}
// Añadir un producto
if (produits == null) {
produits = new ArrayList<Produit>();
}
if (produit != null) {
// se añade el producto
produits.add(produit);
// se establece su categoría
produit.setCategorie(this);
produit.setIdCategorie(this.id);
}
}
// ------------------------------------------------------------
// redefinición de [equals] y [hashcode]
@Override
public int hashCode() {
Long id = getId();
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractCoreEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractCoreEntity other = (AbstractCoreEntity) entity;
Long id = getId();
Long otherId = other.getId();
return id != null && otherId != null && id.equals(otherId);
}
// getters y setters
...
}
- línea 24: la clase es una entidad JPA;
- línea 25: asociada a la tabla [CATEGORIES];
- línea 26: la representación jSON de la entidad [Categorie] está controlada por el filtro denominado [jsonFilterCategorie]. Este deberá configurarse antes de cualquier solicitud de representación jSON de la entidad. El filtro [jsonFilterCategorie] se utilizará para excluir o no de la representación jSON de la entidad [Categorie] el campo [produits] de la línea 40;
- líneas 29-32: el campo [id] está asociado a la clave primaria [ID] de la tabla [CATEGORIES]. El modo de generación elegido es el modo [IDENTITY], por lo que el modo [AUTO_INCREMENT] para MySQL;
- líneas 34-36: el campo [version] está vinculado a la columna de control de versiones [VERSIONING] de la tabla [CATEGORIES];
- líneas 38-39: el tipo de la entidad [Categorie];
- líneas 41-43: el nombre simple de la clase [Categorie];
- líneas 46-47: el campo [nom] está vinculado a la columna [NOM] de la tabla [CATEGORIES]. Se le asignan los atributos JPA y [unique = true, length = 30, nullable=false] para que, al generar la tabla [CATEGORIES], la columna [NOM] tenga los atributos SQL [UNIQUE, VARCHAR(30), NOT NULL];
- líneas 50-51: los productos que pertenecen a la categoría;
- línea 50: la anotación [@OneToMany] es la relación inversa de la relación [@ManyToOne] que hemos encontrado en la entidad [Produit]. El atributo [mappedBy = "categorie"] indica el campo de la entidad [Produit] anotado por la relación inversa [@ManyToOne]. El atributo [cascade = { CascadeType.ALL }] solicita que las operaciones (persist, merge, remove) realizadas sobre una @Entity [Categorie] se propaguen en cascada a las [produits] de la línea 51. Se pueden indicar cascadas parciales con las constantes [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE];
- línea 50: el atributo [fetch = FetchType.LAZY] solicita que, cuando se recuperan los datos de una categoría de la tabla [CATEGORIES], sus productos no se recuperen inmediatamente. Estos se recuperan entonces durante la primera llamada al método [getProduits]. Para ello, en la ejecución, la capa JPA amplía el método [getProduits] inicial (que se limita a devolver el campo productos) mediante una llamada a SGBD para recuperar los productos de la categoría. Este atributo es obligatorio. La implementación JPA no puede ignorarlo. Dado que la propiedad [produits] puede estar inicializada o no, hemos introducido el filtro jSON de la línea 26, que nos permitirá indicar si queremos o no esta propiedad y el tipo de la entidad de la línea 39;
- líneas 71-88: el método [addProduit] permite añadir un producto a la categoría;
- líneas 73-76: para uniformizar la gestión de los proxies entre diferentes implementaciones de JPA, se ha decidido que no se pueden añadir productos a una entidad [Categorie] de tipo PROXY;
- líneas 92-112: dos entidades [Categorie] se considerarán iguales si tienen la misma clave primaria [id];
6.3.4. El archivo [persistence.xml]
![]() |
Las aplicaciones JPA deben definir ciertas propiedades del proveedor JPA utilizado, así como las entidades JPA que se van a utilizar, en un archivo [META-INF/persistence.xml] presente en la ruta de clases (Classpath) de la aplicación. En el ejemplo anterior, se ha colocado en la carpeta [src/main/resources], que forma parte efectivamente de la ruta de clases de un proyecto Eclipse. Cuando se utiliza JPA junto con Spring, cierta información que debería estar en el archivo [persistence.xml] se coloca en otra parte, en clases de configuración de Spring. En una aplicación Spring JPA, es Spring quien controla JPA. Con Spring JPA Hibernate, el archivo [persistence.xml] puede reducirse a su forma más simple:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.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_1_0.xsd">
<persistence-unit name="dummy-persistence-unit" transaction-type="RESOURCE_LOCAL" />
</persistence>
- líneas 1-5: un archivo [persistence.xml] debe tener una etiqueta raíz <persistence>. Los atributos de la etiqueta de la línea 2 no se utilizarán en esta aplicación;
- un archivo de persistencia puede definir una o varias unidades de persistencia con la etiqueta <persistence-unit> (línea 4). Una unidad de persistencia gestiona el acceso a una base de datos concreta. Si la aplicación gestiona dos bases de datos simultáneamente, tendrá dos unidades de persistencia;
- línea 4: una unidad de persistencia tiene un nombre [attribut name], admite un tipo de transacción [attribut transaction-type], tiene propiedades y define las entidades asociadas a las tablas de la base de datos gestionada por la unidad de persistencia. En este caso, dado que los accesos a la base de datos serán gestionados por [Spring JPA Hibernate], estas dos últimas informaciones pueden colocarse en otro lugar. Hay dos tipos de transacción:
- [RESOURCE_LOCAL]: las transacciones son gestionadas por la propia aplicación. Este es el caso aquí, donde será Spring quien gestione las transacciones;
- [JTA] (Java Transaction API): es el contenedor EJB (Enterprise Java Bean) el que ejecuta la aplicación y gestiona automáticamente las transacciones en función de las anotaciones Java encontradas en el código. Aquí no nos encontramos en esta configuración;
Veremos más adelante que el contenido de este archivo [persistence.xml] depende de la implementación JPA utilizada.
6.4. El proyecto [spring-jpa-generic]
Recordemos lo que queremos hacer. Queremos implementar la siguiente arquitectura:
![]() |
en la que la capa [DAO] implementaría la interfaz [IDao<Produit>, IDao<Categorie>] estudiada en el capítulo 4. Se trata de comparar dos implementaciones de esta interfaz:
- una construida con Spring JDBC;
- la otra construida con Spring JPA;
En la arquitectura anterior:
- la capa [JDBC] está implementada por el proyecto [mysql-config-jdbc] estudiado en el apartado 3.3;
- La capa [JPA] se implementa mediante el proyecto [mysql-config-jpa-hibernate], analizado en el apartado 6.3;
El proyecto [spring-jpa-generic] se encarga de la implementación de las capas [DAO] y [Spring Data].
![]() |
6.4.1. Configuración de Maven
El proyecto [spring-jpa-generic] es un proyecto Maven configurado por el siguiente archivo [pom.xml]:
<?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>dvp.spring.database</groupId>
<artifactId>spring-jpa-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-jpa-generic</name>
<description>démo spring data avec tables de catégories et de produits</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- configuración JPA del SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jpa</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- líneas 22-26: el proyecto solo tiene una única dependencia, la del proyecto que configura la capa [JPA] de la aplicación y que acabamos de estudiar. Se trata de una aplicación genérica:
- se cambia de SGBD modificando el proyecto de configuración de la capa [JDBC];
- se cambia la implementación JPA modificando el proyecto de configuración de la capa [JPA];
Al final, las dependencias son las siguientes:
![]() |
6.4.2. Configuración de Spring
![]() |
La clase [AppConfig] configura el proyecto Spring:
package spring.data.config;
import generic.jpa.config.ConfigJpa;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories(basePackages = { "spring.data.repositories" })
@Configuration
@ComponentScan(basePackages = { "spring.data.dao" })
@Import({ ConfigJpa.class })
public class AppConfig {
}
- línea 11: la clase es una clase de configuración de Spring;
- línea 10: la anotación [@EnableJpaRepositories] sirve para designar los paquetes que contienen las interfaces [CrudRepository] de Spring Data. Esto las convierte en componentes Spring que pueden inyectarse en otros componentes Spring;
- línea 12: la anotación [@ComponentScan] indica que se debe explorar el paquete [spring.data.dao] en busca de componentes Spring. Se encontrarán los componentes [DaoCategorie] y [DaoProduit];
- línea 13: se importan los beans de la clase de configuración [ConfigJpa]. En ella se encontrará el bean de la implementación JPA utilizada (Hibernate, Eclipselink, OpenJpa), la fuente de datos que se va a utilizar, el EntityManager que gestionará las operaciones JPA, el gestor de transacciones;
6.4.3. La capa [Spring Data]
![]() |
![]() |
6.4.3.1. La interfaz [CategoriesRepository]
La interfaz [CategoriesRepository] gestiona los accesos a la tabla [CATEGORIES]:
package spring.data.repositories;
import generic.jpa.entities.dbproduitscategories.Categorie;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
public interface CategoriesRepository extends CrudRepository<Categorie, Long> {
// categorie avec ses produits
@Query("select c from Categorie c left join fetch c.produits where c.id=?1")
public Categorie getLongCategorieById(Long id);
@Query("select c from Categorie c left join fetch c.produits where c.nom=?1")
public Categorie getLongCategorieByName(String nom);
@Query("select c from Categorie c where c.nom in ?1")
public List<Categorie> getShortCategoriesByName(Iterable<String> names);
@Query("select c from Categorie c where c.id in ?1")
public List<Categorie> getShortCategoriesById(Iterable<Long> ids);
@Query("select distinct c from Categorie c left join fetch c.produits where c.id in ?1")
public List<Categorie> getLongCategoriesById(List<Long> names);
@Query("select distinct c from Categorie c left join fetch c.produits where c.nom in ?1")
public List<Categorie> getLongCategoriesByName(List<String> names);
@Query("select c from Categorie c")
public List<Categorie> getAllShortCategories();
@Query("select distinct c from Categorie c left join fetch c.produits")
public List<Categorie> getAllLongCategories();
}
- línea 10: la interfaz [CrudRepository] se ha utilizado y explicado en el apartado 5.1.3. Recordemos que:
- el primer tipo de parámetro de la interfaz es la entidad JPA gestionada para los accesos CRUD (findOne, findAll, guardar, borrar, deleteAll),
- el segundo tipo de parámetro de la interfaz es el de la clave primaria de la entidad JPA, en este caso un entero [Long];
Los métodos de la interfaz se implementan mediante consultas JPQL (Java Persistence Query Language). Esta consulta se dirige a las entidades JPA. En una consulta de este tipo:
- las tablas se sustituyen por sus entidades JPA asociadas;
- las columnas se sustituyen por campos de las entidades JPA utilizadas en la consulta;
Tomemos el ejemplo de las líneas 31-32: el método de la línea 32 devuelve todas las categorías de la base de datos en su forma abreviada version. Se implementa mediante la consulta JPQL (Java Persistence Query Language) de la línea 31, que se parece mucho a su homóloga SQL. Para profundizar en JPQL, se puede consultar [ref2] (véase el apartado 1.2).
Los métodos de la interfaz [CategoriesRepository] son los siguientes:
- líneas 13-14: el método [getLongCategorieById] devuelve la version de una categoría referenciada por su clave primaria [id], es decir, la categoría con sus productos. Recordemos que en la entidad [Categorie], el campo [produits] tenía el atributo [fetch = FetchType.LAZY] (carga diferida). En la consulta JPQL, forzamos la carga de los productos con la clave [fetch]. El parámetro ?1 de la consulta se sustituirá en la ejecución por el valor del primer parámetro del método de la línea 12, es decir, por el parámetro [Long id];
- líneas 16-17: el método [getLongCategorieByName] devuelve la versión larga de una categoría referenciada por su nombre version;
- líneas 19-20: el método [getShortCategoriesByName] devuelve las versiones cortas de las categorías referenciadas por sus nombres. El campo [produits] de estas categorías no es nulo. Contiene la referencia de un proxy (una clase creada por la implementación JPA) cuya función es devolver los productos de la categoría cuando se le llama. Su llamada fuera del contexto de persistencia JPA provoca una excepción (Hibernate y OpenJpa, pero no EclipseLink). Por este motivo, no utilizaremos el campo [produits] de la version corta de una categoría;
- líneas 22-23: el método [getShortCategoriesById] devuelve las versiones cortas de las categorías referenciadas por sus claves primarias [id];
- líneas 25-26: el método [getLongCategoriesById] devuelve las versiones largas de las categorías referenciadas por sus claves primarias [id];
- líneas [28-29]: el método [getLongCategoriesByName] devuelve las versiones largas de las categorías referenciadas por sus nombres;
- líneas 31-32: el método [getAllShortCategories] devuelve las versiones cortas de todas las categorías;
- líneas 34-35: el método [getAllLongCategories] devuelve las versiones largas de todas las categorías;
Nota: no todas las implementaciones JPA aceptan la misma sintaxis JPQL. Así, la siguiente sintaxis es aceptada por Hibernate y EclipseLink, pero no por OpenJpa:
@Query("select c from Categorie c left join fetch c.produits p where c.nom=?1")
OpenJpa no admite el alias [p] mencionado anteriormente.
6.4.3.2. La interfaz [ProduitsRepository]
La interfaz [ProduitsRepository] gestiona los accesos a la tabla [PRODUITS]:
package spring.data.repositories;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Transactional;
@Transactional()
public interface ProduitsRepository extends CrudRepository<Produit, Long> {
// un produit avec sa catégorie
@Query("select p from Produit p left join fetch p.categorie where p.id=?1")
public Produit getLongProduitById(Long id);
@Query("select p from Produit p left join fetch p.categorie where p.nom=?1")
public Produit getLongProduitByName(String nom);
@Query("select p from Produit p where p.id in ?1")
public List<Produit> getShortProduitsById(List<Long> ids);
@Query("select p from Produit p where p.nom in ?1")
public List<Produit> getShortProduitsByName(List<String> names);
@Query("select distinct p from Produit p left join fetch p.categorie where p.id in ?1")
public List<Produit> getLongProduitsById(List<Long> ids);
@Query("select distinct p from Produit p left join fetch p.categorie where p.nom in ?1")
public List<Produit> getLongProduitsByName(List<String> names);
@Query("select distinct p from Produit p left join fetch p.categorie")
public List<Produit> getAllLongProduits();
@Query("select p from Produit p")
public List<Produit> getAllShortProduits();
}
- Líneas [15-16]: el método [getLongProduitById] devuelve el registro version con un producto identificado por su clave primaria [id], es decir, con su categoría. Recordemos que en la entidad [Produit], el campo [categorie] tenía el atributo [fetch = FetchType.LAZY] (carga diferida). En la consulta JPQL, forzamos la carga de la categoría con la clave [fetch];
- líneas 18-19: el método [getLongProduitByName] devuelve la version larga de un producto identificado por su nombre;
- líneas 21-22: el método [getShortProduitsById] devuelve la version corta de los productos identificados por su clave primaria [id]. En esta version corta, el campo [categorie] no tiene el valor null. Contiene la referencia de un proxy generado por la implementación JPA, que, si se invoca, irá a buscar la categoría del producto. Esta llamada solo puede realizarse en el contexto de persistencia JPA. Hacerlo en otro lugar provoca una excepción (Hibernate y OpenJpa, pero no EclipseLink). Por lo tanto, en la capa [DAO] o en cualquier otro lugar, no utilizaremos el campo [categorie] de un producto en su version corta. En la version corta del producto, se inicializa el campo [idCategorie]. Su valor es la clave primaria de la categoría a la que pertenece el producto. Esto permite posteriormente solicitar esta categoría a la capa [DAO] mediante el método [DaoCategorie. getShortCategoriesById(idCategorie)];
- líneas 24-25: el método [getShortProduitsByName] devuelve el campo corto version de los productos identificados por sus nombres;
- líneas 27-28: el método [getLongProduitsById] devuelve la version larga de los productos identificados por sus claves primarias;
- líneas 30-31: el método [getLongProduitsByName] devuelve la versión larga version de los productos identificados por sus nombres;
- líneas 33-34: el método [getAllLongProduits] devuelve la version larga de todos los productos;
- líneas 36-37: el método [getAllShortProduits] devuelve la version corta de todos los productos;
Estas interfaces serán implementadas por clases generadas por la implementación JPA en el momento de la ejecución del proyecto. A estas clases se las denomina clases [proxy]. Por defecto, los métodos de la interfaz [CrudRepository] se ejecutan en una transacción. El hecho de que las interfaces [ProduitsRepository, CategoriesRepository] extiendan la clase [CrudRepository] las convierte en componentes Spring. Como tales, pueden inyectarse en otros componentes Spring.
6.4.4. La capa [DAO]
![]() |
![]() |
6.4.4.1. La interfaz [IDao<T>]
La interfaz [IDao<T>] es la que ya se ha estudiado en la implementación de la capa [DAO] realizada con Spring JDBC (véase el apartado 4.7);
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity;
import java.util.List;
public interface IDao<T extends AbstractCoreEntity> {
// lista de todas las entidades T
public List<T> getAllShortEntities();
public List<T> getAllLongEntities();
// de entidades específicas - version breve
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
public List<T> getShortEntitiesByName(Iterable<String> names);
public List<T> getShortEntitiesByName(String... names);
// de entidades específicas - version larga
public List<T> getLongEntitiesById(Iterable<Long> ids);
public List<T> getLongEntitiesById(Long... ids);
public List<T> getLongEntitiesByName(Iterable<String> names);
public List<T> getLongEntitiesByName(String... names);
// actualización de varias entidades
public List<T> saveEntities(Iterable<T> entities);
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
// eliminación de todas las entidades
public void deleteAllEntities();
// eliminación de varias entidades
public void deleteEntitiesById(Iterable<Long> ids);
public void deleteEntitiesById(Long... ids);
public void deleteEntitiesByName(Iterable<String> names);
public void deleteEntitiesByName(String... names);
public void deleteEntitiesByEntity(Iterable<T> entities);
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}
6.4.4.2. La clase abstracta [AbstractDao]
![]() |
La clase abstracta [AbstractDao] es la clase padre de las clases que implementan la capa [DAO]:
- la clase [DaoProduit], que implementa la interfaz [IDao<Produit>] y gestiona los accesos a la tabla [PRODUITS];
- la clase [DaoCategorie], que implementa la interfaz [IDao<Categorie>] y gestiona los accesos a la tabla [CATEGORIES];
Su código es el descrito en el apartado 4.8, con la siguiente diferencia: ningún método tiene el atributo [@Transactional], que hace que el método se ejecute en una transacción. Aquí se aprovecha el hecho de que las interfaces [CrudRepository] de Spring Data se ejecutan por defecto en una transacción.
6.4.4.3. La clase [DaoCategorie]
![]() |
La clase [DaoCategorie] implementa la interfaz [IDao<Categorie>] de la siguiente manera:
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;
@Component
public class DaoCategorie extends AbstractDao<Categorie> {
@Autowired
private ProduitsRepository produitsRepository;
@Autowired
private CategoriesRepository categoriesRepository;
@Override
public List<Categorie> getAllShortEntities() {
try {
return setShortCategoriesType(categoriesRepository.getAllShortCategories());
} catch (Exception e) {
throw new DaoException(211, e, simpleClassName);
}
}
private List<Categorie> setShortCategoriesType(List<Categorie> categories) {
for (Categorie categorie : categories) {
categorie.setEntityType(EntityType.PROXY);
}
return categories;
}
@Override
public List<Categorie> getAllLongEntities() {
try {
return categoriesRepository.getAllLongCategories();
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
@Override
public void deleteAllEntities() {
try {
categoriesRepository.deleteAll();
} catch (Exception e) {
throw new DaoException(208, e, simpleClassName);
}
}
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
try {
return setShortCategoriesType(categoriesRepository.getShortCategoriesById(ids));
} catch (Exception e) {
throw new DaoException(203, e, simpleClassName);
}
}
@Override
protected List<Categorie> getShortEntitiesByName(List<String> names) {
try {
return setShortCategoriesType(categoriesRepository.getShortCategoriesByName(names));
} catch (Exception e) {
throw new DaoException(204, e, simpleClassName);
}
}
@Override
protected List<Categorie> getLongEntitiesById(List<Long> ids) {
try {
return categoriesRepository.getLongCategoriesById(ids);
} catch (Exception e) {
throw new DaoException(205, e, simpleClassName);
}
}
@Override
protected List<Categorie> getLongEntitiesByName(List<String> names) {
try {
return categoriesRepository.getLongCategoriesByName(names);
} catch (Exception e) {
throw new DaoException(206, e, simpleClassName);
}
}
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
...
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
try {
categoriesRepository.delete(getShortEntitiesById(ids));
} catch (Exception e) {
throw new DaoException(209, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesByName(List<String> names) {
try {
categoriesRepository.delete(getShortEntitiesByName(names));
} catch (Exception e) {
throw new DaoException(212, e, simpleClassName);
}
}
}
- línea 17: la anotación [@Component] convierte la clase [DaoCategorie] en un componente Spring;
- línea 18: la clase [DaoCategorie] extiende la clase [AbstractDao<Categorie>], lo que hace que implemente la interfaz [IDao<Categorie>];
- líneas 20-24: inyección de referencias en las dos interfaces [CrudRepository] de [Spring Data]. Esta inyección tendrá lugar durante la instanciación de los objetos Spring, generalmente al inicio de la ejecución del proyecto Spring;
- todos los métodos de la clase delegan el trabajo a los métodos del mismo nombre de las interfaces [CrudRepository];
- todos los métodos que devuelven las entidades a su version corto lo indican estableciendo el tipo de la entidad en [EntityType.PROXY] (líneas 29, 63, 72);
El método [saveEntities] merece una explicación:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// se anotan los productos que se van a insertar
List<Produit> insertedProduits = new ArrayList<Produit>();
for (Categorie categorie : categories) {
EntityType categorieType = categorie.getEntityType();
List<Produit> produits = null;
if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
for (Produit produit : produits) {
if (produit.getId() == null) {
insertedProduits.add(produit);
}
// se aprovecha para restablecer (si es necesario) la relación producto --> categoría
produit.setCategorie(categorie);
}
}
}
// se guardan las categorías y los productos
try {
categoriesRepository.save(categories);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// se actualiza el campo [idCategorie] de los productos insertados
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// resultado
return categories;
}
- línea 2: las categorías pasadas como parámetros son tanto categorías que se deben insertar ([id==null]) como categorías que se deben modificar ([id!=null]);
- línea 20: se guardan las categorías con el método [categoriesRepository.save(entities)]. En las pruebas, se observa que el campo [idCategorie] de los productos guardados (id==null) no está rellenado. Para solucionar este problema, en las líneas 4-17 se anotan los productos que se van a insertar y, una vez guardados, se rellena su campo [idCategorie] (líneas 25-27);
- líneas 5-17: se recorre la lista de categorías;
- líneas 8-16: se recorre la lista de productos de cada categoría. Aquí surge una dificultad. El método [saveEntities] se utiliza tanto para persistir como para modificar una categoría. En este último caso, la categoría puede haberse obtenido en su versión corta version, con la referencia de un método proxy en el campo [produits]. Utilizarlo con Hibernate provoca entonces una excepción, ya que la categoría utilizada ya no se encuentra en el contexto de persistencia JPA, que se cerró al finalizar la transacción del método que devolvió las versiones cortas de las categorías. A continuación, se utiliza el campo [EntityType] de la entidad [Categorie], línea 8, para saber si se puede acceder o no a la lista de productos de la categoría;
- línea 14: se vincula el producto a su categoría. Normalmente, esto ya debería ser así. Pero no sabemos cómo se ha creado este producto ni si se ha vinculado a su categoría. Por lo tanto, para evitar cualquier problema (para gestionar la entidad [Produit], JPA necesita que esta haga referencia a la entidad [Categorie] a la que está vinculada), realizamos esta vinculación nosotros mismos.
Al comparar este código con el de la clase [DaoProduit] de la implementación Spring JDBC (véase el apartado 4.9) , se observa que la biblioteca Spring Data JPA facilita enormemente la escritura de la capa [DAO].
6.4.4.4. La clase [DaoProduit]
![]() |
La clase [DaoProduit] implementa la interfaz [IDao<Produit>] de la siguiente manera:
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;
import com.google.common.collect.Lists;
@Component
public class DaoProduit extends AbstractDao<Produit> {
@Autowired
private ProduitsRepository produitsRepository;
@Autowired
private CategoriesRepository categoriesRepository;
@Override
public List<Produit> getAllShortEntities() {
try {
return setShortProduitsType(produitsRepository.getAllShortProduits());
} catch (Exception e) {
throw new DaoException(102, e, simpleClassName);
}
}
private List<Produit> setShortProduitsType(List<Produit> produits) {
for (Produit produit : produits) {
produit.setEntityType(EntityType.PROXY);
}
return produits;
}
@Override
public List<Produit> getAllLongEntities() {
try {
return produitsRepository.getAllLongProduits();
} catch (Exception e) {
throw new DaoException(117, e, simpleClassName);
}
}
@Override
public void deleteAllEntities() {
try {
produitsRepository.deleteAll();
} catch (Exception e) {
throw new DaoException(112, e, simpleClassName);
}
}
@Override
protected List<Produit> getShortEntitiesById(List<Long> ids) {
try {
return setShortProduitsType(produitsRepository.getShortProduitsById(ids));
} catch (Exception e) {
throw new DaoException(103, e, simpleClassName);
}
}
@Override
protected List<Produit> getShortEntitiesByName(List<String> names) {
try {
return setShortProduitsType(produitsRepository.getShortProduitsByName(names));
} catch (Exception e) {
throw new DaoException(104, e, simpleClassName);
}
}
@Override
protected List<Produit> getLongEntitiesById(List<Long> ids) {
try {
return linkLongProduitsToCategories(produitsRepository.getLongProduitsById(ids));
} catch (Exception e) {
throw new DaoException(105, e, simpleClassName);
}
}
@Override
protected List<Produit> getLongEntitiesByName(List<String> names) {
try {
return linkLongProduitsToCategories(produitsRepository.getLongProduitsByName(names));
} catch (Exception e) {
throw new DaoException(106, e, simpleClassName);
}
}
private List<Produit> linkLongProduitsToCategories(List<Produit> produits) {
for (Produit produit : produits) {
Categorie categorie = produit.getCategorie();
if (categorie != null) {
produit.setCategorie(categorie);
produit.setIdCategorie(categorie.getId());
}
}
return produits;
}
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
// se restablece (si es necesario) el vínculo entre un producto y su categoría
for (Produit produit : entities) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// se mantienen los productos
try {
return Lists.newArrayList(produitsRepository.save(entities));
} catch (Exception e) {
throw new DaoException(111, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
try {
produitsRepository.delete(getShortEntitiesById(ids));
} catch (Exception e) {
throw new DaoException(113, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesByName(List<String> names) {
try {
produitsRepository.delete(getShortEntitiesByName(names));
} catch (Exception e) {
throw new DaoException(118, e, simpleClassName);
}
}
}
El código es similar al de la clase [DaoCategorie]:
- en las versiones largas de las categorías, las pruebas revelan que el campo [idCategorie] de los productos no está rellenado. El método [linkLongProduitsToCategories] de las líneas 96-105 soluciona este problema;
- el método [saveEntities] de las líneas 108-121 inserta nuevos productos o modifica los existentes. La capa JPA exige que cada entidad [Produit] esté vinculada a una entidad [Categorie]. Como no sabemos si el usuario lo ha hecho, lo hacemos nosotros mismos en las líneas 110-113. Basta con vincular el [Produit] a una entidad [Categorie] cuya clave primaria sea igual al campo [idCategorie] del [Produit]. En las pruebas, se observa que se produce un error si se establece en null el campo version de la categoría. Por lo tanto, aquí se le asigna el valor 0, pero se puede establecer el valor que se desee. Aparte de la clave primaria, ningún campo de la entidad [Categorie] es necesario en la capa JPA para insertar o modificar una entidad [Produit];
6.4.5. La capa de pruebas
![]() |
![]() |
Las pruebas anteriores son idénticas a las de la implementación Spring JDBC. Consulte las siguientes páginas si es necesario:
- [JUnitTestCheckArguments]: apartado 4.11.1;
- [JUnitTestDao]: apartado 4.11.2;
- [JUnitTestPushTheLimits]: apartado 4.11.3;
Utilizamos las siguientes configuraciones de ejecución:
![]() | ![]() |
![]() | ![]() |
Los resultados obtenidos en las diferentes pruebas son los siguientes:
![]() | ![]() |
![]() |
En [1], la prueba [JUnitTestPushTheLimits] con la implementación Spring Data JPA Hibernate y en [2], con la implementación Spring JDBC. Se observa que esta última ofrece un mejor rendimiento. Llegamos, por tanto, a una primera conclusión: es mucho más fácil desarrollar una capa [DAO] con Spring Data JPA, pero es menos eficiente que una implementación Spring JDBC.
La prueba [JUnitTestProxies] es una prueba ficticia JUnit. Su objetivo es mostrar el comportamiento de cada implementación JPA frente a los proxies, es decir, las versiones abreviadas de las entidades:
package spring.data.tests;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.data.config.AppConfig;
import spring.data.dao.IDao;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestProxies {
// capa [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
@Before
public void clean() {
// se limpia la base de datos antes de cada prueba
log("Vidage de la base de données", 1);
// se vacía la tabla [CATEGORIES] y, en cadena, la tabla [PRODUITS]
daoCategorie.deleteAllEntities();
}
@Test
public void doNothing() {
System.out.println("doNothing");
}
private List<Categorie> fill(int nbCategories, int nbProduits) {
// se rellenan las tablas
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < nbCategories; i++) {
Categorie categorie = new Categorie(null, null, String.format("categorie[%d]", i), null);
categorie.setProduits(new ArrayList<Produit>());
for (int j = 0; j < nbProduits; j++) {
Produit produit = new Produit(null, null, String.format("produit[%d,%d]", i, j), null,
100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
categorie.addProduit(produit);
}
categories.add(categorie);
}
// se añade la categoría; de forma cascada, los productos también se
// insertados
daoCategorie.saveEntities(categories);
// resultado
return categories;
}
@Test
public void getShortCategoriesByName1() {
// relleno
fill(1, 1);
// prueba
log("getShortCategoriesByName1", 1);
Categorie categorie = daoCategorie.getShortEntitiesByName(Lists.newArrayList("categorie[0]")).get(0);
System.out.println(String.format("Catégorie de type : %s", categorie.getEntityType()));
System.out.println("Catégorie :");
try {
System.out.println(categorie.getProduits().size());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getShortProduitsByName1() {
// relleno
fill(1, 1);
// prueba
log("getShortProduitsByName1", 1);
Produit produit = daoProduit.getShortEntitiesByName(Lists.newArrayList("produit[0,0]")).get(0);
System.out.println(String.format("Produit de type : %s", produit.getEntityType()));
System.out.println("Nom de la catégorie du produit :");
try {
System.out.println(produit.getCategorie().getNom());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getLongCategoriesByName1() {
// relleno
fill(1, 1);
// prueba
log("getLongCategoriesByName1", 1);
Categorie categorie = daoCategorie.getLongEntitiesByName(Lists.newArrayList("categorie[0]")).get(0);
System.out.println(String.format("Catégorie de type : %s", categorie.getEntityType()));
System.out.println("Catégorie :");
try {
System.out.println(categorie.getProduits().size());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getLongProduitsByName1() {
// relleno
fill(1, 1);
// prueba
log("getLongProduitsByName1", 1);
Produit produit = daoProduit.getLongEntitiesByName(Lists.newArrayList("produit[0,0]")).get(0);
System.out.println(String.format("Produit de type : %s", produit.getEntityType()));
System.out.println("Nom de la catégorie du produit :");
try {
System.out.println(produit.getCategorie().getNom());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
private void log(String message, int mode) {
// muestra mensaje
String toPrint = null;
switch (mode) {
case 1:
toPrint = String.format("%s --------------------------------", message);
break;
case 2:
toPrint = String.format("-- %s", message);
break;
}
System.out.println(toPrint);
}
}
Los resultados obtenidos son los siguientes:
Vidage de la base de données --------------------------------
doNothing
Vidage de la base de données --------------------------------
getShortCategoriesByName1 --------------------------------
Catégorie de type : PROXY
Catégorie :
Exception : org.hibernate.LazyInitializationException, Message : failed to lazily initialize a collection of role: generic.jpa.entities.dbproduitscategories.Categorie.produits, could not initialize proxy - no Session
Vidage de la base de données --------------------------------
getLongCategoriesByName1 --------------------------------
Catégorie de type : POJO
Catégorie :
1
Vidage de la base de données --------------------------------
getShortProduitsByName1 --------------------------------
Produit de type : PROXY
Nom de la catégorie du produit :
Exception : org.hibernate.LazyInitializationException, Message : could not initialize proxy - no Session
Vidage de la base de données --------------------------------
getLongProduitsByName1 --------------------------------
Produit de type : POJO
Nom de la catégorie du produit :
categorie[0]
Aquí vemos que, al acceder al campo [Categorie.produits] de una categoría de tipo PROXY y al campo [Produit.categorie] de un producto de tipo PROXY, se produce una excepción de tipo [org.hibernate.LazyInitializationException] en ambos casos (líneas 7 y 17).



































