Skip to content

11. [Cours]: Gestión de bases de datos relacionales con Spring Data

Palabras clave: arquitectura multicapa, Spring, inyección de dependencias, API JPA (Java Persistence API), Spring Data.

Vamos a implementar la capa [DAO] de TD con [Spring Data], una rama del ecosistema Spring. [Spring Data] se basa en una capa JPA (Java Persistence API) que permite a la capa [DAO] manipular objetos en lugar de órdenes SQL. En definitiva, la capa [DAO] ignora que se comunica con una base de datos. Solo conoce la interfaz de la capa [Spring Data].

En primer lugar, vamos a descubrir [Spring Data] a través de dos ejemplos.

11.1. Support

  • En [1], la carpeta [support / chap-11] contiene tres proyectos de Eclipse;
  • en [2], el script SQL que permite crear la base de datos de ejemplo de este capítulo;

11.2. Ejemplo 1

En la página web de Spring hay numerosos tutoriales para iniciarse en Spring [http://spring.io/guides]. Vamos a utilizar uno de ellos para introducir Spring Data. Para ello, utilizaremos Spring Tool Suite (STS).

  • En [1], importamos uno de los tutoriales de [spring.io/guides];
  • en [2], seleccionamos el tutorial [Accessing Data Jpa], que muestra cómo acceder a una base de datos con Spring Data;
  • En [3], se elige un proyecto configurado por Maven;
  • en [4], el tutorial se puede obtener en dos formatos: [initial], que es una versión vacía que se rellena siguiendo el tutorial, o [complete], que es la versión final del tutorial. Elegimos esta última;
  • en [5], podemos elegir visualizar el tutorial en un navegador;
  • en [6], el proyecto final.

11.2.1. La configuración de Maven del proyecto

Las dependencias de Maven del proyecto se configuran en el archivo [pom.xml]:


    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.10.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <!-- utiliza UTF-8 para todo -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • líneas 5-9: definen un proyecto Maven principal. Es este el que define la mayor parte de las dependencias del proyecto. Pueden ser suficientes, en cuyo caso no se añaden más, o no, en cuyo caso se añaden las dependencias que faltan;
  • líneas 12-15: definen una dependencia de [spring-boot-starter-data-jpa]. Este artefacto contiene las clases de Spring Data;
  • líneas 16-19: definen una dependencia de SGBD y H2, que permiten crear y gestionar bases de datos en memoria.

Veamos las clases que aportan estas dependencias:

Son muchas:

  • algunas pertenecen al ecosistema Spring (las que empiezan por «spring»);
  • otras pertenecen al ecosistema Hibernate (hibernate, jboss), de las que aquí utilizamos la implementación JPA;
  • otras son bibliotecas de pruebas (junit, hamcrest);
  • otras son bibliotecas de registros (log4j, logback, slf4j);

Las vamos a conservar todas. Para una aplicación en producción, habría que conservar solo las que sean necesarias.

En la línea 26 del archivo [pom.xml] encontramos la línea:


<start-class>hello.Application</start-class>

Esta línea está relacionada con las siguientes:


<build>
        <plugins>
            <plugin> 
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

En las líneas 6-9, el complemento [spring-boot-maven-plugin] permite generar el archivo JAR ejecutable de la aplicación. La línea 26 del archivo [pom.xml] designa, por tanto, la clase ejecutable de dicho archivo JAR.

11.2.2. La capa [JPA]

El acceso a la base de datos se realiza a través de una capa [JPA], Java Persistence API:

  

La aplicación es básica y gestiona clientes [Customer]. La clase [Customer] forma parte de la capa [JPA] y es la siguiente:


package hello;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String firstName;
    private String lastName;

    protected Customer() {
    }

    public Customer(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
    }

}

Un cliente tiene un identificador [id], un nombre [firstName] y un apellido [lastName]. Cada instancia [Customer] representa una fila de una tabla de la base de datos.

  • Línea 8: anotación JPA que hace que la persistencia de las instancias [Customer] (Create, Read, Update, Delete) vaya a ser gestionada por una implementación JPA. Según las dependencias de Maven, se observa que se utiliza la implementación JPA / Hibernate;
  • líneas 11-12: anotaciones JPA que asocian el campo [id] a la clave primaria de la tabla [Customer]. La línea 12 indica que la implementación JPA utilizará el método de generación de clave primaria propio de la implementación SGBD utilizada, en este caso H2;

No hay otras anotaciones para JPA. En ese caso, se utilizarán los valores por defecto:

  • la tabla de [Customer] llevará el nombre de la clase, es decir, [Customer];
  • las columnas de esta tabla llevarán el nombre de los campos de la clase: [id, firstName, lastName], teniendo en cuenta que no se distingue entre mayúsculas y minúsculas en el nombre de una columna de tabla;

Cabe señalar que en ningún momento se menciona el nombre de la implementación JPA utilizada.

11.2.3. La capa [Spring Data]

La clase [CustomerRepository] implementa la capa de acceso a la tabla [Customer]. Su código es el siguiente:

  

package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}

Por lo tanto, se trata de una interfaz y no de una clase (línea 7). Extiende la interfaz [CrudRepository], una interfaz de Spring Data (línea 5). Esta interfaz se define mediante dos tipos: el primero es el tipo de los elementos gestionados, en este caso el tipo [Customer]; el segundo, el tipo de la clave primaria de los elementos gestionados, en este caso un tipo [Long]. La interfaz [CrudRepository] es la siguiente:


package org.springframework.data.repository;

import java.io.Serializable;

@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {

    <S extends T> S save(S entity);

    <S extends T> Iterable<S> save(Iterable<S> entities);

    T findOne(ID id);

    boolean exists(ID id);

    Iterable<T> findAll();

    Iterable<T> findAll(Iterable<ID> ids);

    long count();

    void delete(ID id);

    void delete(T entity);

    void delete(Iterable<? extends T> entities);

    void deleteAll();
}

Esta interfaz define las operaciones CRUD (Crear – Leer – Actualizar – Eliminar) que se pueden realizar sobre un tipo JPA T:

  • línea 8: el método «save» permite guardar una entidad T en la base de datos. Guarda la entidad con la clave primaria que le ha asignado SGBD. También permite actualizar una entidad T identificada por su clave primaria id. La elección de una u otra acción depende del valor de la clave primaria id: si este es nulo, se lleva a cabo la operación de persistencia; en caso contrario, se realiza la operación de actualización;
  • línea 10: lo mismo, pero para una lista de entidades;
  • línea 12: el método findOne permite recuperar una entidad T identificada por su clave primaria id;
  • línea 22: el método delete permite eliminar una entidad T identificada por su clave primaria id;
  • líneas 24-28: variantes del método [delete];
  • línea 16: el método [findAll] permite recuperar todas las entidades persistentes T;
  • línea 18: lo mismo, pero limitado a las entidades cuya lista de identificadores se haya pasado;

Volvamos a la interfaz [CustomerRepository]:


package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}
  • la línea 9 permite recuperar un [Customer] por su nombre [lastName];

Y eso es todo en cuanto a la capa [DAO]. No hay ninguna clase que implemente la interfaz anterior. Esta se genera en tiempo de ejecución mediante [Spring Data]. Los métodos de la interfaz [CrudRepository] se implementan automáticamente. En cuanto a los métodos añadidos en la interfaz [CustomerRepository], depende. Volvamos a la definición de [Customer]:


private long id;
private String firstName;
private String lastName;

El método de la línea 9 se implementa automáticamente mediante [Spring Data] porque hace referencia al campo [lastName] (línea 3) de [Customer]. Cuando encuentra un método [findBySomething] en la interfaz que se va a implementar, Spring Data lo implementa mediante la siguiente consulta JPQL (Java Persistence Query Language):

select t from T t where t.something=:value

Por lo tanto, el tipo T debe tener un campo denominado [something]. De este modo, el método

List<Customer> findByLastName(String lastName);

se implementará mediante un código similar al siguiente:

return [em].createQuery("select c from Customer c where  c.lastName=:value").setParameter("value",lastName).getResultList()

donde [em] hace referencia al contexto de persistencia JPA. Esto solo es posible si la clase [Customer] tiene un campo llamado [lastName], lo cual es así.

En conclusión, en los casos sencillos, Spring Data nos permite implementar la capa [DAO] con una simple interfaz.

11.2.4. La capa [console]

  

La clase [Application] es la siguiente:


