Skip to content

11. [Cours] : Gestion des bases de données relationnelles avec Spring Data

Mots clés : architecture multicouche, Spring, injection de dépendances, API JPA (Java Persistence API), Spring Data.

Nous allons implémenter la couche [DAO] du TD avec [Spring Data], une branche de l'écosystème Spring. [Spring Data] s'appuie sur une couche JPA (Java Persistence API) qui permet à la couche [DAO] de manipuler des objets plutôt que des ordres SQL. Au final, la couche [DAO] ignore qu'elle dialogue avec une base de données. Elle ne connaît que l'interface de la couche [Spring Data].

Nous allons d'abord découvrir [Spring Data] sur deux exemples.

11.1. Support

  • en [1], le dossier [support / chap-11] contient trois projets Eclipse ;
  • en [2], le script SQL permettant de créer la base de données exemple de ce chapitre ;

11.2. Exemple 1

Sur le site de Spring existent de nombreux tutoriels pour démarrer avec Spring [http://spring.io/guides]. Nous allons utiliser l'un d'eux pour introduire Spring Data. Nous utilisons pour cela Spring Tool Suite (STS).

  • en [1], nous importons l'un des tutoriels de [spring.io/guides] ;
  • en [2], on choisit le tutoriel [Accessing Data Jpa] qui montre comment accéder à une base de données avec Spring Data ;
  • en [3], on choisit un projet configuré par Maven ;
  • en [4], le tutoriel peut être délivré sous deux formes : [initial] qui est une version vide qu'on remplit en suivant le tutoriel ou [complete] qui est la version finale du tutoriel. Nous choisissons cette dernière ;
  • en [5], on peut choisir de visualiser le tutoriel dans un navigateur ;
  • en [6], le projet final.

11.2.1. La configuration Maven du projet

Les dépendances Maven du projet sont configurées dans le fichier [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>
        <!-- use UTF-8 for everything -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • lignes 5-9 : définissent un projet Maven parent. C'est lui qui définit l'essentiel des dépendances du projet. Elles peuvent être suffisantes, auquel cas on n'en rajoute pas, ou pas, auquel cas on rajoute les dépendances manquantes ;
  • lignes 12-15 : définissent une dépendance sur [spring-boot-starter-data-jpa]. Cet artifact contient les classes de Spring Data ;
  • lignes 16-19 : définissent une dépendance sur le SGBD H2 qui permet de créer et gérer des bases de données en mémoire.

Regardons les classes amenées par ces dépendances :

Elles sont très nombreuses :

  • certaines appartiennent à l'écosystème Spring (celles commençant par spring) ;
  • d'autres appartiennent à l'écosystème Hibernate (hibernate, jboss) dont on utilise ici l'implémentation JPA ;
  • d'autres sont des bibliothèques de tests (junit, hamcrest) ;
  • d'autres des bibliothèques de logs (log4j, logback, slf4j) ;

Nous allons les garder toutes. Pour une application en production, il faudrait ne garder que celles qui sont nécessaires.

Ligne 26 du fichier [pom.xml] on trouve la ligne :


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

Cette ligne est liée aux lignes suivantes :


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

Lignes 6-9, le plugin [spring-boot-maven-plugin] permet de générer le jar exécutable de l'application. La ligne 26 du fichier [pom.xml] désigne alors la classe exécutable de ce jar.

11.2.2. La couche [JPA]

L'accès à la base de données se fait au travers d'une couche [JPA], Java Persistence API :

  

L'application est basique et gère des clients [Customer]. La classe [Customer] fait partie de la couche [JPA] et est la suivante :


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 client a un identifiant [id], un prénom [firstName] et un nom [lastName]. Chaque instance [Customer] représente une ligne d'une table de la base de données.

  • ligne 8 : annotation JPA qui fait que la persistence des instances [Customer] (Create, Read, Update, Delete) va être gérée par une implémentation JPA. D'après les dépendances Maven, on voit que c'est l'implémentation JPA / Hibernate qui est utilisée ;
  • lignes 11-12 : annotations JPA qui associent le champ [id] à la clé primaire de la table des [Customer]. La ligne 12, indique que l'implémentation JPA utilisera la méthode de génération de clé primaire propre au SGBD utilisé, ici H2 ;

Il n'y a pas d'autres annotations JPA. Des valeurs par défaut seront alors utilisées :

  • la table des [Customer] portera le nom de la classe, ç-à-d [Customer] ;
  • les colonnes de cette table porteront le nom des champs de la classe : [id, firstName, lastName] sachant que la casse n'est pas prise en compte dans le nom d'une colonne de table ;

On notera qu'à aucun moment, l'implémentation JPA utilisée n'est nommée.

11.2.3. La couche [Spring Data]

La classe [CustomerRepository] implémente la couche d'accès à la table [Customer]. Son code est le suivant :

  

package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}

C'est donc une interface et non une classe (ligne 7). Elle étend l'interface [CrudRepository], une interface de Spring Data (ligne 5). Cette interface est paramétrée par deux types : le premier est le type des éléments gérés, ici le type [Customer], le second le type de la clé primaire des éléments gérés, ici un type [Long]. L'interface [CrudRepository] est la suivante :


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();
}

Cette interface définit les opérations CRUD (Create – Read – Update – Delete) qu'on peut faire sur un type JPA T :

  • ligne 8 : la méthode save permet de persister une entité T en base. Elle rend l'entité persistée avec la clé primaire que lui a donnée le SGBD. Elle permet également de mettre à jour une entité T identifiée par sa clé primaire id. Le choix de l'une ou l'autre action se fait selon la valeur de la clé primaire id : si celle-ci vaut null c'est l'opération de persistence qui a lieu, sinon c'est l'opération de mise à jour ;
  • ligne 10 : idem mais pour une liste d'entités ;
  • ligne 12 : la méthode findOne permet de retrouver une entité T identifiée par sa clé primaire id ;
  • ligne 22 : la méthode delete permet de supprimer une entité T identifiée par sa clé primaire id ;
  • lignes 24-28 : des variantes de la méthode [delete] ;
  • ligne 16 : la méthode [findAll] permet de retrouver toutes les entités persistées T ;
  • ligne 18 : idem mais limitées aux entités dont on a passé la liste des identifiants ;

Revenons à l'interface [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 ligne 9 permet de retrouver un [Customer] par son nom [lastName] ;