package hello;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application implements CommandLineRunner {

    @Autowired
    CustomerRepository repository;
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }

    @Override
    public void run(String... strings) throws Exception {
        // guardar un par de clientes
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));

        // recoger todos los clientes
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : repository.findAll()) {
            System.out.println(customer);
        }
        System.out.println();

        // recoger un cliente concreto mediante ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();

        // recuperar clientes por apellido
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : repository.findByLastName("Bauer")) {
            System.out.println(bauer);
        }
    }

}
  • línea 9: la clase implementa la interfaz [CommandLineRunner], que a su vez es una interfaz de [Spring Boot] (línea 4). Esta interfaz solo tiene un método, el de la línea 19;
  • línea 8: @SpringBootApplication es una anotación que agrupa varias anotaciones [Spring Boot]:
    • @Configuration: indica que la clase es una clase de configuración;
    • @EnableAutoConfiguration: solicita a [Spring Boot] que cree por sí mismo un determinado número de beans en función de diversas propiedades, en particular el contenido del Classpath del proyecto. Dado que las bibliotecas de Hibernate se encuentran en el Classpath, el bean [entityManagerFactory] se implementará con Hibernate. Dado que la biblioteca de SGBD H2 se encuentra en el Classpath, el bean [dataSource] se implementará con H2. En el bean [dataSource], también hay que definir el usuario y su contraseña. En este caso, Spring Boot utilizará el administrador predeterminado de H2, que no tiene contraseña. Dado que la biblioteca [spring-tx] se encuentra en el Classpath, se utilizará el gestor de transacciones de Spring;
    • @EnableWebMvc: si en el Classpath se encuentra la biblioteca [spring-mvc]. En este caso, se realiza una autoconfiguración para la aplicación web;
    • @ComponentScan: indica a Spring dónde buscar los demás beans, configuraciones y servicios. Aquí se buscan por defecto en el paquete que contiene la clase etiquetada, es decir, el paquete [hello]. De este modo, se encontrarán las clases [Customer] y [CustomerRepository]. Dado que la primera tiene la anotación [@Entity], se catalogará como entidad que debe gestionar Hibernate. Dado que la segunda extiende la interfaz [CrudRepository], se registrará como bean de Spring;
  • líneas 11-12: el bean [CustomerRepository] se inyecta en el código de la clase principal;
  • línea 15: se ejecuta el método estático [run] de la clase [SpringApplication] del proyecto Spring Boot. Su parámetro es la clase que tiene una anotación [Configuration] o [EnableAutoConfiguration]. A continuación, se llevará a cabo todo lo explicado anteriormente. El resultado es un contexto de aplicación Spring, es decir, un conjunto de beans gestionados por Spring;

Las operaciones siguientes se limitan a utilizar los métodos del bean que implementa la interfaz [CustomerRepository]. Los resultados en la consola son los siguientes:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.2.RELEASE)

2015-03-10 15:35:43.661  INFO 5784 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 5784 (started by ST in C:\Users\Serge Tahé\Documents\workspace-sts-3.6.3.RELEASE\gs-accessing-data-jpa-complete)
2015-03-10 15:35:43.708  INFO 5784 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@5d11346a: startup date [Tue Mar 10 15:35:43 CET 2015]; root of context hierarchy
2015-03-10 15:35:45.230  INFO 5784 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2015-03-10 15:35:45.254  INFO 5784 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
2015-03-10 15:35:45.331  INFO 5784 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.8.Final}
2015-03-10 15:35:45.332  INFO 5784 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2015-03-10 15:35:45.334  INFO 5784 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2015-03-10 15:35:45.651  INFO 5784 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
2015-03-10 15:35:45.754  INFO 5784 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2015-03-10 15:35:45.877  INFO 5784 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Using ASTQueryTranslatorFactory
2015-03-10 15:35:46.154  INFO 5784 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2015-03-10 15:35:46.169  INFO 5784 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
2015-03-10 15:35:46.779  INFO 5784 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']
2015-03-10 15:35:47.040  INFO 5784 --- [           main] hello.Application                        : Started Application in 3.623 seconds (JVM running for 4.324)
2015-03-10 15:35:47.042  INFO 5784 --- [       Thread-1] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@5d11346a: startup date [Tue Mar 10 15:35:43 CET 2015]; root of context hierarchy
2015-03-10 15:35:47.044  INFO 5784 --- [       Thread-1] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
2015-03-10 15:35:47.046  INFO 5784 --- [       Thread-1] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2015-03-10 15:35:47.047  INFO 5784 --- [       Thread-1] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2015-03-10 15:35:47.051  INFO 5784 --- [       Thread-1] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
  • líneas 1-8: el logotipo del proyecto Spring Boot;
  • línea 9: se ejecuta la clase [hello.Application];
  • línea 10: [AnnotationConfigApplicationContext] es una clase que implementa la interfaz [ApplicationContext] de Spring. Se trata de un contenedor de beans;
  • línea 11: el bean [entityManagerFactory] se implementa con la clase [LocalContainerEntityManagerFactory], una clase de Spring;
  • línea 12: aparece [Hibernate]. Se ha elegido esta implementación, JPA;
  • línea 19: el dialecto de Hibernate es la variante SQL, que se utilizará con SGBD. Aquí, el dialecto [H2Dialect] indica que Hibernate va a trabajar con SGBD y H2;
  • líneas 21-22: se crea la base de datos. Se crea la tabla [CUSTOMER]. Esto significa que Hibernate se ha configurado para generar las tablas a partir de las definiciones JPA; en este caso, la definición JPA de la clase [Customer];
  • líneas 26-30: resultado del método [findAll] de la interfaz;
  • línea 34: resultado del método [findOne] de la interfaz;
  • líneas 38-39: resultados del método [findByLastName];
  • líneas 41 y siguientes: registros del cierre del contexto de Spring.

11.2.5. Configuración manual del proyecto Spring Data

Duplicamos el proyecto anterior en el proyecto [gs-accessing-data-jpa-02]:

  

En este nuevo proyecto, no vamos a basarnos en la configuración automática realizada por Spring Boot. La realizaremos manualmente. Esto puede resultar útil si las configuraciones por defecto no nos convencen.

En primer lugar, vamos a especificar las dependencias necesarias en el archivo [pom.xml]:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa-02</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <!-- Base de datos H2 -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <!-- utiliza UTF-8 para todo -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/libs-release</url>
        </repository>
        <repository>
            <id>org.jboss.repository.releases</id>
            <name>JBoss Maven Release Repository</name>
            <url>https://repository.jboss.org/nexus/content/repositories/releases</url>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/libs-release</url>
        </pluginRepository>
    </pluginRepositories>

</project>
  • líneas 10-14: el proyecto Maven principal, cuyas bibliotecas vamos a utilizar;
  • líneas 18-21: Spring Data, utilizado para acceder a la base de datos;
  • líneas 23-26: la implementación de Hibernate de la especificación JPA;
  • líneas 28-31: el SGBD H2;
  • líneas 33-36: las bases de datos suelen utilizarse con grupos de conexiones abiertas que evitan tener que abrir y cerrar conexiones repetidamente. En este caso, la implementación utilizada es la de [tomcat-jdbc];

En el nuevo proyecto, la entidad [Customer] y la interfaz [CustomerRepository] no sufren cambios. Vamos a modificar la clase [Application], que se dividirá en dos clases:

  • [Config], que será la clase de configuración;
  • [Main], que será la clase ejecutable;
  

La clase ejecutable [Application] queda ahora así:


package console;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import repositories.CustomerRepository;
import config.AppConfig;
import entities.Customer;

public class Application {
    public static void main(String[] args) {
        // instanciación del contexto de Spring
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);

        // guardar un par de clientes
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));

        ...

        // cierre del contexto
        context.close();
    }
}
  • línea 9: la clase [Application] ya no tiene anotaciones de configuración;
  • líneas 3-7: cabe destacar que ya no hay importaciones de paquetes [Spring Boot];
  • línea 12: se instancian los beans de Spring. Se obtiene el contexto de Spring que contiene la referencia de los beans así creados;
  • línea 13: se solicita una referencia al bean de tipo [CustomerRepository];

La clase [Config] que configura el proyecto es la siguiente:


package config;

import javax.persistence.EntityManagerFactory;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
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;
import org.springframework.transaction.annotation.EnableTransactionManagement;

//@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "repositories" })
@Configuration
// @ComponentScan(basePackages={"package1","package2"})
public class AppConfig {

    // la base de datos H2
    @Bean
    public DataSource dataSource() {
        // fuente de datos TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuración de acceso JDBC
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        // una conexión abierta inicialmente
        dataSource.setInitialSize(1);
        // resultado
        return dataSource;
    }