Et c'est tout pour la couche [DAO]. Il n'y a pas de classe d'implémentation de l'interface précédente. Celle-ci est générée à l'exécution par [Spring Data]. Les méthodes de l'interface [CrudRepository] sont automatiquement implémentées. Pour les méthodes rajoutées dans l'interface [CustomerRepository], ça dépend. Revenons à la définition de [Customer] :


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

La méthode de la ligne 9 est implémentée automatiquement par [Spring Data] parce qu'elle référence le champ [lastName] (ligne 3) de [Customer]. Lorsqu'il rencontre une méthode [findBySomething] dans l'interface à implémenter, Spring Data l'implémente par la requête JPQL (Java Persistence Query Language) suivante :

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

Il faut donc que le type T ait un champ nommé [something]. Ainsi la méthode

List<Customer> findByLastName(String lastName);

va être implémentée par un code ressemblant au suivant :

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

où [em] désigne le contexte de persistance JPA. Cela n'est possible que si la classe [Customer] a un champ nommé [lastName], ce qui est le cas.

En conclusion, dans les cas simples, Spring Data nous permet d'implémenter la couche [DAO] avec une simple interface.

11.2.4. La couche [console]

  

La classe [Application] est la suivante :


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 {
        // save a couple of customers
        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"));

        // fetch all customers
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : repository.findAll()) {
            System.out.println(customer);
        }
        System.out.println();

        // fetch an individual customer by ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();

        // fetch customers by last name
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : repository.findByLastName("Bauer")) {
            System.out.println(bauer);
        }
    }

}
  • ligne 9 : la classe implémente l'interface [CommandLineRunner] qui est une interface [Spring Boot] (ligne 4). Cette interfae n'a qu'une méthode, celle de la ligne 19 ;
  • ligne 8 : @SpringBootApplication est une annotation regroupant plusieurs annotations [Spring Boot] :
    • @Configuration : indique que la classe est une classe de configuration ;
    • @EnableAutoConfiguration : demande à [Spring Boot] de créer lui-même un certain nombre de beans en fonction de diverses propriétés, en particulier le contenu du Classpath du projet. Parce que les bibliothèques Hibernate sont dans le Classpath, le bean [entityManagerFactory] sera implémenté avec Hibernate. Parce que la bibliothèque du SGBD H2 est dans le Classpath, le bean [dataSource] sera implémenté avec H2. Dans le bean [dataSource], on doit définir également l'utilisateur et son mot de passe. Ici Spring Boot utilisera l'administrateur par défaut de H2, sa sans mot de passe. Parce que la bibliothèque [spring-tx] est dans le Classpath, c'est le gestionnaire de transactions de Spring qui sera utilisé ;
    • @EnableWebMvc : si dans le Classpath se trouve la bibliothèque [spring-mvc]. Dans ce cas, une auto-configuration est faite pour l'application web ;
    • @ComponentScan : qui dit à Spring où chercher les autres beans, configurations et services. Ici ils sont cherchés par défaut dans le package contenant la classe taguée, ç-à-d le package [hello]. Ainsi les classes [Customer] et [CustomerRepository] vont-elles être trouvées. Parce que la première a l'annotation [@Entity] elle sera cataloguée comme entité à gérer par Hibernate. Parce que la seconde étend l'interface [CrudRepository] elle sera enregistrée comme bean Spring ;
  • lignes 11-12 : le bean [CustomerRepository] est injecté dans le code de la classe principale ;
  • ligne 15 : la méthode statique [run] de la classe [SpringApplication] du projet Spring Boot est exécutée. Son paramètre est la classe qui a une annotation [Configuration] ou [EnableAutoConfiguration]. Tout ce qui a été expliqué précédemment va alors se dérouler. Le résultat est un contexte d'application Spring, ç-à-d un ensemble de beans gérés par Spring ;

Les opérations qui suivent ne font qu'utiliser les méthodes du bean implémentant l'interface [CustomerRepository]. Les résultats console sont les suivants :

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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
  • lignes 1-8 : le logo du projet Spring Boot ;
  • ligne 9 : la classe [hello.Application] est exécutée ;
  • ligne 10 : [AnnotationConfigApplicationContext] est une classe implémentant l'interface [ApplicationContext] de Spring. C'est un conteneur de beans ;
  • ligne 11 : le bean [entityManagerFactory] est implémenté avec la classe [LocalContainerEntityManagerFactory], une classe de Spring ;
  • ligne 12 : on voit apparaître [Hibernate]. C'est cette implémentation JPA qui a été choisie ;
  • ligne 19 : un dialecte Hibernate est la variante SQL à utiliser avec le SGBD. Ici le dialecte [H2Dialect] montre qu'Hibernate va travailler avec le SGBD H2 ;
  • lignes 21-22 : la base de données est créée. La table [CUSTOMER] est créée. Cela signifie qu'Hibernate a été configuré pour générer les tables à partir des définitions JPA, ici la définition JPA de la classe [Customer] ;
  • lignes 26-30 : résultat de la méthode [findAll] de l'interface ;
  • ligne 34 : résultat de la méthode [findOne] de l'interface ;
  • lignes 38-39 : résultats de la méthode [findByLastName] ;
  • lignes 41 et suivantes : logs de la fermeture du contexte Spring.

11.2.5. Configuration manuelle du projet Spring Data

Nous dupliquons le projet précédent dans le projet [gs-accessing-data-jpa-02] :

  

Dans ce nouveau projet, nous n'allons pas nous reposer sur la configuration automatique faite par Spring Boot. Nous allons la faire manuellement. Cela peut être utile si les configurations par défaut ne nous conviennent pas.

Tout d'abord, nous allons expliciter les dépendances nécessaires dans le fichier [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>
        <!-- H2 Database -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <!-- use UTF-8 for everything -->
        <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>
  • lignes 10-14 : le projet Maven parent dont nous allons utiliser les bibliothèques qu'il définit ;
  • lignes 18-21 : Spring Data utilisé pour accéder à la base de données ;
  • lignes 23-26 : l'implémentation Hibernate de la spécification JPA ;
  • lignes 28-31 : le SGBD H2 ;
  • lignes 33-36 : les bases de données sont souvent utilisées avec des pools de connexions ouvertes qui évitent les ouvertures / fermetures de connexion à répétition. Ici, l'implémentation utilisée est celle de [tomcat-jdbc] ;

Dans le nouveau projet, l'entité [Customer] et l'interface [CustomerRepository] ne changent pas. On va changer la classe [Application] qui va être scindée en deux classes :

  • [Config] qui sera la classe de configuration :
  • [Main] qui sera la classe exécutable ;
  

La classe exécutable [Application] est désormais la suivante :


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) {
        // instanciation contexte Spring
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);

        // save a couple of customers
        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"));

        ...

        // fermeture contexte
        context.close();
    }
}
  • ligne 9 : la classe [Application] n'a plus d'annotations de configuration ;
  • lignes 3-7 : on notera qu'il n'y a plus d'imports de packages [Spring Boot] ;
  • ligne 12 : on instancie les beans Spring. On obtient le contexte de Spring qui contient la référence des beans ainsi créés ;
  • ligne 13 : on demande une référence sur le bean de type [CustomerRepository] ;

La classe [Config] qui configure le projet est la suivante :


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 données H2
    @Bean
    public DataSource dataSource() {
        // source de données TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration accès JDBC
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        // une connexion ouverte initialement
        dataSource.setInitialSize(1);
        // résultat
        return dataSource;
    }

    // le provider 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();
    }

    // Transaction manager
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

}
  • ligne 17 : l'annotation [@EnableTransactionManagement] indique que les méthodes des interfaces [CrudRepository] doivent se dérouler à l'intérieur d'une transaction. Elle a été mise en commentaires car c'est le cas par défaut ;
  • ligne 18 : l'annotation [@EnableJpaRepositories] permet de désigner les dossiers où se trouvent les interfaces Spring Data [CrudRepository]. Ces interfaces vont devenir des composants Spring et être disponibles dans son contexte ;
  • ligne 19 : l'annotation [@Configuration] fait de la classe [Config] une classe de configuration Spring ;
  • ligne 20 : l'annotation [@ComponentScan] permet de lister les dossiers où les composants Spring doivent être recherchés. Les composants Spring sont des classes taguées avec des annotations Spring telles que @Service, @Component, @Controller, ... Ici, il n'y en a pas d'autres que ceux qui sont définis au sein de la classe [AppConfig], aussi l'annotation a-t-elle été mise en commentaires ;
  • lignes 24-37 : définissent la source de données, la base de données H2. C'est l'annotation @Bean de la ligne 25 qui fait de l'objet créé par cette méthode un composant géré par Spring. Le nom de la méthode peut être ici quelconque. Cependant elle doit être appelée [dataSource] si l'EntityManagerFactory de la ligne 51 est absent et défini par autoconfiguration ;
  • ligne 30 : la base de données s'appellera [demo] et sera générée dans le dossier du projet ;
  • lignes 40-47 : définissent l'implémentation JPA utilisée, ici une implémentation Hibernate. Le nom de la méthode peut être ici quelconque ;
  • ligne 43 : pas de logs SQL ;
  • ligne 44 : la base de données sera créée si elle n'existe pas ;
  • lignes 50-58 : définissent l'EntityManagerFactory qui va gérer la persistance JPA. La méthode doit s'appeler obligatoirement [entityManagerFactory] ;
  • ligne 51 : la méthode reçoit deux paramètres ayant le type des deux beans définis précédemment. Ceux-ci seront alors construits puis injectés par Spring comme paramètres de la méthode ;
  • ligne 53 : fixe l'implémentation JPA utilisée ;
  • ligne 54 : fixe les dossiers où trouver les entités JPA ;
  • ligne 55 : fixe la source de données à gérer ;
  • lignes 61-66 : le gestionnaire de transactions. La méthode doit s'appeler obligatoirement [transactionManager]. Elle reçoit pour paramètre le bean des lignes 51-58 ;
  • ligne 64 : le gestionnaire de transactions est associé à l'EntityManagerFactory ;

Les méthodes précédentes peuvent être définies dans un ordre quelconque.

L'exécution du projet donne les mêmes résultats. Un nouveau fichier apparaît dans le dossier du projet, celui de la base de données H2 :

  

11.2.6. Création d'une archive exécutable

Pour créer une archive exécutable du projet, on peut procéder ainsi :

  • en [1] : on crée une configuration d'exécution ;
  • en [2] : de type [Java Application]
  • en [3] : désigne le projet à exécuter (utiliser le bouton Browse) ;
  • en [4] : désigne la classe à exécuter ;
  • en [5] : le nom de la configuration d'exécution – peut être quelconque ;
  • en [6] : on exporte le projet ;
  • en [7] : sous la forme d'une archive JAR exécutable ;
  • en [8] : indique le chemin et le nom du fichier exécutable à créer ;
  • en [9] : le nom de la configuration d'exécution créée en [5] ;
  • en [10], l'archive créée ;

Ceci fait, on ouvre une console dans le dossier contenant l'archive exécutable :

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

L'archive est exécutée de la façon suivante :


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

Les résultats obtenus dans la console sont les suivants :

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 for further details.
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. Exemple 2

11.3.1. Introduction

Nous allons reprendre l'exemple de la table de produits que nous avons utilisée pour introduire l'API JDBC et créer l'architecture suivante :

La base de données [dbintrospringjpa] a deux tables [PRODUITS] et [CATEGORIES]. La table [CATEGORIES] est la suivante :

 
  • [ID] : clé primaire en mode AUTO_INCREMENT ;
  • [VERSION] : n° de version de l'enregistrement ;
  • [NOM] : nom de la catégorie - unique ;

La table [PRODUITS] est la suivante :

 
  • [ID] : clé primaire en mode AUTO_INCREMENT ;
  • [VERSION] : n° de version de l'enregistrement ;
  • [NOM] : nom d'un produit - unique ;
  • [ID_CATEGORIE] : n° de sa catégorie - clé étrangère sur le champ [CATEGORIES.ID] ;
  • [PRIX] : son prix ;
  • [DESCRIPTION] : une description du produit ;

Travail à faire : créez la base de données [dbintrospringdata] avec le script SQL [dbintrospringdata.sql] du support :


11.3.2. Création du projet Maven

Pour créer un squelette de projet Spring Data, on peut procéder de la façon suivante :

  • en [1], on crée un nouveau projet ;
  • en [2] : de type [Spring Starter Project] ;
  • le projet généré sera un projet Maven. En [3], on indique le nom du groupe du projet ;
  • en [4] : on indique le nom de l'artifact (un jar ici) qui sera créé par construction du projet ;
  • en [5] : le nom Eclipse du projet – peut être quelconque (n'a pas à être identique à [4]) ;
  • en [7] : on indique qu'on va créer un projet ayant une couche [JPA] avec le SGBD MySQL. Les dépendances nécessaires à un tel projet vont alors être incluses dans le fichier [pom.xml] ;
  • en [8], donner le nom du dossier du projet ;
  • en [9], terminer l'assistant ;
  • en [10] : le projet créé ;

Le fichier [pom.xml] intègre les dépendances nécessaires à un projet 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/> <!-- lookup parent from repository -->
    </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>
  • lignes 14-19 : le projet Maven parent - définit un grand nombre de bibliothèques avec leurs versions - on utilise ces bibliothèques comme dépendances Maven sans préciser leur version ;
  • lignes 28-31 : la dépendance nécessaire à JPA – va inclure [Spring Data] ;
  • lignes 32-36 : la dépendance sur le pilote JDBC de MySQL ;
  • lignes 37-41 : les dépendances nécessaires aux tests JUnit intégrés avec Spring ;

La classe exécutable [Application] ne fait rien mais est préconfigurée :


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);
    }
}
  • l'annotation [@SpringBootApplication] fait de la classe une classe d'auto-configuration du projet ;