    // el proveedor JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        hibernateJpaVendorAdapter.setDatabase(Database.H2);
        return hibernateJpaVendorAdapter;
    }

    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan("entities");
        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 17: la anotación [@EnableTransactionManagement] indica que los métodos de las interfaces [CrudRepository] deben ejecutarse dentro de una transacción. Se ha comentado porque es el comportamiento por defecto;
  • línea 18: la anotación [@EnableJpaRepositories] permite designar las carpetas donde se encuentran las interfaces Spring Data [CrudRepository]. Estas interfaces se convertirán en componentes de Spring y estarán disponibles en su contexto;
  • línea 19: la anotación [@Configuration] convierte la clase [Config] en una clase de configuración de Spring;
  • línea 20: la anotación [@ComponentScan] permite enumerar las carpetas en las que se deben buscar los componentes de Spring. Los componentes de Spring son clases etiquetadas con anotaciones de Spring como @Service, @Component, @Controller, etc. En este caso, no hay más que los definidos dentro de la clase [AppConfig], por lo que la anotación se ha comentado;
  • líneas 24-37: definen la fuente de datos, la base de datos H2. Es la anotación @Bean de la línea 25 la que convierte al objeto creado por este método en un componente gestionado por Spring. El nombre del método puede ser cualquiera. Sin embargo, debe llamarse [dataSource] si el EntityManagerFactory de la línea 51 no existe y se define mediante autoconfiguración;
  • línea 30: la base de datos se llamará [demo] y se generará en la carpeta del proyecto;
  • líneas 40-47: definen la implementación JPA utilizada, en este caso una implementación de Hibernate. El nombre del método puede ser cualquiera;
  • línea 43: no hay registros de SQL;
  • línea 44: la base de datos se creará si no existe;
  • líneas 50-58: definen el método EntityManagerFactory que gestionará la persistencia de JPA. El método debe llamarse obligatoriamente [entityManagerFactory];
  • línea 51: el método recibe dos parámetros del tipo de los dos beans definidos anteriormente. A continuación, Spring los creará y los inyectará como parámetros del método;
  • línea 53: establece la implementación JPA que se va a utilizar;
  • línea 54: establece las carpetas donde se encuentran las entidades JPA;
  • línea 55: establece la fuente de datos que se va a gestionar;
  • líneas 61-66: el gestor de transacciones. El método debe llamarse obligatoriamente [transactionManager]. Recibe como parámetro el bean de las líneas 51-58;
  • línea 64: el gestor de transacciones se asocia a EntityManagerFactory;

Los métodos anteriores pueden definirse en cualquier orden.

La ejecución del proyecto ofrece los mismos resultados. Aparece un nuevo archivo en la carpeta del proyecto, el de la base de datos H2:

  

11.2.6. Creación de un archivo ejecutable

Para crear un archivo ejecutable del proyecto, se puede proceder de la siguiente manera:

  • en [1]: se crea una configuración de ejecución;
  • en [2]: de tipo [Java Application]
  • en [3]: se indica el proyecto que se va a ejecutar (utilizar el botón Browse);
  • en [4]: indica la clase que se va a ejecutar;
  • en [5]: el nombre de la configuración de ejecución; puede ser cualquiera;
  • en [6]: se exporta el proyecto;
  • en [7]: en forma de archivo ejecutable JAR;
  • en [8]: indica la ruta y el nombre del archivo ejecutable que se va a crear;
  • en [9]: el nombre de la configuración de ejecución creada en [5];
  • en [10], el archivo comprimido creado;

Una vez hecho esto, abrimos una consola en la carpeta que contiene el archivo ejecutable:

.....\dist>dir
12/06/2014  09:11        15 104 869 gs-accessing-data-jpa-02.jar

El archivo se ejecuta de la siguiente manera:


.....\dist>java -jar gs-accessing-data-jpa-02.jar

Los resultados obtenidos en la consola son los siguientes:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder para obtener más detalles.
mars 10, 2015 5:27:20 PM org.hibernate.jpa.internal.util.LogHelper logPersistenceUnitInformation
INFO: HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
mars 10, 2015 5:27:20 PM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {4.3.8.Final}
mars 10, 2015 5:27:20 PM org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
mars 10, 2015 5:27:20 PM org.hibernate.cfg.Environment buildBytecodeProvider
INFO: HHH000021: Bytecode provider name : javassist
mars 10, 2015 5:27:22 PM org.hibernate.annotations.common.reflection.java.JavaReflectionManager <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
mars 10, 2015 5:27:22 PM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
mars 10, 2015 5:27:22 PM org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory <init>
INFO: HHH000397: Using ASTQueryTranslatorFactory
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000228: Running hbm2ddl schema update
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000102: Fetching database metadata
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000396: Updating schema
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000232: Schema update complete
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']

11.3. Ejemplo 2

11.3.1. Introducción

Vamos a retomar el ejemplo de la tabla de productos que utilizamos para presentar el API JDBC y crear la siguiente arquitectura:

La base de datos [dbintrospringjpa] contiene dos tablas: [PRODUITS] y [CATEGORIES]. La tabla [CATEGORIES] es la siguiente:

 
  • [ID]: clave primaria en modo AUTO_INCREMENT;
  • [VERSION]: número de versión del registro;
  • [NOM]: nombre de la categoría —único—;

La tabla [PRODUITS] es la siguiente:

 
  • [ID]: clave primaria en modo AUTO_INCREMENT;
  • [VERSION]: número de versión del registro;
  • [NOM]: nombre de un producto —único—;
  • [ID_CATEGORIE]: número de su categoría —clave externa en el campo [CATEGORIES.ID]—;
  • [PRIX]: su precio;
  • [DESCRIPTION]: una descripción del producto;

Tarea a realizar: crea la base de datos [dbintrospringdata] con el script SQL [dbintrospringdata.sql] del soporte:


11.3.2. Creación del proyecto Maven

Para crear un esqueleto de proyecto Spring Data, se puede proceder de la siguiente manera:

  • en [1], se crea un nuevo proyecto;
  • en [2]: de tipo [Spring Starter Project];
  • el proyecto generado será un proyecto Maven. En [3], se indica el nombre del grupo del proyecto;
  • en [4]: se indica el nombre del artefacto (en este caso, un archivo jar) que se creará al compilar el proyecto;
  • en [5]: el nombre del proyecto en Eclipse; puede ser cualquiera (no tiene por qué ser idéntico a [4]);
  • en [7]: se indica que se va a crear un proyecto con una capa [JPA] que contenga SGBD y MySQL. Las dependencias necesarias para dicho proyecto se incluirán entonces en el archivo [pom.xml];
  • en [8], introduce el nombre de la carpeta del proyecto;
  • en [9], finaliza el asistente;
  • en [10]: el proyecto creado;

El archivo [pom.xml] incluye las dependencias necesarias para un proyecto JPA:


<?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>istia.st.springdata</groupId>
    <artifactId>intro-spring-data-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>intro-spring-data-01</name>
    <description>démo spring data avec table de produits</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.2.RELEASE</version>
        <relativePath/> <!-- buscar el elemento principal en el repositorio -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>demo.IntroSpringData01Application</start-class>
        <java.version>1.7</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • líneas 14-19: el proyecto padre de Maven —define un gran número de bibliotecas con sus versiones—; estas bibliotecas se utilizan como dependencias de Maven sin especificar su versión;
  • líneas 28-31: la dependencia necesaria para JPA —incluirá [Spring Data]—;
  • líneas 32-36: la dependencia del controlador JDBC de MySQL;
  • líneas 37-41: las dependencias necesarias para las pruebas JUnit integradas con Spring;

La clase ejecutable [Application] no hace nada, pero está preconfigurada:


package demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class IntroSpringData01Application {

    public static void main(String[] args) {
        SpringApplication.run(IntroSpringData01Application.class, args);
    }
}
  • La anotación [@SpringBootApplication] convierte a la clase en una clase de autoconfiguración del proyecto;

La clase de pruebas [ApplicationTests] no hace nada, pero está preconfigurada:


package demo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = IntroSpringData01Application.class)
public class IntroSpringData01ApplicationTests {

    @Test
    public void contextLoads() {
    }

}
  • línea 9: la anotación [@SpringApplicationConfiguration] permite utilizar el archivo de configuración [Application]. De este modo, la clase de pruebas se beneficiará de todos los beans que se definan en dicho archivo;
  • línea 8: la anotación [@RunWith] permite la integración de Spring con JUnit: la clase podrá ejecutarse como una prueba JUnit. [@RunWith] es una anotación JUnit (línea 4), mientras que la clase [SpringJUnit4ClassRunner] es una clase de Spring (línea 6);

Ahora que tenemos un esqueleto de aplicación JPA, podemos completarlo para escribir el proyecto de la capa de persistencia asociada a la base de datos de productos.

11.3.3. El proyecto de Eclipse