La classe de tests [ApplicationTests] ne fait rien mais est préconfigurée :


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() {
    }

}
  • ligne 9 : l'annotation [@SpringApplicationConfiguration] permet d'exploiter le fichier de configuration [Application]. La classe de test bénéficiera ainsi de tous les beans qui seront définis par ce fichier ;
  • ligne 8 : l'annotation [@RunWith] permet l'intégration de Spring avec JUnit : la classe va pouvoir être exécutée comme un test JUnit. [@RunWith] est une annotation JUnit (ligne 4) alors que la classe [SpringJUnit4ClassRunner] est une classe Spring (ligne 6) ;

Maintenant que nous avons un squelette d'application JPA, nous pouvons le compléter pour écrire le projet de la couche de persistance associée à la base de données des produits.

11.3.3. Le projet Eclipse

Nous faisons évoluer le projet précédent de la façon suivante :

  
  • [AppConfig.java] : la classe de configuration du projet Spring ;
  • [Main.java] : la classe exécutable du projet ;
  • [IDao.java] : l'interface de la couche [DAO] ;
  • [Dao.java] : la classe d'implémentation de la couche [DAO] ;
  • [AbstractEntity.java] : la classe parent des classes [Produit] et [Categorie] ;
  • [Produit.java] : classe associée à une ligne de la table [PRODUITS] de la base de données ;
  • [Categorie.java] : classe associée à une ligne de la table [CATEGORIES] de la base de données ;
  • [ProduitsRepository] : l'interface Spring Data d'accès à la table [PRODUITS] ;
  • [CategoriesRepository] : l'interface Spring Data d'accès à la table [CATEGORIES] ;
  • [pom.xml] : le fichier de configuration du projet Maven ;

Ce projet implémente l'architecture suivante :

La couche [DAO] ne voit que la couche implémentée par [Spring Data].

11.3.4. Configuration Maven

Le fichier [pom.xml] du projet Maven est le suivant :


<?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 Database -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- bibliothèque 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>
        <!-- Spring Boot Test -->
        <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>
        <!-- bibliothèque de logs -->
        <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>

Cette configuration est celle utilisée et expliquée au paragraphe 11.2.5. Nous y ajoutons les bibliothèques suivantes :

  • lignes 42-49 : une bibliothèque jSON utilisée par la méthode [toString] de la classe [Produit] ;
  • lignes 51-55 : la bibliothèque [Google Guava] qui amène des méthodes utilitaires pour gérer des collections d'éléments. Elle sera utilisée par la classe [Dao] qui implémente la couche [DAO] ;
  • lignes 56-67 : les bibliothèques nécessaires aux tests JUnit ;
  • lignes 69-72 : une bibliothèque de logs ;
  • lignes 81-86 : les plugins Maven nécessaires au projet ;

11.3.5. Les entités de la couche [JPA]

Couche[Spring Data]Pilote[JDBC]Couche[JPA]Couche[console]Couche[DAO]Spring 4SGBD

  

11.3.5.1. La classe [AbstractEntity]

La classe [AbstractEntity] est la suivante :


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 {
    // propriétés
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    protected Long id;
    @Version
    @Column(name = "VERSION")
    protected Long version;

    // constructeurs
    public AbstractEntity() {

    }

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

    // redéfinition [equals] et [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();
    }

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

Cette classe a pour objectif de fournir une classe mère aux entités JPA en encapsulant à un unique endroit les propriétés [id, version] (lignes 19, 22) communes aux deux entités [Produit] et [Categorie] liées à la base de données. Ces propriétés sont liées aux colonnes [ID, VERSION] des tables (lignes 18, 21).

  • ligne 13 : l'annotation [@MappedSuperclass] indique que la classe est une classe parent d'entités JPA ;
  • ligne 16 : l'annotation [@Id] indique que le champ [id] (il pourrait avoir un autre nom) est associé à la clé primaire d'une table ;
  • ligne 17 : l'annotation [@GeneratedValue(strategy=GenerationType.IDENTITY)] fixe le mode de génération des clés primaires. Le mode [GenerationType.IDENTITY] va utiliser avec MySQL le mode [AUTO_INCREMENT]. Avec un autre SGBD, ce mode utiliserait une autre méthode. L'intérêt est que le développeur n'a pas à s'en préoccuper et que son code reste valide quelque soit le SGBD utilisé ;
  • ligne 18 : l'annotation [@Column] indique la colonne associée au champ. Lorsque cette annotation n'est pas présente, JPA assume que la colonne porte le même nom que le champ. C'est le cas ici. On n'aurait donc pu ne pas mettre cette annotation ;
  • ligne 20 : l'annotation [@Version] indique que le champ [version] est associé à une colonne de versioning. L'implémentation JPA va incrémenter ce n° de version à chaque fois que l'entité sera modifiée. Ce n° sert à empêcher la mise à jour simultanée de l'entité par deux utilisateur différents : deux utilisateurs U1 et U2 lisent l'entité E avec un n° de version égal à V1. U1 modifie E et persiste cette modification en base : le n° de version passe alors à V1+1. U2 modifie E à son tour et persiste cette modification en base : il recevra une exception car il possède une version (V1) différente de celle en base (V1+1) ;
  • lignes 35-52 : redéfinition des méthodes [hashCode] et [equals]. Par défaut, [obj1.equals(obj2)] vaut true si [obj1==obj2], ç-à-d si ob1 et obj2 sont deux pointeurs égaux. Si on veut comparer les objets pointés plutôt que les pointeurs eux-mêmes, il faut redéfinir la méthode [equals] et la méthode [hashCode]. Celle-ci doit rendre la même valeur pour deux objets que la méthode [equals] dit égaux ;
  • lignes 42-51 : deux objets de type [AbstractEntity] ou dérivés seront dits égaux si leurs clés primaires [id] sont égales ;
  • lignes 35-38 : la méthode [hashCode] rend bien la même valeur pour deux objets [AbstractEntity] identiques et donc ayant la même clé primaire [id] ;
  • lignes 55-63 : la méthode [toString] rend la chaîne jSon de l'objet [this]. Si cet objet désigne une classe fille, cette méthode rendra alors la chaîne jSON de la classe fille. Cela nous dispense de créer une méthode [toString] dans les classes filles ;