Modificamos el proyecto anterior de la siguiente manera:

  
  • [AppConfig.java]: la clase de configuración del proyecto Spring;
  • [Main.java]: la clase ejecutable del proyecto;
  • [IDao.java]: la interfaz de la capa [DAO];
  • [Dao.java]: la clase de implementación de la capa [DAO];
  • [AbstractEntity.java]: la clase padre de las clases [Produit] y [Categorie];
  • [Produit.java]: clase asociada a una fila de la tabla [PRODUITS] de la base de datos;
  • [Categorie.java]: clase asociada a una fila de la tabla [CATEGORIES] de la base de datos;
  • [ProduitsRepository]: la interfaz de Spring Data para acceder a la tabla [PRODUITS];
  • [CategoriesRepository]: la interfaz de Spring Data para acceder a la tabla [CATEGORIES];
  • [pom.xml]: el archivo de configuración del proyecto Maven;

Este proyecto implementa la siguiente arquitectura:

La capa [DAO] solo ve la capa implementada por [Spring Data].

11.3.4. Configuración de Maven

El archivo [pom.xml] del proyecto Maven es el siguiente:


<?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>istia.st.springdata</groupId>
    <artifactId>intro-spring-data-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>intro-spring-data-01</name>
    <description>démo spring data avec table de produits</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <!-- MySQL Base de datos -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- biblioteca jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- Google Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
        <!-- Prueba de Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- biblioteca de registros -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</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>

Esta configuración es la que se utiliza y se explica en el apartado 11.2.5. Añadimos las siguientes bibliotecas:

  • líneas 42-49: una biblioteca jSON utilizada por el método [toString] de la clase [Produit];
  • líneas 51-55: la biblioteca [Google Guava], que proporciona métodos de utilidad para gestionar colecciones de elementos. Será utilizada por la clase [Dao], que implementa la capa [DAO];
  • líneas 56-67: las bibliotecas necesarias para las pruebas JUnit;
  • líneas 69-72: una biblioteca de registros;
  • líneas 81-86: los complementos de Maven necesarios para el proyecto;

11.3.5. Las entidades de la capa [JPA]

Capa

[DAO]

Capa

[console]

Capa

[JPA]

Piloto

[JDBC]

Capa

[Spring Data]

Spring 4

SGBD

  

11.3.5.1. La clase [AbstractEntity]

La clase [AbstractEntity] es la siguiente:


package spring.data.entities;

import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

@MappedSuperclass
public abstract class AbstractEntity {
    // propiedades
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    protected Long id;
    @Version
    @Column(name = "VERSION")
    protected Long version;

    // constructores
    public AbstractEntity() {

    }

    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }

    // redefinición de [equals] y [hashcode]
    @Override
    public int hashCode() {
        return (id != null ? id.hashCode() : 0);
    }

    @Override
    public boolean equals(Object entity) {
        if (!(entity instanceof AbstractEntity)) {
            return false;
        }
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return id != null && this.id.longValue() == other.id.longValue();
    }

    // firma jSON
    public String toString() {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
    
    // getters y setters
....
}

El objetivo de esta clase es proporcionar una clase padre a las entidades JPA, encapsulando en un único lugar las propiedades [id, version] (líneas 19 y 22) comunes a las dos entidades [Produit] y [Categorie] vinculadas a la base de datos. Estas propiedades están vinculadas a las columnas [ID, VERSION] de las tablas (líneas 18 y 21).

  • línea 13: la anotación [@MappedSuperclass] indica que la clase es una clase padre de las entidades JPA;
  • línea 16: la anotación [@Id] indica que el campo [id] (podría tener otro nombre) está asociado a la clave primaria de una tabla;
  • línea 17: la anotación [@GeneratedValue(strategy=GenerationType.IDENTITY)] establece el modo de generación de las claves primarias. El modo [GenerationType.IDENTITY] utilizará, junto con MySQL, el modo [AUTO_INCREMENT]. Con otro SGBD, este modo utilizaría otro método. La ventaja es que el desarrollador no tiene que preocuparse por ello y que su código sigue siendo válido independientemente del SGBD que se utilice;
  • línea 18: la anotación [@Column] indica la columna asociada al campo. Cuando esta anotación no está presente, JPA asume que la columna tiene el mismo nombre que el campo. Este es el caso aquí. Por lo tanto, se podría haber omitido esta anotación;
  • línea 20: 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 versión 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 versión igual a V1. U1 modifica E y guarda esta modificación en la base de datos: el número de versión pasa entonces a V1+1. U2 modifica a su vez E y guarda esta modificación en la base de datos: recibirá una excepción porque tiene una versión (V1) diferente a la de la base de datos (V1+1);
  • líneas 35-52: redefinición de los métodos [hashCode] y [equals]. Por defecto, [obj1.equals(obj2)] es «true» si [obj1==obj2] es «true», es decir, si ob1 y obj2 son dos punteros iguales. Si se desea comparar los objetos a los que apuntan los punteros en lugar de los propios punteros, hay que redefinir el método [equals] y el método [hashCode]. Este último debe devolver el mismo valor para dos objetos que el método [equals] considere iguales;
  • líneas 42-51: dos objetos de tipo [AbstractEntity] o derivados se considerarán iguales si sus claves primarias [id] son iguales;
  • líneas 35-38: el método [hashCode] devuelve efectivamente el mismo valor para dos objetos [AbstractEntity] idénticos y que, por lo tanto, tienen la misma clave primaria [id];
  • líneas 55-63: el método [toString] devuelve la cadena jSon del objeto [this]. Si este objeto hace referencia a una clase hija, este método devolverá entonces la cadena jSON de la clase hija. Esto nos evita tener que crear un método [toString] en las clases hijas;

11.3.5.2. La entidad JPA [Produit]

La clase [Produit] es una entidad JPA asociada a una fila de la tabla [PRODUITS]:

 

package spring.data.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonFilter;

@Entity
@Table(name = "PRODUITS")
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {

    // propiedades
    @Column(name = "NOM")
    private String nom;

    @Column(name = "CATEGORIE_ID", insertable = false, updatable = false)    
    private Long idCategorie;

    @Column(name = "PRIX")
    private double prix;

    @Column(name = "DESCRIPTION")
    private String description;

    // la categoría
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CATEGORIE_ID")    
    private Categorie categorie;

    // constructores
    public Produit() {

    }

    public Produit(String nom, double prix, String description) {
        this.nom = nom;
        this.prix = prix;
        this.description = description;
    }

    // getters y setters
...
}
  • línea 12: la anotación [@Entity] convierte la clase [Produit] en una entidad gestionada por la capa [JPA];
  • línea 13: 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 14: el nombre del filtro jSON que se debe aplicar a la entidad. Veremos que la propiedad [categorie] de la línea 13 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 18: 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, se puede omitir la anotación [@Column]. Este sería el caso aquí;
  • líneas 31-33: la categoría del producto;
  • línea 31: la anotación [@ManyToOne] indica que la columna de la anotación de la línea 32, [@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 33. Esta anotación debe referirse a una entidad JPA. Por lo tanto, la clase de la línea 33 debe ser una entidad JPA;
  • línea 31: la anotación [fetch = FetchType.LAZY] establece que, cuando se recupera un producto de la tabla [PRODUITS], su categoría (línea 33) no se recupere inmediatamente (carga diferida). Esta se obtiene entonces durante la primera llamada al método [getCategorie]. Este atributo no es obligatorio. La implementación JPA utilizada puede ignorarlo. Dado que la propiedad [categorie] puede estar presente o no, hemos introducido el filtro jSON de la línea 14. Las implementaciones JPA existentes (Hibernate, Eclipselink, OpenJPA) no gestionan esta anotación de la misma manera. Hibernate amplía el método [getCategorie] inicial (que se limita a devolver el campo categorie) con una llamada a SGBD para obtener la categoría. Para que esto sea posible, es necesario que la conexión a SGBD utilizada inicialmente para obtener el producto siga abierta; de lo contrario, se produce una excepción.

11.3.5.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 spring.data.entities;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonFilter;

@Entity
@Table(name = "CATEGORIES")
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractEntity {

    // propiedades
    @Column(name = "NOM")
    private String nom;

    // productos relacionados
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
    public Set<Produit> produits = new HashSet<Produit>();

    // constructores
    public Categorie() {

    }

    public Categorie(String nom) {
        this.nom = nom;
    }

    // métodos
    public void addProduit(Produit produit) {
        // se añade el producto
        produits.add(produit);
        // se establece su categoría
        produit.setCategorie(this);
    }

    // getters y setters
...
}
  • líneas 21-22: el nombre de la categoría;
  • líneas 25-26: los productos de esta categoría;
  • línea 25: 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 }] especifica que las operaciones (persist, merge, remove) realizadas sobre una @Entity [Categorie] se propaguen en cascada a las [produits] de la línea 26. Se pueden indicar cascadas parciales con las constantes [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE];
  • línea 25: el atributo [fetch = FetchType.LAZY] especifica que, al recuperar una categoría de la tabla [CATEGORIES], sus productos no se recuperen inmediatamente. Se recuperarán en la primera llamada al método [getProduits]. Las implementaciones existentes de JPA (Hibernate, Eclipselink, OpenJPA) no gestionan esta anotación de la misma manera. Hibernate amplía el método [getProduits] inicial (que se limita a devolver el campo produits) mediante una llamada al método SGBD para recuperar los productos de la categoría. Para que esto sea posible, es necesario que la conexión a SGBD utilizada inicialmente para obtener la categoría siga abierta. 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 17, que nos permitirá indicar si queremos o no esta propiedad;
  • línea 26: el tipo [Set] es una interfaz. El tipo [HashSet] es una clase que implementa esta interfaz. Implementa una colección de elementos denominada ensemble. Un conjunto no puede contener dos objetos idénticos. En este caso, los objetos son de tipo [Produit]. Por lo tanto, en el conjunto no podrá haber dos objetos idénticos. Dado que el método [equals] de la clase padre [AbstractEntity] se ha redefinido para establecer que dos productos son idénticos si tienen la misma clave primaria, el campo [produits] no podrá contener dos productos con la misma clave primaria;
  • líneas 38-43: el método [addProduit] permite añadir un producto a la categoría;

11.3.6. La capa [Spring Data]

Capa

[DAO]

Capa

[console]

Capa

[JPA]

Pantalón

[JDBC]

Capa

[Spring Data]

Spring 4

SGBD

  

La interfaz [CategoriesRepository] gestiona el acceso a la tabla [CATEGORIES]:


package spring.data.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import spring.data.entities.Categorie;

public interface CategoriesRepository extends CrudRepository<Categorie, Long> {

    // categoría con sus productos
    @Query("select c from Categorie c left join fetch c.produits p where c.id=?1")
    public Categorie getCategorieByIdWithProduits(Long id);

    @Query("select c from Categorie c left join fetch c.produits p where c.nom=?1")
    public Categorie getCategorieByNameWithProduits(String nom);

    // una categoría sin sus productos, identificada por su nombre
    public Categorie findByNom(String nom);
}
  • línea 8: la interfaz [CrudRepository] se ha utilizado y explicado en el apartado 11.2.3. Recordemos que:
    • el primer tipo de la interfaz es la entidad JPA, gestionada para los accesos CRUD (findOne, findAll, guardar, eliminar, deleteAll),
    • el segundo tipo es el de la clave primaria de la entidad JPA, en este caso un entero [Long];
  • línea 12: el método de la línea 12 se implementa mediante la consulta JPQL (Java Persistence Query Language) de la línea 11. Esta consulta recupera 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;
  • línea 11: la consulta JPQL devuelve una 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, se fuerza la carga de los productos con la palabra 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 14-15: un método similar para una categoría identificada por su nombre;
  • línea 18: el método [findByNom] se implementará automáticamente mediante [Spring Data], ya que el tipo [Category] tiene un campo [nom];

La interfaz [ProduitsRepository] gestiona los accesos a la tabla [PRODUITS]:


package spring.data.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import spring.data.entities.Produit;

public interface ProduitsRepository extends CrudRepository<Produit, Long> {

    // un producto con su categoría
    @Query("select p from Produit p left join fetch p.categorie c where p.id=?1")
    public Produit getProduitByIdWithCategorie(Long id);

    @Query("select p from Produit p left join fetch p.categorie c where p.nom=?1")
    public Produit getProduitByNameWithCategorie(String nom);

    // un producto sin su categoría, identificado por su nombre
    public Produit findByNom(String nom);
}

Las explicaciones son las mismas que para la interfaz [CategoriesRepository].

Estas interfaces serán implementadas por clases generadas por [Spring Data] en el momento de la ejecución del proyecto. A estas clases se las denomina [proxy]. Por defecto, los métodos de la clase de implementación se ejecutan en una transacción. El hecho de que estas interfaces extiendan la clase [CrudRepository] las convierte en componentes de Spring.

11.3.7. La capa [DAO]

Capa

[DAO]

Capa

[console]

Capa

[JPA]

Pantalón

[JDBC]

Capa

[Spring Data]

Spring 4

SGBD

  

La interfaz [IDao] de la capa [DAO] es la siguiente:


package spring.data.dao;

import java.util.List;

import spring.data.entities.Categorie;
import spring.data.entities.Produit;

public interface IDao {

    // inserción de una lista de productos
    public List<Produit> addProduits(List<Produit> produits);

    // eliminación de todos los productos
    public void deleteAllProduits();

    // actualización de una lista de productos
    public List<Produit> updateProduits(List<Produit> produits);

    // obtención de todos los productos
    public List<Produit> getAllProduits();

    // Inserción de una lista de categorías
    public List<Categorie> addCategories(List<Categorie> categories);

    // eliminación de todas las categorías
    public void deleteAllCategories();

    // Actualización de una lista de categorías
    public List<Categorie> updateCategories(List<Categorie> categories);

    // obtención de todas las categorías
    public List<Categorie> getAllCategories();

    // un producto concreto, con o sin su categoría
    public Produit getProduitByIdWithoutCategorie(Long idProduit);

    public Produit getProduitByIdWithCategorie(Long idProduit);

    public Produit getProduitByNameWithCategorie(String nom);

    public Produit getProduitByNameWithoutCategorie(String nom);

    // una categoría concreta con o sin sus productos
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie);

    public Categorie getCategorieByIdWithProduits(Long idCategorie);

    public Categorie getCategorieByNameWithProduits(String nom);

    public Categorie getCategorieByNameWithoutProduits(String nom);
}

Aquí se ha adoptado la regla de que cualquier método que modifique los objetos que tiene como parámetros de entrada debe devolverlos en su resultado. El motivo de esta regla se ha explicado en el apartado 4.2: permite que una capa y su cliente se encuentren en dos JVM separadas y, por lo tanto, funcionen en modo cliente/servidor.

La implementación [Dao] de esta interfaz es la siguiente:


package spring.data.dao;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.google.common.collect.Lists;

import spring.data.entities.Categorie;
import spring.data.entities.Produit;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;

@Component
public class Dao implements IDao {

    @Autowired
    private ProduitsRepository produitsRepository;

    @Autowired
    private CategoriesRepository categoriesRepository;

    @Override
    public List<Produit> addProduits(List<Produit> produits) {
        try {
            return Lists.newArrayList(produitsRepository.save(produits));
        } catch (Exception e) {
            throw new DaoException(101, getMessagesForException(e));
        }
    }

    @Override
    public void deleteAllProduits() {
        try {
            produitsRepository.deleteAll();
        } catch (Exception e) {
            throw new DaoException(102, getMessagesForException(e));
        }
    }

    @Override
    public List<Produit> updateProduits(List<Produit> produits) {
        try {
            return Lists.newArrayList(produitsRepository.save(produits));
        } catch (Exception e) {
            throw new DaoException(103, getMessagesForException(e));
        }
    }

    @Override
    public List<Categorie> addCategories(List<Categorie> categories) {
        try {
            return Lists.newArrayList(categoriesRepository.save(categories));
        } catch (Exception e) {
            throw new DaoException(104, getMessagesForException(e));
        }
    }

    @Override
    public void deleteAllCategories() {
        try {
            categoriesRepository.deleteAll();
        } catch (Exception e) {
            throw new DaoException(105, getMessagesForException(e));
        }
    }

    @Override
    public List<Categorie> updateCategories(List<Categorie> categories) {
        try {
            return Lists.newArrayList(categoriesRepository.save(categories));
        } catch (Exception e) {
            throw new DaoException(106, getMessagesForException(e));
        }
    }

    @Override
    public List<Categorie> getAllCategories() {
        try {
            return Lists.newArrayList(categoriesRepository.findAll());
        } catch (Exception e) {
            throw new DaoException(107, getMessagesForException(e));
        }
    }

    @Override
    public List<Produit> getAllProduits() {
        try {
            return Lists.newArrayList(produitsRepository.findAll());
        } catch (Exception e) {
            throw new DaoException(108, getMessagesForException(e));
        }
    }

    @Override
    public Produit getProduitByIdWithCategorie(Long idProduit) {
        try {
            return produitsRepository.getProduitByIdWithCategorie(idProduit);
        } catch (Exception e) {
            throw new DaoException(109, getMessagesForException(e));
        }
    }

    @Override
    public Categorie getCategorieByIdWithProduits(Long idCategorie) {
        try {
            return categoriesRepository.getCategorieByIdWithProduits(idCategorie);
        } catch (Exception e) {
            throw new DaoException(110, getMessagesForException(e));
        }
    }