11.3.5.2. L'entité JPA [Produit]

La classe [Produit] est une entité JPA associée à une ligne de la table [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 {

    // propriétés
    @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 catégorie
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CATEGORIE_ID")    
    private Categorie categorie;

    // constructeurs
    public Produit() {

    }

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

    // getters et setters
...
}
  • ligne 12 : l'annotation [@Entity] fait de la classe [Produit] une entité gérée par la couche [JPA] ;
  • ligne 13 : l'annotation [@Table(name = "PRODUITS")] indique que la classe [Produit] est l'image objet d'une ligne de la table [PRODUITS] de la base de données ;
  • ligne 14 : le nom du filtre jSON à appliquer à l'entité. Nous allons voir que la propriété [categorie] de la ligne 13 n'est pas toujours disponible. Il faut alors l'exclure de la représentation jSON de l'objet. Pour cela nous avons besoin d'un filtre. C'est donc dans un filtre nommé [jsonFilterCategorie] que nous indiquerons si on veut ou non la propriété [categorie] ;
  • ligne 18 : l'annotation [@Column] associe le champ [nom] à la colonne [NOM] de la table [PRODUITS]. Lorsque le champ porte le même nom que la colonne associée, l'annotation [@Column] peut être omise. Ce serait le cas ici ;
  • lignes 31-33 : la catégorie du produit ;
  • ligne 31 : l'annotation [@ManyToOne] indique que la colonne de l'annotation de la ligne 32 [@JoinColumn(name = "CATEGORIE_ID")] est clé étrangère de la table [PRODUITS] de l'entité [Produit] sur la table [CATEGORIES] associée à l'entité de la ligne 33. Cette annotation doit annoter une entité JPA. Ainsi la classe de la ligne 33 doit être une entité JPA ;
  • ligne 31 : l'annotation [fetch = FetchType.LAZY] demande à ce que lorsqu'on ramène un produit de la table [PRODUITS], sa catégorie (ligne 33) ne soit pas ramenée immédiatement (lazy loading). Elle est alors obtenue lors du premier appel à la méthode [getCategorie]. Cet attribut n'est pas contraignant. L'implémentation JPA utilisée a le droit de l'ignorer. C'est parce que la propriété [categorie] peut être présente ou non que nous avons introduit le filtre jSON de la ligne 14. Les implémentations JPA existantes (Hibernate, Eclipselink, OpenJPA) ne gèrent pas cette annotation de la même façon. Hibernate enrichit la méthode [getCategorie] initiale (qui se contente de rendre le champ categorie) par un appel au SGBD pour aller chercher la catégorie. Pour que cela soit possible, il faut que la connexion au SGBD utilisée initialement pour obtenir le produit soit encore ouverte sinon on a une exception.

11.3.5.3. L'entité JPA [Categorie]

La classe [Categorie] est une entité JPA associée à une ligne de la table [CATEGORIES] :

 

Son code est le suivant :


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 {

    // propriétés
    @Column(name = "NOM")
    private String nom;

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

    // constructeurs
    public Categorie() {

    }

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

    // méthodes
    public void addProduit(Produit produit) {
        // on ajoute le produit
        produits.add(produit);
        // on fixe sa catégorie
        produit.setCategorie(this);
    }

    // getters et setters
...
}
  • lignes 21-22 : le nom de la catégorie ;
  • lignes 25-26 : les produits de cette catégorie ;
  • ligne 25 : l'annotation [@OneToMany] est la relation inverse de la relation [@ManyToOne] que nous avons rencontrée dans l'entité [Produit]. L'attribut [mappedBy = "categorie"] indique le champ de l'entité [Produit] annoté par la relation inverse [@ManyToOne]. L'attribut [cascade = { CascadeType.ALL }] demande à ce que les opérations (persist, merge, remove) faites sur une @Entity [Categorie] soient cascadées sur les [produits] de la ligne 26. On peut indiquer des cascades partielles avec les constantes [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE] ;
  • ligne 25 : l'attribut [fetch = FetchType.LAZY] demande à ce que, lorsqu'on ramène une catégorie de la table [CATEGORIES], ses produits ne soient pas immédiatement ramenés. Ils le seront lors du premier appel à la méthode [getProduits]. Les implémentations JPA existantes (Hibernate, Eclipselink, OpenJPA) ne gèrent pas cette annotation de la même façon. Hibernate enrichit la méthode [getProduits] initiale (qui se contente de rendre le champ produits) par un appel au SGBD pour aller chercher les produits de la catégorie. Pour que cela soit possible, il faut que la connexion au SGBD utilisée initialement pour obtenir la catégorie soit encore ouverte. Cet attribut est contraignant. L'implémentation JPA ne peut l'ignorer. Parce que la propriété [produits] peut être ou non initialisée, nous avons introduit le filtre jSON de la ligne 17 qui nous permettra d'indiquer si on veut ou non cette propriété ;
  • ligne 26 : le type [Set] est une interface. Le type [HashSet] est une classe implémentant cette interface. Elle implémente une collection d'éléments appelée ensemble. Un ensemble ne peut contenir deux objets identiques. Ici les objets sont de type [Produit]. Ainsi dans l'ensemble, on ne pourra avoir deux objets identiques. Comme la méthode [equals] de la classe parent [AbstractEntity] a été redéfinie pour dire que deux produits sont identiques s'ils ont la même clé primaire, alors le champ [produits] ne pourra contenir deux produits de même clé primaire ;
  • lignes 38-43 : la méthode [addProduit] permet d'ajouter un produit à la catégorie ;

11.3.6. La couche [Spring Data]

Couche[Spring Data]Pilote[JDBC]Couche[JPA]Couche[console]Couche[DAO]Spring 4SGBD

  

L'interface [CategoriesRepository] gère les accès à la table [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> {

    // categorie avec ses produits
    @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);

    // une catégorie sans ses produits désignée par son nom
    public Categorie findByNom(String nom);
}
  • ligne 8 : l'interface [CrudRepository] a été utilisée et expliquée paragraphe 11.2.3 . On rappelle que :
    • le 1er type de l'interface est l'entité JPA gérée pour des accès CRUD (findOne, findAll, save, delete, deleteAll),
    • le second type est celui de la clé primaire de l'entité JPA, ici un entier [Long] ;
  • ligne 12 : la méthode de la ligne 12 est implémentée par la requête JPQL (Java Persistence Query Language) de la ligne 11. Celle-ci requête des entités JPA. Dans une telle requête :
    • les tables sont remplacées par leurs entités JPA associées ;
    • les colonnes sont remplacées par des champs des entités JPA utilisées dans la requête ;
  • ligne 11 : la requête JPQL ramène une catégorie avec ses produits. On se rappelle que dans l'entité [Categorie], le champ [produits] avait l'attribut [fetch = FetchType.LAZY] (lazy loading). Dans la requête JPQL, on force le chargement des produits avec le mot clé [fetch]. Le paramètre ?1 de la requête sera remplacé à l'exécution par la valeur du 1er paramètre de la méthode de la ligne 12, donc par le paramètre [Long id] ;
  • lignes 14-15 : une méthode analogue pour une catégorie identifiée par son nom ;
  • ligne 18 : la méthode [findByNom] sera automatiquement implémentée par [Spring Data] car le type [Category] a un champ [nom] ;