    @Override
    public Categorie getCategorieByNameWithProduits(String nom) {
        try {
            return categoriesRepository.getCategorieByNameWithProduits(nom);
        } catch (Exception e) {
            throw new DaoException(111, getMessagesForException(e));
        }
    }

    @Override
    public Produit getProduitByNameWithCategorie(String nom) {
        try {
            return produitsRepository.getProduitByNameWithCategorie(nom);
        } catch (Exception e) {
            throw new DaoException(112, getMessagesForException(e));
        }
    }

    @Override
    public Produit getProduitByIdWithoutCategorie(Long idProduit) {
        try {
            return produitsRepository.findOne(idProduit);
        } catch (Exception e) {
            throw new DaoException(113, getMessagesForException(e));
        }
    }

    @Override
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie) {
        try {
            return categoriesRepository.findOne(idCategorie);
        } catch (Exception e) {
            throw new DaoException(114, getMessagesForException(e));
        }
    }

    @Override
    public Produit getProduitByNameWithoutCategorie(String nom) {
        try {
            return produitsRepository.findByNom(nom);
        } catch (Exception e) {
            throw new DaoException(115, getMessagesForException(e));
        }
    }

    @Override
    public Categorie getCategorieByNameWithoutProduits(String nom) {
        try {
            return categoriesRepository.findByNom(nom);
        } catch (Exception e) {
            throw new DaoException(116, getMessagesForException(e));
        }
    }

}
  • línea 16: la anotación [@Component] convierte la clase [Dao] en un componente de Spring;
  • líneas 19-23: 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, normalmente al inicio de la ejecución del proyecto Spring;
  • Cabe destacar, en las líneas 28 y 46, que el método [save] de la interfaz [produitsRepository] se utiliza tanto para la inserción como para la actualización de productos. [Spring Data] utiliza la clave primaria del producto para determinar si debe realizar una inserción o una actualización. Si la clave primaria es [null], se tratará de una inserción; en caso contrario, será una actualización;
  • línea 82: se utiliza el método [Lists.newArrayList] de la biblioteca Guava para obtener una lista de productos. El método [produitsRepository.findAll()] devuelve un tipo [Iterable<Produit>];
  • línea 28: el método [produitsRepository.save(produits)] devuelve un tipo [Iterable<Produit>]. Lo mismo ocurre con las demás operaciones [save] de la clase;

En la clase [Dao] anterior, las excepciones que pueden producirse se encapsulan en el siguiente tipo [DaoException]:


package spring.data.dao;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

// clase de excepción para la aplicación «Elecciones»
// la excepción no está controlada

public class DaoException extends RuntimeException implements Serializable {

    // serie ID
    private static final long serialVersionUID = 1L;

    // campos locales
    private int code;
    private List<String> erreurs;

    // constructores
    public DaoException() {
        super();
    }

    public DaoException(int code, Throwable e) {
        // padre
        super(e);
        // local
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public DaoException(int code, String message, Throwable e) {
        // padre
        super(message, e);
        // local
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public DaoException(int code, String message) {
        // superior
        super(message);
        // local
        this.code = code;
        List<String> erreurs = new ArrayList<>();
        erreurs.add(message);
        this.erreurs = erreurs;
    }

    public DaoException(int code, List<String> erreurs) {
        // superior
        super();
        // local
        this.code = code;
        this.erreurs = erreurs;
    }

    // lista de mensajes de error de una excepción
    private List<String> getErreursForException(Throwable th) {
        // se recupera la lista de mensajes de error de la excepción
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // se recupera el mensaje solo si !=null y no está vacío
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // causa siguiente
            cause = cause.getCause();
        }
        return erreurs;
    }

    // getter y setter
...
}
  • línea 10: la clase deriva de la clase [RuntimeException] y, por lo tanto, es una excepción no controlada;
  • línea 16: un código de error;
  • línea 17: una lista de mensajes de error, los asociados a la pila de excepciones que provocaron la [DaoException];
  • líneas 59-76: el método privado [getMessagesForException] permite obtener la lista de mensajes de error asociados a las excepciones de la pila de excepciones. De hecho, es posible apilar excepciones con los siguientes constructores de la clase Exception:
    • Exception(String mensaje, Throwable causa): crea una excepción con un mensaje y la excepción que se desea encapsular;
    • Exception(Throwable cause): crea una excepción con la excepción que se desea encapsular;

El tipo [Throwable] es la clase padre de la clase [Exception]. Si los constructores anteriores se ejecutan repetidamente, la excepción final contiene varias excepciones. Se dice que se tiene una pila de excepciones.

  • La última causa de una excepción e1 se obtiene mediante la expresión [e1.getCause()];
  • la penúltima causa de una excepción e1 se obtiene mediante la expresión [e1.getCause().getCause()];
  • y así sucesivamente hasta obtener [getCause()==null];

11.3.8. Configuración del proyecto Spring

  

La clase [DaoConfig] configura la capa [DAO]:


package spring.data.config;

import javax.persistence.EntityManagerFactory;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
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;

@EnableJpaRepositories(basePackages = { "spring.data.repositories" })
@Configuration
@ComponentScan(basePackages = { "spring.data.dao" })
public class DaoConfig {

    // constantes
    final static String URL = "jdbc:mysql://localhost:3306/dbIntroSpringData";
    final static String USER = "root";
    final static String PASSWD = "";
    final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
    final static String[] ENTITIES_PACKAGES = { "spring.data.entities" };

    // la fuente de datos [tomcat-jdbc]
    @Bean
    public DataSource dataSource() {
        // fuente de datos TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuración de acceso JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // una conexión abierta inicialmente
        dataSource.setInitialSize(1);
        // resultado
        return dataSource;
    }

    // el proveedor JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        return hibernateJpaVendorAdapter;
    }

    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(packagesToScan());
        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;
    }

    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
    }

}

En el apartado 11.2.5 se ha descrito una configuración similar. A ella hemos añadido las siguientes anotaciones de Spring:

  • línea 17: la anotación [@EnableJpaRepositories] sirve para indicar los paquetes en los que se encuentran las interfaces [CrudRepository] y [Spring Data];
  • línea 18: la clase es una clase de configuración de Spring. Esta información es importante. Si se elimina, el proyecto sigue funcionando. Sin embargo, más adelante en el documento, cuando se creen proyectos basados en este, algunos de ellos dejarán de funcionar si se elimina la anotación de la línea 18;
  • línea 19: la anotación [@ComponentScan] indica los paquetes en los que se encuentran los objetos de Spring. Se trata de las clases anotadas con [@Component, @Service, @Controller, ...]. Aquí se localizará e instanciará el componente de Spring [Dao];
  • líneas 73-76: hemos definido un bean que representa la matriz de paquetes que se deben escanear para encontrar entidades JPA. Esto permitirá que un proyecto que importe la clase [DaoConfig] redefina este bean y, de este modo, modifique los paquetes analizados (línea 59). Más adelante en el documento, nos encontraremos con esta problemática;

La clase [AppConfig] configura todo el proyecto:


package spring.data.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

@Configuration
@Import({DaoConfig.class})
public class AppConfig {
    // filtros jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }

    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // Mapeador jSON
        ObjectMapper mapper = new ObjectMapper();
        // filtros
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // resultado
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // mapeador jSON
        ObjectMapper mapper = new ObjectMapper();
        // filtros
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // resultado
        return mapper;
    }

    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // mapeador jSON
        ObjectMapper mapper = new ObjectMapper();
        // filtros
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // resultado
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // mapeador jSON
        ObjectMapper mapper = new ObjectMapper();
        // filtros
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // resultado
        return mapper;
    }
}
  • línea 11: la clase es una clase de configuración de Spring;
  • línea 12: que importa los beans definidos por la clase [DaoConfig] que acabamos de ver;
  • la capa [console] utiliza mapeadores jSON que se definen aquí;
  • líneas 14-64: definen cinco mapeadores jSON;
  • líneas 15-18: el filtro jSON [jsonMapper] no tiene filtros;
  • líneas 20-30: el filtro jSON [jsonMapperCategorieWithProduits] permite serializar/deserializar un objeto [Categorie] con sus productos;
  • líneas 32-42: el filtro jSON [jsonMapperProduitWithCategorie] permite serializar y deserializar un objeto [Produit] junto con su categoría;
  • líneas 43-53: el filtro jSON [jsonMapperCategorieWithoutProduits] permite serializar y deserializar un objeto [Categorie] sin sus productos;
  • líneas 55-64: el filtro jSON [jsonMapperProduitWithoutCategorie] permite serializar/deserializar un objeto [Produit] sin su categoría;