L'interface [ProduitsRepository] gère les accès à la table [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 produit avec sa catégorie
    @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 produit sans sa catégorie désigné par son nom
    public Produit findByNom(String nom);
}

Les explications sont les mêmes que pour l'interface [CategoriesRepository].

Ces interfaces vont être implémentées par des classes générées par [Spring Data] au moment de l'exécution du projet. On appelle de telles classes des [proxy]. Par défaut, les méthodes de la classe d'implémentation s'exécutent dans une transaction. Le fait que ces interfaces étendent la classe [CrudRepository] font d'elles des composants Spring.

11.3.7. La couche [DAO]

Couche[Spring Data]Pilote[JDBC]Couche[JPA]Couche[console]Couche[DAO]Spring 4SGBD

  

L'interface [IDao] de la couche [DAO] est la suivante :


package spring.data.dao;

import java.util.List;

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

public interface IDao {

    // insertion d'une liste de produits
    public List<Produit> addProduits(List<Produit> produits);

    // suppression de tous les produits
    public void deleteAllProduits();

    // mise à jour d'une liste de produits
    public List<Produit> updateProduits(List<Produit> produits);

    // obtention de tous les produits
    public List<Produit> getAllProduits();

    // insertion d'une liste de categories
    public List<Categorie> addCategories(List<Categorie> categories);

    // suppression de tous les categories
    public void deleteAllCategories();

    // mise à jour d'une liste de categories
    public List<Categorie> updateCategories(List<Categorie> categories);

    // obtention de tous les categories
    public List<Categorie> getAllCategories();

    // un produit particulier avec ou non sa catégorie
    public Produit getProduitByIdWithoutCategorie(Long idProduit);

    public Produit getProduitByIdWithCategorie(Long idProduit);

    public Produit getProduitByNameWithCategorie(String nom);

    public Produit getProduitByNameWithoutCategorie(String nom);

    // une catégorie particulière avec ou pas ses produits
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie);

    public Categorie getCategorieByIdWithProduits(Long idCategorie);

    public Categorie getCategorieByNameWithProduits(String nom);

    public Categorie getCategorieByNameWithoutProduits(String nom);
}

On a adopté ici la règle que toute méthode qui modifie les objets qu'elle a en paramètres d'entrée doit alors les rendre dans son résultat. La raison de cette règle a été expliquée au paragraphe 4.2 : elle permet à une couche et à son client d'être dans deux JVM séparées et donc de travailler en client / serveur.

L'implémentation [Dao] de cette interface est la suivante :


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));
        }
    }

}
  • ligne 16 : l'annotation [@Component] fait de la classe [Dao] un composant Spring ;
  • lignes 19-23 : injection des références sur les deux interfaces [CrudRepository] de [Spring Data]. Cette injection aura lieu lors de l'instantiation des objets Spring, en général au début de l'exécution du projet Spring ;
  • on notera lignes 28 et 46 que la méthode [save] de l'interface [produitsRepository] est utilisée aussi bien pour l'insertion que pour la mise à jour de produits. [Spring Data] utilise la clé primaire du produit pour savoir s'il doit faire une insertion ou une mise à jour. Si la clé primaire vaut [null], ce sera une insertion sinon ce sera une mise à jour ;
  • ligne 82 : on utilise la méthode [Lists.newArrayList] de la bibliothèque Guava pour obtenir une liste de produits. La méthode [produitsRepository.findAll()] rend un type [Iterable<Produit>] ;
  • ligne 28 : la méthode [produitsRepository.save(produits)] rend un type [Iterable<Produit>]. Il en est de même pour les autres opérations [save] de la classe ;

Dans la classe [Dao] ci-dessus, les exceptions qui peuvent se produire sont encapsulées dans le type [DaoException] suivant :


package spring.data.dao;

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

// classe d'exception pour l'application Elections
// l'exception est non contrôlée

public class DaoException extends RuntimeException implements Serializable {

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

    // champs locaux
    private int code;
    private List<String> erreurs;

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

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

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

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

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

    // liste des messages d'erreur d'une exception
    private List<String> getErreursForException(Throwable th) {
        // on récupère la liste des messages d'erreur de l'exception
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // on récupère le message seulement s'il est !=null et non blanc
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // cause suivante
            cause = cause.getCause();
        }
        return erreurs;
    }

    // getters et setters
...
}
  • ligne 10 : la classe dérive de la classe [RuntimeException] et est donc une exception non contrôlée ;
  • ligne 16 : un code d'erreur ;
  • ligne 17 : une liste de messages d'erreur, ceux associés à la pile des exceptions qui ont provoqué la [DaoException] ;
  • lignes 59-76 : la méthode privée [getMessagesForException] permet d'obtenir la liste des messages d'erreurs associées aux exception de la pile d'exceptions. Il est en effet possible d'empiler les exceptions avec les constructeurs suivants de la classe Exception :
    • Exception(String message, Throwable cause) : crée une exception avec un message et l'exception qu'on veut encapsuler ;
    • Exception(Throwable cause) : crée une exception avec l'exception qu'on veut encapsuler ;

Le type [Throwable] est la classe parent de la classe [Exception]. Si les constructeurs précédents sont exécutés de façon répétée, l'exception finale contient alors plusieurs exceptions. On dit qu'on a une pile d'exceptions.

  • la dernière cause d'une exception e1 est obtenue par l'expression [e1.getCause()] ;
  • l'avant-dernière cause d'une exception e1 est obtenue par l'expression [e1.getCause().getCause()] ;
  • on poursuit ainsi jusqu'à obtenir [getCause()==null] ;

11.3.8. Configuration du projet Spring

  

La classe [DaoConfig] configure la couche [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 source de données [tomcat-jdbc]
    @Bean
    public DataSource dataSource() {
        // source de données TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration accès JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // une connexion ouverte initialement
        dataSource.setInitialSize(1);
        // résultat
        return dataSource;
    }

    // le provider 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();
    }

    // Transaction manager
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

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

}

Une configuration analogue a été rencontrée et expliquée paragraphe 11.2.5. Nous y avons ajouté les annotations Spring suivantes :

  • ligne 17 : l'annotation [@EnableJpaRepositories] sert à indiquer les packages où se trouvent les interfaces [CrudRepository] de [Spring Data] ;
  • ligne 18 : la classe est une classe de configuration Spring. Cette information est importante. Si on l'enlève le projet fonctionne. Mais plus loin dans le document, lorsqu'on construira des projets qui s'appuient sur celui-ci, alors certains d'entre-eux ne fonctionnent plus si l'annotation de la ligne 18 est enlevée ;
  • ligne 19 : l'annotation [@ComponentScan] indique les packages où se trouvent les objets Spring. Ce sont les classes annotées par [@Component, @Service, @Controller, ...]. Ici le composant Spring [Dao] va être trouvé et instancié ;
  • lignes 73-76 : nous avons défini un bean qui représente le tableau des packages à scanner pour trouver des entités JPA. Cela permettra à un projet qui importe la classe [DaoConfig] de redéfinir ce bean et de changer ainsi les packages scannés ligne 59. Plus loin dans le document, nous allons rencontrer cette problématique ;

La classe [AppConfig] configure l'ensemble du projet :


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 {
    // filtres jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }

    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // mappeur jSON
        ObjectMapper mapper = new ObjectMapper();
        // filtres
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // résultat
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // mappeur jSON
        ObjectMapper mapper = new ObjectMapper();
        // filtres
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // résultat
        return mapper;
    }

    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // mappeur jSON
        ObjectMapper mapper = new ObjectMapper();
        // filtres
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // résultat
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // mappeur jSON
        ObjectMapper mapper = new ObjectMapper();
        // filtres
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // résultat
        return mapper;
    }
}
  • ligne 11 : la classe est une classe de configuration Spring ;
  • ligne 12 : qui importe les beans définis par la classe [DaoConfig] que nous venons de voir ;
  • la couche [console] utilise des mappeurs jSON qui sont définis ici ;
  • lignes 14-64 : définissent cinq mappeurs jSON ;
  • lignes 15-18 : le filtre jSON [jsonMapper] n'a pas de filtres ;
  • lignes 20-30 : le filtre jSON [jsonMapperCategorieWithProduits] permet de sérialiser / désérialiser un objet [Categorie] avec ses produits ;
  • lignes 32-42 : le filtre jSON [jsonMapperProduitWithCategorie] permet de sérialiser / désérialiser un objet [Produit] avec sa catégorie ;
  • lignes 43-53 : le filtre jSON [jsonMapperCategorieWithoutProduits] permet de sérialiser / désérialiser un objet [Categorie] sans ses produits ;
  • lignes 55-64 : le filtre jSON [jsonMapperProduitWithoutCategorie] permet de sérialiser / désérialiser un objet [Produit] sans sa catégorie ;

On notera que l'on construit un filtre jSON pour une entité T, on doit configurer non seulement le filtre de l'entité T mais également ceux des entités Ti qu'elle-même peut contenir.

11.3.9. La couche [console]

Couche[Spring Data]Pilote[JDBC]Couche[JPA]Couche[console]Couche[DAO]Spring 4SGBD

  