Cabe señalar que, al crear un filtro jSON para una entidad T, hay que configurar no solo el filtro de la entidad T, sino también los de las entidades Ti que esta pueda contener.

11.3.9. La capa [console]

Capa

[DAO]

Capa

[console]

Capa

[JPA]

Pantalón

[JDBC]

Capa

[Spring Data]

Spring 4

SGBD

  

La clase [Main] es la siguiente:


package spring.data.console;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;

import spring.data.config.AppConfig;
import spring.data.dao.DaoException;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;

public class Main {

    public static void main(String[] args) throws JsonProcessingException {
        AnnotationConfigApplicationContext context = null;
        try {
            // instanciación del contexto de Spring
            context = new AnnotationConfigApplicationContext(AppConfig.class);
            ObjectMapper jsonMapperCategorieWithProduits = context.getBean("jsonMapperCategorieWithProduits",
                    ObjectMapper.class);
            ObjectMapper jsonMapperProduitWithCategorie = context.getBean("jsonMapperProduitWithCategorie",
                    ObjectMapper.class);
            ObjectMapper jsonMapperCategorieWithoutProduits = context.getBean("jsonMapperCategorieWithoutProduits",
                    ObjectMapper.class);
            ObjectMapper jsonMapperProduitWithoutCategorie = context.getBean("jsonMapperProduitWithoutCategorie",
                    ObjectMapper.class);
            IDao dao = context.getBean(IDao.class);
            // --------------------------------------------------------------------------------------
            // se vacía la base de datos
            log("Vidage de la base de données", 1);
            // se vacía la tabla [CATEGORIES]; de forma en cascada, se vaciará la tabla [PRODUITS]
            dao.deleteAllCategories();
            // --------------------------------------------------------------------------------------
            log("Remplissage de la base", 1);
            // se rellenan las tablas
            List<Categorie> categories = new ArrayList<Categorie>();
            for (int i = 0; i < 2; i++) {
                Categorie categorie = new Categorie(String.format("categorie%d", i));
                for (int j = 0; j < 5; j++) {
                    categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                            String.format("desc%d%d", i, j)));
                }
                categories.add(categorie);
            }
            // se añade la categoría; de forma cascada, también se insertarán los productos
            dao.addCategories(categories);
            // --------------------------------------------------------------------------------------
            log("Affichage de la base", 1);
            // lista de categorías
            log("Liste des catégories", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            // lista de productos
            log("Liste des produits", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
            // categoría 1 con sus productos
            Categorie categorie = dao.getCategorieByNameWithProduits("categorie1");
            log("Catégorie 1 avec ses produits", 2);
            affiche(categorie, jsonMapperCategorieWithProduits);
            // el producto [produit14] con su categoría
            Produit p = dao.getProduitByNameWithCategorie("produit14");
            log("Produit [produit14] avec sa catégorie", 2);
            affiche(p, jsonMapperProduitWithCategorie);
            // --------------------------------------------------------------------------------------
            log("Mise à jour du prix des produits de [categorie1]", 1);
            log("Produits de la catégorie [categorie1] avant la mise à jour", 2);
            Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
            Set<Produit> produits = categorie1.getProduits();
            affiche(categorie1, jsonMapperCategorieWithProduits);
            for (Produit produit : produits) {
                produit.setPrix(1.1 * produit.getPrix());
            }
            dao.updateProduits(Lists.newArrayList(produits));
            log("Produits de la catégorie [categorie1] après la mise à jour", 2);
            affiche(dao.getCategorieByNameWithProduits("categorie1"), jsonMapperCategorieWithProduits);
            // --------------------------------------------------------------------------------------
            log("Vidage de la base de données", 1);
            // se vacía la tabla [CATEGORIES]; a continuación, de forma en cascada, se vaciará la tabla [PRODUITS]
            dao.deleteAllCategories();
            // visualización de la base de datos
            log("Liste des categories avant l'ajout", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            log("Liste des produits avant l'ajout", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
            log("Ajout d'une catégorie [cat1] avec deux produits de même nom", 1);
            // se realiza la inserción
            categorie = new Categorie("cat1");
            categorie.addProduit(new Produit("x", 1.0, ""));
            categorie.addProduit(new Produit("x", 1.0, ""));
            // se añade la categoría; de forma cascada, también se insertarán los productos
            try {
                dao.addCategories(Lists.newArrayList(categorie));
            } catch (DaoException e) {
                System.out.println(e);
            }
            // comprobación
            log("Liste des categories après l'ajout", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            log("Liste des produits après l'ajout", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
        } catch (DaoException e) {
            System.out.println(e);
        } finally {
            if (context != null) {
                // finalizado
                context.close();
            }
        }
        System.out.println("Travail terminé");
    }

    // visualización de un elemento de tipo T
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }

    // Visualización de una lista de elementos de tipo T
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }

    private static void log(String message, int mode) {
        // muestra un 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);
    }
}
  • línea 25: instanciación de los beans de Spring a partir de la clase de configuración [AppConfig];
  • líneas 26-33: obtención de las referencias a los mapeadores jSON. Se utiliza la siguiente firma del método [ApplicationContext].getBean:
    • [ApplicationContext].getBean(String id, Class clase): que se utiliza cuando hay varios beans del tipo [classe]. En este caso, se especifica el identificador del bean solicitado. Si este se ha definido con la anotación [@Bean], su identificador es el nombre del método anotado. Si se ha definido con la anotación [@Bean(« identifiant »], su identificador es el indicado en la anotación;
  • línea 34: obtención de una referencia en la capa [DAO];
  • líneas 37-39: vaciado de la base de datos. Se vacía la tabla de categorías (línea 39). Porque se ha escrito:

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
    public Set<Produit> produits = new HashSet<Produit>();

cuando se elimina una categoría, también se eliminan todos los productos vinculados a ella;

  • líneas 43-53: rellenado de la tabla con dos categorías de cinco productos cada una. En la línea 50, la inserción de las dos categorías provocará al mismo tiempo la inserción de sus productos, siempre porque hemos escrito [cascade = { CascadeType.ALL }];
  • línea 58: se muestran las categorías. Se utiliza el mapeador jSON [jsonMapperCategorieWithoutProduits] para mostrar las categorías sin sus productos. De hecho, el método [dao.getAllCategories()] muestra las categorías sin sus productos (carga diferida);
  • línea 61: se muestran los productos sin su categoría. De hecho, el método [dao.getAllProduits()] muestra los productos sin su categoría (carga diferida);
  • líneas 63-65: muestran la categoría denominada [categorie1] con sus productos (carga inmediata);
  • líneas 67-69: se muestra un producto con su categoría;
  • líneas 71-81: se incrementan en un 10 % todos los precios de los productos de la categoría [categorie1];
  • líneas 91-101: se añade una categoría con dos productos del mismo nombre. Sin embargo, en la tabla [PRODUITS] existe una restricción de unicidad en la columna [NOM]. Por lo tanto, la inserción del segundo producto será rechazada y se lanzará una excepción. Ahora bien, el método [dao.addProduits] se ejecuta en una transacción. El hecho de que la segunda inserción falle debe, por lo tanto, anular también la inserción del primer producto, así como la de su categoría [cat1]. Esto es lo que queremos comprobar;
  • líneas 119-121: un método genérico capaz de mostrar la cadena jSON de cualquier elemento de tipo T. La serialización jSON se controla mediante el mapeador pasado como parámetro;
  • líneas 124-128: un método análogo, esta vez para una lista de elementos de tipo T;

La ejecución de la clase [Main] ofrece los siguientes resultados (sin contar los registros de Spring):


Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Affichage de la base --------------------------------
-- Liste des catégories
{"id":4,"version":0,"nom":"categorie0"}
{"id":5,"version":0,"nom":"categorie1"}
-- Liste des produits
{"id":13,"version":0,"nom":"produit00","idCategorie":4,"prix":100.0,"description":"desc00"}
{"id":14,"version":0,"nom":"produit01","idCategorie":4,"prix":101.0,"description":"desc01"}
{"id":15,"version":0,"nom":"produit02","idCategorie":4,"prix":102.0,"description":"desc02"}
{"id":16,"version":0,"nom":"produit03","idCategorie":4,"prix":103.0,"description":"desc03"}
{"id":17,"version":0,"nom":"produit04","idCategorie":4,"prix":104.0,"description":"desc04"}
{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"}
{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"}
{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"}
{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"}
{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}
-- Catégorie 1 avec ses produits
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"},{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"},{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"},{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"},{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}]}
-- Produit [produit14] avec sa catégorie
{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14","categorie":{"id":5,"version":0,"nom":"categorie1"}}
Mise à jour du prix des produits de [categorie1] --------------------------------
-- Produits de la catégorie [categorie1] avant la mise à jour
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"},{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"},{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"},{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"},{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}]}
-- Produits de la catégorie [categorie1] après la mise à jour
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":1,"nom":"produit10","idCategorie":5,"prix":121.0,"description":"desc10"},{"id":19,"version":1,"nom":"produit11","idCategorie":5,"prix":122.1,"description":"desc11"},{"id":20,"version":1,"nom":"produit12","idCategorie":5,"prix":123.2,"description":"desc12"},{"id":21,"version":1,"nom":"produit13","idCategorie":5,"prix":124.3,"description":"desc13"},{"id":22,"version":1,"nom":"produit14","idCategorie":5,"prix":125.4,"description":"desc14"}]}
Vidage de la base de données --------------------------------
-- Liste des categories avant l'ajout
-- Liste des produits avant l'ajout
Ajout d'une catégorie [cat1] avec deux produits de même nom --------------------------------
Les erreurs suivantes se sont produites : 
- org.hibernate.exception.ConstraintViolationException: could not execute statement
- could not execute statement
- Duplicate entry 'x' for key 'NOM'
-- Liste des categories après l'ajout
-- Liste des produits après l'ajout
Travail terminé
  • líneas 4-17: las categorías y los productos insertados en la tabla;
  • líneas 18-19: una categoría con sus productos;
  • líneas 20-21: un producto con su categoría;
  • líneas 22-26: actualización del precio de algunos productos. En la línea 24, se observa que los precios han aumentado efectivamente un 10 %;
  • líneas 27-36: se añade la categoría [cat1] con dos productos del mismo nombre. Se observa que la tabla es la misma antes (líneas 28-29) y después de la adición (líneas 35-36), lo que demuestra que todas las inserciones de la transacción se han cancelado correctamente;
  • líneas 31-34: la excepción que se produjo al insertar el segundo producto y que provocó el fallo de toda la transacción;

11.3.10. La prueba unitaria JUnit

  

La clase [Test01] es la siguiente:


package spring.data.tests;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;

import spring.data.config.AppConfig;
import spring.data.dao.DaoException;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;

@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {

    // capa [DAO]
    @Autowired
    private IDao dao;

    // filtros jSON
    @Autowired
    @Qualifier("jsonMapper")
    private ObjectMapper jsonMapper;
    @Autowired
    @Qualifier("jsonMapperCategorieWithProduits")
    private ObjectMapper jsonMapperCategorieWithProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithCategorie")
    private ObjectMapper jsonMapperProduitWithCategorie;
    @Autowired
    @Qualifier("jsonMapperCategorieWithoutProduits")
    private ObjectMapper jsonMapperCategorieWithoutProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithoutCategorie")
    private ObjectMapper jsonMapperProduitWithoutCategorie;

    @Before
    public void cleanAndFill() {
        // 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]; en consecuencia, la tabla [PRODUITS] se vaciará
        dao.deleteAllCategories();
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // se rellenan las tablas
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < 2; i++) {
            Categorie categorie = new Categorie(String.format("categorie%d", i));
            for (int j = 0; j < 5; j++) {
                categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                        String.format("desc%d%d", i, j)));
            }
            categories.add(categorie);
        }
        // se añade la categoría; de forma cascada, también se insertarán los productos
        categories = dao.addCategories(categories);
    }

    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // lista de categorías
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories();
        affiche(categories, jsonMapperCategorieWithoutProduits);
        // lista de productos
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits();
        affiche(produits, jsonMapperProduitWithoutCategorie);
        // algunas comprobaciones
        Assert.assertEquals(2, categories.size());
        Assert.assertEquals(10, produits.size());
        Categorie categorie = findCategorieByName("categorie0", categories);
        Assert.assertNotNull(categorie);
        Produit produit = findProduitByName("produit03", produits);
        Assert.assertNotNull(produit);
        Long idCategorie = produit.getIdCategorie();
        Assert.assertEquals(categorie.getId(), idCategorie);
    }

    @Test
    public void getCategorieByNameWithProduits() {
        log("getCategorieByNameWithProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
        Assert.assertNotNull(categorie1);
        Assert.assertEquals(5, categorie1.getProduits().size());
    }

    @Test
    public void getCategorieByNameWithoutProduits() {
        log("getCategorieByNameWithoutProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithoutProduits("categorie1");
        Assert.assertNotNull(categorie1);
        Assert.assertEquals("categorie1", categorie1.getNom());
    }

    @Test
    public void getProduitByIdWithCategorie() {
        log("getProduitByNameWithCategorie", 1);
        Produit produit = dao.getProduitByNameWithCategorie("produit03");
        Produit produit2 = dao.getProduitByIdWithCategorie(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
        Assert.assertEquals(produit.getCategorie().getId(), produit2.getCategorie().getId());
    }

    @Test
    public void getProduitByIdWithoutCategorie() {
        log("getProduitByIdWithoutCategorie", 1);
        Produit produit = dao.getProduitByNameWithCategorie("produit03");
        Produit produit2 = dao.getProduitByIdWithoutCategorie(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
    }
...
    // -------------- métodos privados
    private Produit findProduitByName(String nom, List<Produit> produits) {
        for (Produit produit : produits) {
            if (produit.getNom().equals(nom)) {
                return produit;
            }
        }
        return null;
    }

    private Categorie findCategorieByName(String nom, List<Categorie> categories) {
        for (Categorie categorie : categories) {
            if (categorie.getNom().equals(nom)) {
                return categorie;
            }
        }
        return null;
    }

    // visualización de un elemento de tipo T
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }

    // Visualización de una lista de elementos de tipo T
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }

    private static void log(String message, int mode) {
        // muestra un 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);
    }

    private static void show(String title, List<String> messages) {
        // título
        System.out.println(String.format("%s : ", title));
        // mensajes
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }

}
  • línea 27: la prueba unitaria se configura mediante la clase [AppConfig], ya presentada en el apartado 11.3.8;
  • líneas 32-33: inyección de una referencia en la capa [DAO];
  • líneas 36-50: inyección de los cinco mapeadores jSON;
  • líneas 60-71: tras vaciar la base de datos (línea 57), se rellena la base de datos con 2 categorías que contienen cada una 5 productos. Este método se ejecuta antes de cada prueba debido a la anotación [@Before] de la línea 52;
  • líneas 75-93: muestra el contenido de la base de datos;
  • líneas 95-101: solicita una categoría con sus productos, identificada por su nombre;
  • líneas 103-109: solicita una categoría sin sus productos, identificada por su nombre;
  • líneas 111-120: solicita un producto con su categoría, identificado por su n.º;
  • líneas 122-130: solicita un producto sin su categoría, identificado por su número;
  • líneas 133-184: métodos privados compartidos por las diferentes pruebas;

Tarea pendiente: ejecuta la prueba. Debe completarse con éxito.


11.3.11. Gestión de registros

Los registros de la aplicación de consola o de la prueba JUnit se configuran mediante el siguiente archivo [logback.xml]:

  

El archivo debe llamarse [logback.xml] y estar en la ruta de clases (Classpath) del proyecto. Para ello, se ha colocado aquí, en la carpeta [src/main/resources], que forma parte de la ruta de clases. Su contenido es el siguiente:


<configuration> 

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- A los codificadores se les asigna por defecto el tipo
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- control del nivel de los registros -->
  <root level="info"> <!-- info, depuración, advertencia -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>
  • línea 12: la etiqueta [<root level="info">] muestra los registros de nivel [info]. En lugar de [info], se puede poner:
    • [debug]: es el nivel de registro más detallado. Se recomienda utilizarlo durante la fase de depuración del proyecto, ya que contiene registros muy interesantes sobre las comunicaciones entre el cliente y el servidor. Es una forma de comprender lo que ocurre «bajo el capó»;
    • [off]: sin registros de log en absoluto;
    • [warn]: un nivel de registro intermedio en el que Spring muestra anomalías que, sin embargo, no son errores. Hay que revisarlas si no se obtiene el resultado esperado;

Tarea a realizar: cambia el nivel de la línea 12 a [debug] y, a continuación, ejecuta la prueba unitaria. Observa la diferencia en los registros.


11.3.12. Generación del archivo Maven del proyecto

Para instalar el archivo del proyecto en el repositorio local de Maven, sigue estos pasos [1-3]:

El archivo se generará con los identificadores que figuran en el archivo [pom.xml]:


    <groupId>istia.st.springdata</groupId>
    <artifactId>intro-spring-data-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

La ubicación del repositorio local de Maven se puede encontrar en la configuración de Eclipse:

 

A continuación, se puede comprobar que el artefacto de Maven se ha instalado correctamente:

 

A partir de ahora, cualquier otro proyecto Maven local podrá utilizar este archivo.