La classe [Main] est la suivante :


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 {
            // instanciation contexte 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);
            // --------------------------------------------------------------------------------------
            // on vide la base de données
            log("Vidage de la base de données", 1);
            // on vide la table [CATEGORIES] - par cascade la table [PRODUITS] va être vidée
            dao.deleteAllCategories();
            // --------------------------------------------------------------------------------------
            log("Remplissage de la base", 1);
            // on remplit les tables
            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);
            }
            // ajout de la catégorie - par cascade les produits vont eux aussi être insérés
            dao.addCategories(categories);
            // --------------------------------------------------------------------------------------
            log("Affichage de la base", 1);
            // liste des catégories
            log("Liste des catégories", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            // liste des produits
            log("Liste des produits", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
            // catégorie 1 avec ses produits
            Categorie categorie = dao.getCategorieByNameWithProduits("categorie1");
            log("Catégorie 1 avec ses produits", 2);
            affiche(categorie, jsonMapperCategorieWithProduits);
            // le produit [produit14] avec sa catégorie
            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);
            // on vide la table [CATEGORIES] - par cascade la table [PRODUITS] va être vidée
            dao.deleteAllCategories();
            // affichage de la base
            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);
            // on fait l'insertion
            categorie = new Categorie("cat1");
            categorie.addProduit(new Produit("x", 1.0, ""));
            categorie.addProduit(new Produit("x", 1.0, ""));
            // ajout de la catégorie - par cascade les produits vont eux aussi être insérés
            try {
                dao.addCategories(Lists.newArrayList(categorie));
            } catch (DaoException e) {
                System.out.println(e);
            }
            // vérification
            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) {
                // fini
                context.close();
            }
        }
        System.out.println("Travail terminé");
    }

    // affichage d'un élément de type T
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }

    // affichage d'une liste d'éléments de type 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) {
        // affiche message
        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);
    }
}
  • ligne 25 : instanciation des beans Spring à partir de la classe de configuration [AppConfig] ;
  • lignes 26-33 : récupération des références sur les mappeurs jSON. On utilise la signature suivante de la méthode [ApplicationContext].getBean :
    • [ApplicationContext].getBean(String id, Class classe) : qu'on utilise lorsqu'il y a plusieurs beans ayant le type [classe]. Dans ce cas, on précise l'identifiant du bean demandé. Si celui-ci a été défini avec l'annotation [@Bean], son identifiant est le nom de la méthode annotée. S'il a été défini avec l'annotation [@Bean(« identifiant »], son identifiant est celui indiqué dans l'annotation ;
  • ligne 34 : récupération d'une référence sur la couche [DAO] ;
  • lignes 37-39 : vidage de la base de données. On vide la table des catégories (ligne 39). Parce qu'on a écrit :

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

lorsqu'une catégorie est supprimée, tous les produits qui lui sont liés le sont aussi ;

  • lignes 43-53 : remplissage de la table avec 2 catégories de 5 produits chacune. Ligne 50, l'insertion des deux catégories va provoquer en même temps l'insertion de leurs produits, toujours parce qu'on a écrit [cascade = { CascadeType.ALL }] ;
  • ligne 58 : on affiche les catégories. On utilise le mappeur jSON [jsonMapperCategorieWithoutProduits] pour afficher les catégories sans leurs produits. En effet la méthode [dao.getAllCategories()] rend les catégories sans leurs produits (lazy loading) ;
  • ligne 61 : on affiche les produits sans leur catégorie. En effet la méthode [dao.getAllProduits()] rend les produits sans leur catégorie (lazy loading) ;
  • lignes 63-65 : affichent la catégorie de nom [categorie1] avec ses produits (eager loading) ;
  • lignes 67-69 : affiche un produit avec sa catégorie ;
  • lignes 71-81 : on augmente de 10% tous les prix des produits de la catégorie [categorie1] ;
  • lignes 91-101 : on ajoute une catégorie avec deux produits de même nom. Or dans la table [PRODUITS] on a une contrainte d'unicité sur la colonne [NOM]. L'insertion du 2ième produit va donc être rejetée et une exception lancée. Or la méthode [dao.addProduits] s'exécute dans une transaction. Le fait que la seconde insertion échoue doit donc également annuler l'insertion du premier produit ainsi que celle de leur catégorie [cat1]. C'est ce qu'on veut vérifier ;
  • lignes 119-121 : une méthode générique capable d'afficher la chaîne jSON de tout élément de type T. La sérialisation jSON est contrôlée par le mappeur passé en paramètre ;
  • lignes 124-128 : une méthode analogue, cette-fois pour une liste d'éléments de type T ;

L'exécution de la classe [Main] donne les résultats suivants (hors logs 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é
  • lignes 4-17 : les catégories et produits insérés dans la table ;
  • lignes 18-19 : une catégorie avec ses produits ;
  • lignes 20-21 : un produit avec sa catégorie ;
  • lignes 22-26 : mise à jour du prix de certains produits. Ligne 24, on voit que les prix ont bien augmenté de 10% ;
  • lignes 27-36 : ajout de la catégorie [cat1] avec deux produits de même nom. On voit que la table est la même avant (lignes 28-29) et après ajout (lignes 35-36) montrant par là que toutes les insertions de la transaction ont bien été annulées ;
  • lignes 31-34 : l'exception qui s'est produite lors de l'insertion du second produit et qui a fait échouer toute la transaction ;

11.3.10. Le test unitaire JUnit

  

La classe [Test01] est la suivante :


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 {

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

    // filtres 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() {
        // on nettoie la base avant chaque test
        log("Vidage de la base de données", 1);
        // on vide la table [CATEGORIES] - par cascade la table [PRODUITS] va être vidée
        dao.deleteAllCategories();
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // on remplit les tables
        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);
        }
        // ajout de la catégorie - par cascade les produits vont eux aussi être insérés
        categories = dao.addCategories(categories);
    }

    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // liste des catégories
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories();
        affiche(categories, jsonMapperCategorieWithoutProduits);
        // liste des produits
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits();
        affiche(produits, jsonMapperProduitWithoutCategorie);
        // quelques vérifications
        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éthodes privées
    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;
    }

    // affichage d'un élément de type T
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }

    // affichage d'une liste d'éléments de type 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) {
        // affiche message
        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) {
        // titre
        System.out.println(String.format("%s : ", title));
        // messages
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }

}
  • ligne 27 : le test unitaire est configuré par la classe [AppConfig] déjà présentée au paragraphe 11.3.8 ;
  • lignes 32-33 : injection d'une référence sur la couche [DAO] ;
  • lignes 36-50 : injection des cinq mappeurs jSON ;
  • lignes 60-71 : après avoir vidé la base (ligne 57), on remplit la base de données avec 2 catégories contenant chacune 5 produits. Cette méthode est exécutée avant chaque test à cause de l'annotation [@Before] de la ligne 52 ;
  • lignes 75-93 : affiche le contenu de la base ;
  • lignes 95-101 : demande une catégorie avec ses produits, catégorie identifiée par son nom ;
  • lignes 103-109 : demande une catégorie sans ses produits, catégorie identifiée par son nom ;
  • lignes 111-120 : demande un produit avec sa catégorie, produit identifié par son n° ;
  • lignes 122-130 : demande un produit sans sa catégorie, produit identifié par son n° ;
  • lignes 133-184 : des méthodes privées partagées par les différents tests ;

Travail à faire : exécutez le test. Il doit réussir.


11.3.11. Gestion des logs

Les logs de l'application console ou du test JUnit sont configurés par le fichier [logback.xml] suivant :

  

Le fichier doit s'appeler [logback.xml] et être dans le Classpath du projet. Pour cela, il a été placé ici dans le dossier [src/main/resources] qui fait partie du Classpath. Son contenu est le suivant :


<configuration> 

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- encoders are  by default assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- contrôle niveau des logs -->
  <root level="info"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>
  • ligne 12 : la balise [<root level="info">] affiche les logs de niveau [info]. A la place de [info], on peut mettre :
    • [debug] : c'est le niveau le plus détaillé de logs. Il est conseillé de l'utiliser pendant la phase de débogage du projet car il y a des logs très intéressants sur les échanges client / serveur. C'est une façon de comprendre ce qui se passe 'sous le capot' ;
    • [off] : pas de logs du tout ;
    • [warn] : un niveau de logs intermédiaire où Spring affiche des anomalies qui ne pas pour autant des erreurs. Il faut les regarder si on n'obtient pas le résultat escompté ;

Travail à faire : passez le niveau de la ligne 12 à [debug] puis exécutez le test unitaire. Regardez la différence de logs.


11.3.12. Génération de l'archive Maven du projet

Pour installer l'archive du projet dans le dépôt local Maven, procédez comme suit [1-3] :

L'archive va être générée avec les identifiants trouvés dans le fichier [pom.xml] :


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

La localisation du dépôt local Maven peut être trouvé dans la configuration d'Eclipse :

 

Il est alors possible de vérifier la bonne installation de l'artifact Maven :

 

Désormais, un autre projet Maven local pourra utiliser cette archive.