Skip to content

11. [Cours]: Gestione dei database relazionali con Spring Data

Parole chiave: architettura a più livelli, Spring, iniezione di dipendenze, API JPA (Java Persistence API), Spring Data.

Implementeremo il livello [DAO] del TD utilizzando [Spring Data], un ramo dell’ecosistema Spring. [Spring Data] si basa su un livello JPA (Java Persistence API) che consente al livello [DAO] di gestire oggetti anziché comandi SQL. In definitiva, il livello [DAO] ignora di interagire con un database. Conosce solo l’interfaccia del livello [Spring Data].

Inizieremo ad approfondire [Spring Data] attraverso due esempi.

11.1. Support

  • in [1], la cartella [support / chap-11] contiene tre progetti Eclipse;
  • in [2], lo script SQL che consente di creare il database di esempio di questo capitolo;

11.2. Esempio 1

Sul sito di Spring sono disponibili numerosi tutorial per iniziare a utilizzare Spring [http://spring.io/guides]. Ne useremo uno per introdurre Spring Data. A tal fine utilizzeremo Spring Tool Suite (STS).

  • in [1], importiamo uno dei tutorial da [spring.io/guides];
  • in [2], si seleziona il tutorial [Accessing Data Jpa] che illustra come accedere a un database con Spring Data;
  • in [3], si sceglie un progetto configurato da Maven;
  • in [4], il tutorial può essere fornito in due forme: [initial], che è una versione vuota da compilare seguendo il tutorial, oppure [complete], che è la versione finale del tutorial. Scegliamo quest'ultima;
  • in [5], è possibile scegliere di visualizzare il tutorial in un browser;
  • in [6], il progetto finale.

11.2.1. La configurazione Maven del progetto

Le dipendenze Maven del progetto sono configurate nel file [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>
        <!-- utilizza UTF-8 per tutto -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • righe 5-9: definiscono un progetto Maven padre. È questo che definisce la maggior parte delle dipendenze del progetto. Possono essere sufficienti, nel qual caso non se ne aggiungono altre, oppure no, nel qual caso si aggiungono le dipendenze mancanti;
  • righe 12-15: definiscono una dipendenza da [spring-boot-starter-data-jpa]. Questo artefatto contiene le classi di Spring Data;
  • righe 16-19: definiscono una dipendenza da SGBD e H2, che consentono di creare e gestire database in memoria.

Diamo un'occhiata alle classi fornite da queste dipendenze:

Sono davvero numerose:

  • alcune appartengono all'ecosistema Spring (quelle che iniziano con spring);
  • altre appartengono all'ecosistema Hibernate (hibernate, jboss), di cui qui si utilizza l'implementazione JPA;
  • altre sono librerie di test (junit, hamcrest);
  • altre sono librerie di log (log4j, logback, slf4j);

Le manterremo tutte. Per un'applicazione in produzione, bisognerebbe mantenere solo quelle necessarie.

Alla riga 26 del file [pom.xml] si trova la riga:


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

Questa riga è collegata alle righe seguenti:


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

Righe 6-9: il plugin [spring-boot-maven-plugin] consente di generare il file JAR eseguibile dell'applicazione. La riga 26 del file [pom.xml] indica quindi la classe eseguibile di questo JAR.

11.2.2. Il livello [JPA]

L'accesso al database avviene tramite un livello [JPA], Java Persistence API:

  

L'applicazione è semplice e gestisce i clienti [Customer]. La classe [Customer] fa parte del livello [JPA] ed è la seguente:


package hello;

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

@Entity
public class Customer {

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

    protected Customer() {
    }

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

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

}

Un cliente ha un identificativo [id], un nome [firstName] e un cognome [lastName]. Ogni istanza [Customer] rappresenta una riga di una tabella del database.

  • riga 8: annotazione JPA che fa sì che la persistenza delle istanze [Customer] (Create, Read, Update, Delete) venga gestita da un'implementazione JPA. In base alle dipendenze Maven, si vede che viene utilizzata l’implementazione JPA / Hibernate;
  • righe 11-12: annotazioni JPA che associano il campo [id] alla chiave primaria della tabella [Customer]. La riga 12 indica che l'implementazione JPA utilizzerà il metodo di generazione della chiave primaria specifico del SGBD utilizzato, in questo caso H2;

Non sono presenti altre annotazioni relative a JPA. Verranno quindi utilizzati i valori predefiniti:

  • la tabella [Customer] porterà il nome della classe, ovvero [Customer];
  • le colonne di questa tabella porteranno il nome dei campi della classe: [id, firstName, lastName], tenendo presente che nel nome di una colonna della tabella non viene fatta distinzione tra maiuscole e minuscole;

Si noti che in nessun momento viene menzionata l’implementazione JPA utilizzata.

11.2.3. Il livello [Spring Data]

La classe [CustomerRepository] implementa il livello di accesso alla tabella [Customer]. Il suo codice è il seguente:

  

package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}

Si tratta quindi di un'interfaccia e non di una classe (riga 7). Essa estende l'interfaccia [CrudRepository], un'interfaccia di Spring Data (riga 5). Questa interfaccia è parametrizzata da due tipi: il primo è il tipo degli elementi gestiti, in questo caso il tipo [Customer], il secondo è il tipo della chiave primaria degli elementi gestiti, in questo caso un tipo [Long]. L’interfaccia [CrudRepository] è la seguente:


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

Questa interfaccia definisce le operazioni CRUD (Create – Read – Update – Delete) che è possibile eseguire su un tipo JPA T:

  • riga 8: il metodo save consente di salvare un'entità T nel database. L'entità viene salvata con la chiave primaria assegnatale da SGBD. Consente inoltre di aggiornare un'entità T identificata dalla sua chiave primaria id. La scelta tra l'una o l'altra azione dipende dal valore della chiave primaria id: se questa è null, viene eseguita l'operazione di persistenza, altrimenti viene eseguita l'operazione di aggiornamento;
  • riga 10: lo stesso, ma per un elenco di entità;
  • riga 12: il metodo findOne consente di recuperare un'entità T identificata dalla sua chiave primaria id;
  • riga 22: il metodo delete consente di eliminare un'entità T identificata dalla sua chiave primaria id;
  • righe 24-28: varianti del metodo [delete];
  • riga 16: il metodo [findAll] consente di recuperare tutte le entità persistenti T;
  • riga 18: lo stesso, ma limitato alle entità di cui è stato fornito l'elenco degli identificatori;

Torniamo all'interfaccia [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 riga 9 consente di recuperare un [Customer] in base al suo nome [lastName];

E questo è tutto per il livello [DAO]. Non esiste una classe di implementazione dell’interfaccia precedente. Questa viene generata in fase di esecuzione da [Spring Data]. I metodi dell’interfaccia [CrudRepository] vengono implementati automaticamente. Per i metodi aggiunti nell’interfaccia [CustomerRepository], dipende. Torniamo alla definizione di [Customer]:


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

Il metodo alla riga 9 viene implementato automaticamente da [Spring Data] poiché fa riferimento al campo [lastName] (riga 3) di [Customer]. Quando incontra un metodo [findBySomething] nell’interfaccia da implementare, Spring Data lo implementa tramite la seguente query JPQL (Java Persistence Query Language):

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

È quindi necessario che il tipo T abbia un campo denominato [something]. Pertanto, il metodo

List<Customer> findByLastName(String lastName);

verrà implementato con un codice simile al seguente:

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

dove [em] indica il contesto di persistenza JPA. Ciò è possibile solo se la classe [Customer] dispone di un campo denominato [lastName], come effettivamente avviene.

In conclusione, nei casi semplici, Spring Data ci permette di implementare il livello [DAO] con una semplice interfaccia.

11.2.4. Il livello [console]

  

La classe [Application] è la seguente:


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 {
        // salva un paio di clienti
        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"));

        // recupera tutti i clienti
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : repository.findAll()) {
            System.out.println(customer);
        }
        System.out.println();

        // recupera un singolo cliente tramite ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();

        // recupera i clienti in base al cognome
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : repository.findByLastName("Bauer")) {
            System.out.println(bauer);
        }
    }

}
  • riga 9: la classe implementa l'interfaccia [CommandLineRunner], che a sua volta è un'interfaccia di [Spring Boot] (riga 4). Questa interfaccia ha un solo metodo, quello della riga 19;
  • riga 8: @SpringBootApplication è un'annotazione che raggruppa diverse annotazioni [Spring Boot]:
    • @Configuration: indica che la classe è una classe di configurazione;
    • @EnableAutoConfiguration: richiede a [Spring Boot] di creare autonomamente un certo numero di bean in base a varie proprietà, in particolare al contenuto del Classpath del progetto. Poiché le librerie Hibernate si trovano nel Classpath, il bean [entityManagerFactory] verrà implementato con Hibernate. Poiché la libreria di SGBD H2 si trova nel Classpath, il bean [dataSource] verrà implementato con H2. Nel bean [dataSource], è necessario definire anche l'utente e la relativa password. In questo caso Spring Boot utilizzerà l’amministratore predefinito di H2, che non ha password. Poiché la libreria [spring-tx] si trova nel Classpath, verrà utilizzato il gestore delle transazioni di Spring;
    • @EnableWebMvc: se nel Classpath è presente la libreria [spring-mvc]. In questo caso, viene eseguita una configurazione automatica per l’applicazione web;
    • @ComponentScan: indica a Spring dove cercare gli altri bean, le configurazioni e i servizi. Qui vengono cercati per impostazione predefinita nel pacchetto contenente la classe annotata, ovvero il pacchetto [hello]. In questo modo verranno individuate le classi [Customer] e [CustomerRepository]. Poiché la prima presenta l’annotazione [@Entity], verrà catalogata come entità da gestire da Hibernate. Poiché la seconda estende l’interfaccia [CrudRepository], verrà registrata come bean Spring;
  • righe 11-12: il bean [CustomerRepository] viene iniettato nel codice della classe principale;
  • riga 15: viene eseguito il metodo statico [run] della classe [SpringApplication] del progetto Spring Boot. Il suo parametro è la classe che presenta un'annotazione [Configuration] o [EnableAutoConfiguration]. A questo punto si verificherà tutto ciò che è stato spiegato in precedenza. Il risultato è un contesto applicativo Spring, ovvero un insieme di bean gestiti da Spring;

Le operazioni successive si limitano a utilizzare i metodi del bean che implementa l’interfaccia [CustomerRepository]. I risultati visualizzati in console sono i seguenti:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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
  • righe 1-8: il logo del progetto Spring Boot;
  • riga 9: viene eseguita la classe [hello.Application];
  • riga 10: [AnnotationConfigApplicationContext] è una classe che implementa l’interfaccia [ApplicationContext] di Spring. Si tratta di un contenitore di bean;
  • riga 11: il bean [entityManagerFactory] è implementato dalla classe [LocalContainerEntityManagerFactory], una classe di Spring;
  • riga 12: compare [Hibernate]. È stata scelta proprio questa implementazione, JPA;
  • riga 19: il dialetto Hibernate da utilizzare con SGBD è la variante SQL. Qui il dialetto [H2Dialect] indica che Hibernate opererà con SGBD e H2;
  • righe 21-22: viene creato il database. Viene creata la tabella [CUSTOMER]. Ciò significa che Hibernate è stato configurato per generare le tabelle a partire dalle definizioni JPA; in questo caso, la definizione JPA della classe [Customer];
  • righe 26-30: risultato del metodo [findAll] dell'interfaccia;
  • riga 34: risultato del metodo [findOne] dell'interfaccia;
  • righe 38-39: risultati del metodo [findByLastName];
  • righe 41 e seguenti: log relativi alla chiusura del contesto Spring.

11.2.5. Configurazione manuale del progetto Spring Data

Duplichiamo il progetto precedente nel progetto [gs-accessing-data-jpa-02]:

  

In questo nuovo progetto non ci affideremo alla configurazione automatica effettuata da Spring Boot. La eseguiremo manualmente. Ciò può essere utile se le configurazioni predefinite non ci soddisfano.

Per prima cosa, specificheremo le dipendenze necessarie nel file [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>
        <!-- Database H2 -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <!-- utilizza UTF-8 per tutto -->
        <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>
  • righe 10-14: il progetto Maven padre di cui utilizzeremo le librerie che definisce;
  • righe 18-21: Spring Data utilizzato per accedere al database;
  • righe 23-26: l’implementazione Hibernate della specifica JPA;
  • righe 28-31: il SGBD H2;
  • righe 33-36: i database vengono spesso utilizzati con pool di connessioni aperte che evitano ripetute operazioni di apertura e chiusura delle connessioni. In questo caso, l'implementazione utilizzata è quella di [tomcat-jdbc];

Nel nuovo progetto, l’entità [Customer] e l’interfaccia [CustomerRepository] rimangono invariate. Modificheremo la classe [Application], che verrà suddivisa in due classi:

  • [Config], che sarà la classe di configurazione;
  • [Main], che sarà la classe eseguibile;
  

La classe eseguibile [Application] è ora la seguente:


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

        // salvataggio di un paio di clienti
        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"));

        ...

        // chiusura del contesto
        context.close();
    }
}
  • riga 9: la classe [Application] non presenta più annotazioni di configurazione;
  • righe 3-7: si noti che non ci sono più importazioni di pacchetti [Spring Boot];
  • riga 12: si istanziano i bean Spring. Si ottiene il contesto Spring che contiene il riferimento ai bean così creati;
  • riga 13: si richiede un riferimento al bean di tipo [CustomerRepository];

La classe [Config] che configura il progetto è la seguente:


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 {

    // il database H2
    @Bean
    public DataSource dataSource() {
        // fonte dati TomcatJdbc
        DataSource dataSource = new DataSource();
        // configurazione di accesso JDBC
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        // una connessione inizialmente aperta
        dataSource.setInitialSize(1);
        // risultato
        return dataSource;
    }

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

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

}
  • riga 17: l’annotazione [@EnableTransactionManagement] indica che i metodi delle interfacce [CrudRepository] devono essere eseguiti all’interno di una transazione. È stata messa tra commenti poiché si tratta dell’impostazione predefinita;
  • riga 18: l’annotazione [@EnableJpaRepositories] consente di specificare le cartelle in cui si trovano le interfacce Spring Data [CrudRepository]. Queste interfacce diventeranno componenti Spring e saranno disponibili nel relativo contesto;
  • riga 19: l’annotazione [@Configuration] rende la classe [Config] una classe di configurazione Spring;
  • riga 20: l'annotazione [@ComponentScan] consente di elencare le cartelle in cui devono essere cercati i componenti Spring. I componenti Spring sono classi contrassegnate con annotazioni Spring quali @Service, @Component, @Controller, ... In questo caso, non ce ne sono altri oltre a quelli definiti all’interno della classe [AppConfig], pertanto l’annotazione è stata commentata;
  • righe 24-37: definiscono la fonte dati, il database H2. È l’annotazione @Bean alla riga 25 che rende l’oggetto creato da questo metodo un componente gestito da Spring. Il nome del metodo può essere qualsiasi in questo caso. Tuttavia, deve essere chiamato [dataSource] se il metodo EntityManagerFactory della riga 51 è assente e viene definito tramite autoconfigurazione;
  • riga 30: il database si chiamerà [demo] e verrà generato nella cartella del progetto;
  • righe 40-47: definiscono l’implementazione JPA utilizzata, in questo caso un’implementazione Hibernate. Il nome del metodo può essere qualsiasi;
  • riga 43: nessun log SQL;
  • riga 44: il database verrà creato se non esiste;
  • righe 50-58: definiscono il metodo EntityManagerFactory che gestirà la persistenza di JPA. Il metodo deve chiamarsi obbligatoriamente [entityManagerFactory];
  • riga 51: il metodo riceve due parametri del tipo dei due bean definiti in precedenza. Questi verranno quindi creati e poi iniettati da Spring come parametri del metodo;
  • riga 53: specifica l’implementazione JPA utilizzata;
  • riga 54: specifica le cartelle in cui si trovano le entità JPA;
  • riga 55: specifica la fonte dati da gestire;
  • righe 61-66: il gestore delle transazioni. Il metodo deve chiamarsi obbligatoriamente [transactionManager]. Riceve come parametro il bean delle righe 51-58;
  • riga 64: il gestore delle transazioni è associato a EntityManagerFactory;

I metodi precedenti possono essere definiti in qualsiasi ordine.

L'esecuzione del progetto produce gli stessi risultati. Nella cartella del progetto compare un nuovo file, quello del database H2:

  

11.2.6. Creazione di un archivio eseguibile

Per creare un archivio eseguibile del progetto, è possibile procedere come segue:

  • in [1]: si crea una configurazione di esecuzione;
  • in [2]: di tipo [Java Application]
  • in [3]: si indica il progetto da eseguire (utilizzare il pulsante Browse);
  • in [4]: indica la classe da eseguire;
  • in [5]: il nome della configurazione di esecuzione – può essere qualsiasi;
  • in [6]: si esporta il progetto;
  • in [7]: sotto forma di archivio eseguibile JAR;
  • in [8]: indica il percorso e il nome del file eseguibile da creare;
  • in [9]: il nome della configurazione di esecuzione creata in [5];
  • in [10], l'archivio creato;

Fatto ciò, si apre una console nella cartella contenente l'archivio eseguibile:

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

L'archivio viene eseguito nel modo seguente:


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

I risultati ottenuti nella console sono i seguenti:

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 per ulteriori dettagli.
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. Esempio 2

11.3.1. Introduzione

Riprenderemo l'esempio della tabella dei prodotti che abbiamo utilizzato per introdurre API JDBC e creeremo la seguente architettura:

Il database [dbintrospringjpa] contiene due tabelle: [PRODUITS] e [CATEGORIES]. La tabella [CATEGORIES] è la seguente:

 
  • [ID]: chiave primaria in modalità AUTO_INCREMENT;
  • [VERSION]: numero di versione del record;
  • [NOM]: nome della categoria - univoco;

La tabella [PRODUITS] è la seguente:

 
  • [ID]: chiave primaria in modalità AUTO_INCREMENT;
  • [VERSION]: numero di versione del record;
  • [NOM]: nome di un prodotto - univoco;
  • [ID_CATEGORIE]: numero della categoria - chiave esterna sul campo [CATEGORIES.ID];
  • [PRIX]: il suo prezzo;
  • [DESCRIPTION]: una descrizione del prodotto;

Compito da svolgere: creare il database [dbintrospringdata] con lo script SQL [dbintrospringdata.sql] fornito nel supporto:


11.3.2. Creazione del progetto Maven

Per creare uno scheletro di progetto Spring Data, è possibile procedere come segue:

  • in [1], si crea un nuovo progetto;
  • in [2]: di tipo [Spring Starter Project];
  • il progetto generato sarà un progetto Maven. In [3], si indica il nome del gruppo del progetto;
  • in [4]: si indica il nome dell’artefatto (in questo caso un jar) che verrà creato durante la compilazione del progetto;
  • in [5]: il nome del progetto in Eclipse – può essere qualsiasi nome (non deve necessariamente coincidere con [4]);
  • in [7]: si specifica che verrà creato un progetto con un livello [JPA] contenente SGBD e MySQL. Le dipendenze necessarie per tale progetto saranno quindi incluse nel file [pom.xml];
  • in [8], specificare il nome della cartella del progetto;
  • in [9], completare la procedura guidata;
  • in [10]: il progetto creato;

Il file [pom.xml] include le dipendenze necessarie per un progetto 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/> <!-- ricerca del genitore dal 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>
  • righe 14-19: il progetto Maven padre - definisce un gran numero di librerie con le relative versioni - queste librerie vengono utilizzate come dipendenze Maven senza specificarne la versione;
  • righe 28-31: la dipendenza necessaria per JPA – includerà [Spring Data];
  • righe 32-36: la dipendenza dal driver JDBC di MySQL;
  • righe 37-41: le dipendenze necessarie per i test JUnit integrati con Spring;

La classe eseguibile [Application] non esegue alcuna operazione, ma è preconfigurata:


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'annotazione [@SpringBootApplication] rende la classe una classe di autoconfigurazione del progetto;

La classe di test [ApplicationTests] non esegue alcuna operazione, ma è preconfigurata:


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

}
  • riga 9: l'annotazione [@SpringApplicationConfiguration] consente di utilizzare il file di configurazione [Application]. La classe di test beneficerà così di tutti i bean definiti da tale file;
  • riga 8: l'annotazione [@RunWith] consente l'integrazione di Spring con JUnit: la classe potrà essere eseguita come un test JUnit. [@RunWith] è un'annotazione JUnit (riga 4), mentre la classe [SpringJUnit4ClassRunner] è una classe Spring (riga 6);

Ora che disponiamo di uno scheletro dell’applicazione JPA, possiamo completarlo per scrivere il progetto del livello di persistenza associato al database dei prodotti.

11.3.3. Il progetto Eclipse

Modifichiamo il progetto precedente nel modo seguente:

  
  • [AppConfig.java]: la classe di configurazione del progetto Spring;
  • [Main.java]: la classe eseguibile del progetto;
  • [IDao.java]: l'interfaccia del livello [DAO];
  • [Dao.java]: la classe di implementazione del livello [DAO];
  • [AbstractEntity.java]: la classe padre delle classi [Produit] e [Categorie];
  • [Produit.java]: classe associata a una riga della tabella [PRODUITS] del database;
  • [Categorie.java]: classe associata a una riga della tabella [CATEGORIES] del database;
  • [ProduitsRepository]: l'interfaccia Spring Data per l'accesso alla tabella [PRODUITS];
  • [CategoriesRepository]: l'interfaccia Spring Data per l'accesso alla tabella [CATEGORIES];
  • [pom.xml]: il file di configurazione del progetto Maven;

Questo progetto implementa la seguente architettura:

Il livello [DAO] vede solo il livello implementato da [Spring Data].

11.3.4. Configurazione Maven

Il file [pom.xml] del progetto Maven è il seguente:


<?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>
        <!-- libreria 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>
        <!-- Test Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- libreria di log -->
        <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>

Questa configurazione è quella utilizzata e spiegata nel paragrafo 11.2.5. Aggiungiamo le seguenti librerie:

  • righe 42-49: una libreria jSON utilizzata dal metodo [toString] della classe [Produit];
  • righe 51-55: la libreria [Google Guava] che fornisce metodi di utilità per la gestione di collezioni di elementi. Sarà utilizzata dalla classe [Dao] che implementa il livello [DAO];
  • righe 56-67: le librerie necessarie per i test JUnit;
  • righe 69-72: una libreria di log;
  • righe 81-86: i plugin Maven necessari per il progetto;

11.3.5. Le entità del livello [JPA]

Livello

[DAO]

Livello

[console]

Livello

[JPA]

Pilota

[JDBC]

Strato

[Spring Data]

Spring 4

SGBD

  

11.3.5.1. La classe [AbstractEntity]

La classe [AbstractEntity] è la seguente:


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

    // costruttori
    public AbstractEntity() {

    }

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

    // ridefinizione di [equals] e [hashcode]
    @Override
    public int hashCode() {
        return (id != null ? id.hashCode() : 0);
    }

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

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

Questa classe ha lo scopo di fornire una classe madre alle entità JPA, incapsulando in un unico punto le proprietà [id, version] (righe 19, 22) comuni alle due entità [Produit] e [Categorie] collegate al database. Queste proprietà sono associate alle colonne [ID, VERSION] delle tabelle (righe 18, 21).

  • riga 13: l’annotazione [@MappedSuperclass] indica che la classe è una classe padre delle entità JPA;
  • riga 16: l’annotazione [@Id] indica che il campo [id] (potrebbe avere un altro nome) è associato alla chiave primaria di una tabella;
  • riga 17: l'annotazione [@GeneratedValue(strategy=GenerationType.IDENTITY)] definisce la modalità di generazione delle chiavi primarie. La modalità [GenerationType.IDENTITY] utilizzerà, insieme a MySQL, la modalità [AUTO_INCREMENT]. Con un altro SGBD, questa modalità utilizzerebbe un altro metodo. Il vantaggio è che lo sviluppatore non deve preoccuparsene e che il suo codice rimane valido indipendentemente dal SGBD utilizzato;
  • riga 18: l’annotazione [@Column] indica la colonna associata al campo. Quando questa annotazione non è presente, JPA presume che la colonna abbia lo stesso nome del campo. È il caso qui. Non sarebbe stato quindi necessario inserire questa annotazione;
  • riga 20: l'annotazione [@Version] indica che il campo [version] è associato a una colonna di versioning. L'implementazione JPA incrementerà questo numero di versione ogni volta che l'entità verrà modificata. Questo numero serve a impedire l'aggiornamento simultaneo dell'entità da parte di due utenti diversi: due utenti, U1 e U2, leggono l'entità E con un numero di versione pari a V1. U1 modifica E e salva tale modifica nel database: il numero di versione passa quindi a V1+1. U2 a sua volta modifica E e salva tale modifica nel database: riceverà un'eccezione poiché possiede una versione (V1) diversa da quella presente nel database (V1+1);
  • righe 35-52: ridefinizione dei metodi [hashCode] e [equals]. Per impostazione predefinita, [obj1.equals(obj2)] è vero se [obj1==obj2] è vero, ovvero se ob1 e obj2 sono due puntatori uguali. Se si desidera confrontare gli oggetti puntati anziché i puntatori stessi, è necessario ridefinire il metodo [equals] e il metodo [hashCode]. Quest’ultimo deve restituire lo stesso valore per due oggetti che il metodo [equals] considera uguali;
  • righe 42-51: due oggetti di tipo [AbstractEntity] o derivati saranno considerati uguali se le loro chiavi primarie [id] sono uguali;
  • righe 35-38: il metodo [hashCode] restituisce effettivamente lo stesso valore per due oggetti [AbstractEntity] identici e quindi aventi la stessa chiave primaria [id];
  • righe 55-63: il metodo [toString] restituisce la stringa jSon dell’oggetto [this]. Se tale oggetto indica una classe figlia, questo metodo restituirà quindi la stringa jSON della classe figlia. Ciò ci evita di creare un metodo [toString] nelle classi figlie;

11.3.5.2. L’entità JPA [Produit]

La classe [Produit] è un’entità JPA associata a una riga della tabella [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 {

    // proprietà
    @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 categoria
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CATEGORIE_ID")    
    private Categorie categorie;

    // costruttori
    public Produit() {

    }

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

    // getter e setter
...
}
  • riga 12: l'annotazione [@Entity] rende la classe [Produit] un'entità gestita dal livello [JPA];
  • riga 13: l'annotazione [@Table(name = "PRODUITS")] indica che la classe [Produit] è l'immagine oggetto di una riga della tabella [PRODUITS] del database;
  • riga 14: il nome del filtro jSON da applicare all’entità. Vedremo che la proprietà [categorie] della riga 13 non è sempre disponibile. È quindi necessario escluderla dalla rappresentazione jSON dell’oggetto. A tal fine abbiamo bisogno di un filtro. È quindi in un filtro denominato [jsonFilterCategorie] che indicheremo se desideriamo o meno la proprietà [categorie];
  • riga 18: l'annotazione [@Column] associa il campo [nom] alla colonna [NOM] della tabella [PRODUITS]. Quando il campo ha lo stesso nome della colonna associata, l'annotazione [@Column] può essere omessa. Questo sarebbe il caso qui;
  • righe 31-33: la categoria del prodotto;
  • riga 31: l’annotazione [@ManyToOne] indica che la colonna dell’annotazione della riga 32 [@JoinColumn(name = "CATEGORIE_ID")] è una chiave esterna della tabella [PRODUITS] dell’entità [Produit] sulla tabella [CATEGORIES] associata all’entità della riga 33. Questa annotazione deve riferirsi a un’entità JPA. Pertanto, la classe della riga 33 deve essere un’entità JPA;
  • riga 31: l'annotazione [fetch = FetchType.LAZY] richiede che, quando si recupera un prodotto dalla tabella [PRODUITS], la sua categoria (riga 33) non venga recuperata immediatamente (lazy loading). Viene quindi recuperata al primo richiamo del metodo [getCategorie]. Questo attributo non è vincolante. L'implementazione JPA utilizzata può ignorarlo. È proprio perché la proprietà [categorie] può essere presente o meno che abbiamo introdotto il filtro jSON alla riga 14. Le implementazioni JPA esistenti (Hibernate, Eclipselink, OpenJPA) non gestiscono questa annotazione allo stesso modo. Hibernate arricchisce il metodo [getCategorie] iniziale (che si limita a restituire il campo categorie) con una chiamata a SGBD per recuperare la categoria. Affinché ciò sia possibile, è necessario che la connessione a SGBD utilizzata inizialmente per ottenere il prodotto sia ancora aperta; in caso contrario, si verifica un'eccezione.

11.3.5.3. L'entità JPA [Categorie]

La classe [Categorie] è un'entità JPA associata a una riga della tabella [CATEGORIES]:

 

Il suo codice è il seguente:


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 {

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

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

    // costruttori
    public Categorie() {

    }

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

    // metodi
    public void addProduit(Produit produit) {
        // si aggiunge il prodotto
        produits.add(produit);
        // si imposta la categoria
        produit.setCategorie(this);
    }

    // getter e setter
...
}
  • righe 21-22: il nome della categoria;
  • righe 25-26: i prodotti di questa categoria;
  • riga 25: l'annotazione [@OneToMany] è la relazione inversa della relazione [@ManyToOne] che abbiamo incontrato nell'entità [Produit]. L'attributo [mappedBy = "categorie"] indica il campo dell'entità [Produit] annotato dalla relazione inversa [@ManyToOne]. L'attributo [cascade = { CascadeType.ALL }] richiede che le operazioni (persist, merge, remove) eseguite su un'@Entity [Categorie] vengano propagate in cascata alle [produits] della riga 26. È possibile specificare cascate parziali utilizzando le costanti [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE];
  • riga 25: l'attributo [fetch = FetchType.LAZY] specifica che, quando si recupera una categoria dalla tabella [CATEGORIES], i relativi prodotti non vengano recuperati immediatamente. Verranno recuperati al primo richiamo del metodo [getProduits]. Le implementazioni JPA esistenti (Hibernate, Eclipselink, OpenJPA) non gestiscono questa annotazione allo stesso modo. Hibernate arricchisce il metodo [getProduits] iniziale (che si limita a restituire il campo produits) con una chiamata al metodo SGBD per recuperare i prodotti della categoria. Affinché ciò sia possibile, è necessario che la connessione a SGBD utilizzata inizialmente per ottenere la categoria sia ancora aperta. Questo requisito è vincolante. L’implementazione JPA non può ignorarlo. Poiché la proprietà [produits] può essere inizializzata o meno, abbiamo introdotto il filtro jSON alla riga 17 che ci consentirà di specificare se desideriamo o meno questa proprietà;
  • riga 26: il tipo [Set] è un'interfaccia. Il tipo [HashSet] è una classe che implementa tale interfaccia. Essa implementa una collezione di elementi denominata ensemble. Un insieme non può contenere due oggetti identici. In questo caso gli oggetti sono di tipo [Produit]. Pertanto, nell’insieme non potranno esserci due oggetti identici. Poiché il metodo [equals] della classe padre [AbstractEntity] è stato ridefinito per stabilire che due prodotti sono identici se hanno la stessa chiave primaria, il campo [produits] non potrà contenere due prodotti con la stessa chiave primaria;
  • righe 38-43: il metodo [addProduit] consente di aggiungere un prodotto alla categoria;

11.3.6. Il livello [Spring Data]

Livello

[DAO]

Livello

[console]

Strato

[JPA]

Pantaloni

[JDBC]

Strato

[Spring Data]

Molla 4

SGBD

  

L'interfaccia [CategoriesRepository] gestisce gli accessi alla tabella [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> {

    // categoria con i relativi prodotti
    @Query("select c from Categorie c left join fetch c.produits p where c.id=?1")
    public Categorie getCategorieByIdWithProduits(Long id);

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

    // una categoria senza i relativi prodotti, identificata dal nome
    public Categorie findByNom(String nom);
}
  • riga 8: l'interfaccia [CrudRepository] è stata utilizzata e illustrata nel paragrafo 11.2.3. Si ricorda che:
    • il primo tipo dell'interfaccia è l'entità JPA gestita per gli accessi CRUD (findOne, findAll, salva, elimina, deleteAll),
    • il secondo tipo è quello della chiave primaria dell'entità JPA, in questo caso un intero [Long];
  • riga 12: il metodo della riga 12 è implementato dalla query JPQL (Java Persistence Query Language) della riga 11. Questa query interroga le entità JPA. In una query di questo tipo:
    • le tabelle vengono sostituite dalle relative entità JPA;
    • le colonne vengono sostituite dai campi delle entità JPA utilizzate nella query;
  • riga 11: la query JPQL restituisce una categoria con i relativi prodotti. Ricordiamo che nell’entità [Categorie], il campo [produits] aveva l’attributo [fetch = FetchType.LAZY] (lazy loading). Nella query JPQL, si forza il caricamento dei prodotti con la parola chiave [fetch]. Il parametro ?1 della query verrà sostituito in fase di esecuzione con il valore del primo parametro del metodo alla riga 12, quindi con il parametro [Long id];
  • righe 14-15: un metodo analogo per una categoria identificata dal proprio nome;
  • riga 18: il metodo [findByNom] verrà automaticamente implementato da [Spring Data] poiché il tipo [Category] possiede un campo [nom];

L'interfaccia [ProduitsRepository] gestisce gli accessi alla tabella [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 prodotto con la sua categoria
    @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 prodotto senza la sua categoria, identificato dal nome
    public Produit findByNom(String nom);
}

Le spiegazioni sono le stesse valide per l’interfaccia [CategoriesRepository].

Queste interfacce saranno implementate da classi generate da [Spring Data] al momento dell'esecuzione del progetto. Tali classi sono denominate [proxy]. Per impostazione predefinita, i metodi della classe di implementazione vengono eseguiti all’interno di una transazione. Il fatto che queste interfacce estendano la classe [CrudRepository] le rende componenti Spring.

11.3.7. Il livello [DAO]

Livello

[DAO]

Livello

[console]

Strato

[JPA]

Pantaloni

[JDBC]

Strato

[Spring Data]

Molla 4

SGBD

  

L'interfaccia [IDao] del livello [DAO] è la seguente:


package spring.data.dao;

import java.util.List;

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

public interface IDao {

    // inserimento di un elenco di prodotti
    public List<Produit> addProduits(List<Produit> produits);

    // eliminazione di tutti i prodotti
    public void deleteAllProduits();

    // aggiornamento di un elenco di prodotti
    public List<Produit> updateProduits(List<Produit> produits);

    // recupero di tutti i prodotti
    public List<Produit> getAllProduits();

    // inserimento di un elenco di categorie
    public List<Categorie> addCategories(List<Categorie> categories);

    // eliminazione di tutte le categorie
    public void deleteAllCategories();

    // aggiornamento di un elenco di categorie
    public List<Categorie> updateCategories(List<Categorie> categories);

    // recupero di tutte le categorie
    public List<Categorie> getAllCategories();

    // un prodotto specifico con o senza la relativa categoria
    public Produit getProduitByIdWithoutCategorie(Long idProduit);

    public Produit getProduitByIdWithCategorie(Long idProduit);

    public Produit getProduitByNameWithCategorie(String nom);

    public Produit getProduitByNameWithoutCategorie(String nom);

    // una categoria specifica con o senza i relativi prodotti
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie);

    public Categorie getCategorieByIdWithProduits(Long idCategorie);

    public Categorie getCategorieByNameWithProduits(String nom);

    public Categorie getCategorieByNameWithoutProduits(String nom);
}

Qui è stata adottata la regola secondo cui ogni metodo che modifica gli oggetti che ha come parametri di input deve poi restituirli nel proprio risultato. Il motivo di questa regola è stato spiegato nel paragrafo 4.2: essa consente a un livello e al suo client di trovarsi in due JVM separate e quindi di operare in modalità client/server.

L’implementazione [Dao] di questa interfaccia è la seguente:


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

}
  • riga 16: l’annotazione [@Component] rende la classe [Dao] un componente Spring;
  • righe 19-23: iniezione dei riferimenti nelle due interfacce [CrudRepository] e [Spring Data]. Tale iniezione avverrà durante l'istanziazione degli oggetti Spring, in genere all'inizio dell'esecuzione del progetto Spring;
  • si noti, alle righe 28 e 46, che il metodo [save] dell’interfaccia [produitsRepository] viene utilizzato sia per l’inserimento che per l’aggiornamento dei prodotti. [Spring Data] utilizza la chiave primaria del prodotto per determinare se deve eseguire un inserimento o un aggiornamento. Se la chiave primaria è [null], si tratterà di un inserimento, altrimenti di un aggiornamento;
  • riga 82: si utilizza il metodo [Lists.newArrayList] della libreria Guava per ottenere un elenco di prodotti. Il metodo [produitsRepository.findAll()] restituisce un tipo [Iterable<Produit>];
  • riga 28: il metodo [produitsRepository.save(produits)] restituisce un tipo [Iterable<Produit>]. Lo stesso vale per le altre operazioni [save] della classe;

Nella classe [Dao] sopra riportata, le eccezioni che possono verificarsi sono incapsulate nel seguente tipo [DaoException]:


package spring.data.dao;

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

// classe di eccezione per l'applicazione Elezioni
// l'eccezione non è controllata

public class DaoException extends RuntimeException implements Serializable {

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

    // campi locali
    private int code;
    private List<String> erreurs;

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

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

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

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

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

    // elenco dei messaggi di errore di un'eccezione
    private List<String> getErreursForException(Throwable th) {
        // si recupera l'elenco dei messaggi di errore dell'eccezione
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // si recupera il messaggio solo se !=null e non è vuoto
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // causa successiva
            cause = cause.getCause();
        }
        return erreurs;
    }

    // getter e setter
...
}
  • riga 10: la classe deriva dalla classe [RuntimeException] ed è quindi un'eccezione non controllata;
  • riga 16: un codice di errore;
  • riga 17: un elenco di messaggi di errore, quelli associati alla pila delle eccezioni che hanno causato la [DaoException];
  • righe 59-76: il metodo privato [getMessagesForException] consente di ottenere l'elenco dei messaggi di errore associati alle eccezioni presenti nello stack delle eccezioni. È infatti possibile impilare le eccezioni utilizzando i seguenti costruttori della classe Exception:
    • Exception(String message, Throwable cause): crea un'eccezione con un messaggio e l'eccezione che si desidera incapsulare;
    • Exception(Throwable cause): crea un'eccezione con l'eccezione che si desidera incapsulare;

Il tipo [Throwable] è la classe padre della classe [Exception]. Se i costruttori precedenti vengono eseguiti ripetutamente, l’eccezione finale conterrà quindi più eccezioni. Si parla in questo caso di una pila di eccezioni.

  • l'ultima causa di un'eccezione e1 si ottiene tramite l'espressione [e1.getCause()];
  • la penultima causa di un'eccezione e1 si ottiene tramite l'espressione [e1.getCause().getCause()];
  • si procede in questo modo fino a ottenere [getCause()==null];

11.3.8. Configurazione del progetto Spring

  

La classe [DaoConfig] configura il livello [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 {

    // costanti
    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 fonte dei dati [tomcat-jdbc]
    @Bean
    public DataSource dataSource() {
        // fonte dati TomcatJdbc
        DataSource dataSource = new DataSource();
        // configurazione di accesso JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // una connessione inizialmente aperta
        dataSource.setInitialSize(1);
        // risultato
        return dataSource;
    }

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

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

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

}

Una configurazione analoga è stata illustrata nel paragrafo 11.2.5. Vi abbiamo aggiunto le seguenti annotazioni Spring:

  • riga 17: l’annotazione [@EnableJpaRepositories] serve a indicare i pacchetti in cui si trovano le interfacce [CrudRepository] e [Spring Data];
  • riga 18: la classe è una classe di configurazione Spring. Questa informazione è importante. Se la si rimuove, il progetto funziona comunque. Tuttavia, più avanti nel documento, quando si realizzeranno progetti basati su questo, alcuni di essi non funzioneranno più se l’annotazione della riga 18 viene rimossa;
  • riga 19: l'annotazione [@ComponentScan] indica i pacchetti in cui si trovano gli oggetti Spring. Si tratta delle classi annotate con [@Component, @Service, @Controller, ...]. Qui il componente Spring [Dao] verrà individuato e istanziato;
  • righe 73-76: abbiamo definito un bean che rappresenta l’array dei pacchetti da scansionare per individuare le entità JPA. Ciò consentirà a un progetto che importa la classe [DaoConfig] di ridefinire questo bean e quindi di modificare i pacchetti analizzati alla riga 59. Più avanti nel documento affronteremo questa problematica;

La classe [AppConfig] configura l'intero progetto:


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

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

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

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

    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // mappatore jSON
        ObjectMapper mapper = new ObjectMapper();
        // filtri
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // risultato
        return mapper;
    }
}
  • riga 11: la classe è una classe di configurazione Spring;
  • riga 12: che importa i bean definiti dalla classe [DaoConfig] che abbiamo appena visto;
  • il livello [console] utilizza i mappatori jSON definiti qui;
  • righe 14-64: definiscono cinque mappatori jSON;
  • righe 15-18: il filtro jSON [jsonMapper] non ha filtri;
  • righe 20-30: il filtro jSON [jsonMapperCategorieWithProduits] consente di serializzare/deserializzare un oggetto [Categorie] con i relativi prodotti;
  • righe 32-42: il filtro jSON [jsonMapperProduitWithCategorie] consente di serializzare/deserializzare un oggetto [Produit] con la sua categoria;
  • righe 43-53: il filtro jSON [jsonMapperCategorieWithoutProduits] consente di serializzare/deserializzare un oggetto [Categorie] senza i relativi prodotti;
  • righe 55-64: il filtro jSON [jsonMapperProduitWithoutCategorie] consente di serializzare/deserializzare un oggetto [Produit] senza la sua categoria;

Si noti che, quando si crea un filtro jSON per un'entità T, è necessario configurare non solo il filtro dell'entità T, ma anche quelli delle entità Ti che essa stessa può contenere.

11.3.9. Il livello [console]

Livello

[DAO]

Livello

[console]

Strato

[JPA]

Pantaloni

[JDBC]

Strato

[Spring Data]

Spring 4

SGBD

  

La classe [Main] è la seguente:


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 {
            // istanziazione del contesto 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);
            // --------------------------------------------------------------------------------------
            // si svuota il database
            log("Vidage de la base de données", 1);
            // si svuota la tabella [CATEGORIES] - per effetto a cascata, la tabella [PRODUITS] verrà svuotata
            dao.deleteAllCategories();
            // --------------------------------------------------------------------------------------
            log("Remplissage de la base", 1);
            // si riempiono le tabelle
            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);
            }
            // aggiunta della categoria - per effetto a cascata verranno inseriti anche i prodotti
            dao.addCategories(categories);
            // --------------------------------------------------------------------------------------
            log("Affichage de la base", 1);
            // elenco delle categorie
            log("Liste des catégories", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            // elenco dei prodotti
            log("Liste des produits", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
            // categoria 1 con i relativi prodotti
            Categorie categorie = dao.getCategorieByNameWithProduits("categorie1");
            log("Catégorie 1 avec ses produits", 2);
            affiche(categorie, jsonMapperCategorieWithProduits);
            // il prodotto [produit14] con la relativa categoria
            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);
            // si svuota la tabella [CATEGORIES] - a cascata verrà svuotata la tabella [PRODUITS]
            dao.deleteAllCategories();
            // visualizzazione del database
            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);
            // si esegue l'inserimento
            categorie = new Categorie("cat1");
            categorie.addProduit(new Produit("x", 1.0, ""));
            categorie.addProduit(new Produit("x", 1.0, ""));
            // aggiunta della categoria - per effetto a cascata verranno inseriti anche i prodotti
            try {
                dao.addCategories(Lists.newArrayList(categorie));
            } catch (DaoException e) {
                System.out.println(e);
            }
            // verifica
            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) {
                // finito
                context.close();
            }
        }
        System.out.println("Travail terminé");
    }

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

    // visualizzazione di un elenco di elementi di tipo T
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }

    private static void log(String message, int mode) {
        // visualizza messaggio
        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);
    }
}
  • riga 25: istanziazione dei bean Spring a partire dalla classe di configurazione [AppConfig];
  • righe 26-33: recupero dei riferimenti ai mappatori jSON. Si utilizza la seguente firma del metodo [ApplicationContext].getBean:
    • [ApplicationContext].getBean(String id, Class classe): che viene utilizzato quando sono presenti più bean di tipo [classe]. In questo caso, si specifica l’identificatore del bean richiesto. Se quest’ultimo è stato definito con l’annotazione [@Bean], il suo identificatore è il nome del metodo annotato. Se è stato definito con l’annotazione [@Bean(« identifiant »], il suo identificatore è quello indicato nell’annotazione;
  • riga 34: recupero di un riferimento sul livello [DAO];
  • righe 37-39: svuotamento del database. Si svuota la tabella delle categorie (riga 39). Poiché è stato scritto:

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

quando una categoria viene eliminata, vengono eliminati anche tutti i prodotti ad essa collegati;

  • righe 43-53: inserimento nella tabella di 2 categorie da 5 prodotti ciascuna. Alla riga 50, l’inserimento delle due categorie provocherà contemporaneamente l’inserimento dei relativi prodotti, sempre perché abbiamo scritto [cascade = { CascadeType.ALL }];
  • riga 58: si visualizzano le categorie. Si utilizza il mappatore jSON [jsonMapperCategorieWithoutProduits] per visualizzare le categorie senza i relativi prodotti. Infatti, il metodo [dao.getAllCategories()] rende le categorie senza i relativi prodotti (lazy loading);
  • riga 61: vengono visualizzati i prodotti senza la loro categoria. Infatti, il metodo [dao.getAllProduits()] restituisce i prodotti senza la loro categoria (lazy loading);
  • righe 63-65: visualizzano la categoria denominata [categorie1] con i relativi prodotti (eager loading);
  • righe 67-69: visualizzano un prodotto con la relativa categoria;
  • righe 71-81: si aumenta del 10% tutti i prezzi dei prodotti della categoria [categorie1];
  • righe 91-101: si aggiunge una categoria con due prodotti dallo stesso nome. Tuttavia, nella tabella [PRODUITS] è presente un vincolo di unicità sulla colonna [NOM]. L'inserimento del secondo prodotto verrà quindi rifiutato e verrà generata un'eccezione. Tuttavia, il metodo [dao.addProduits] viene eseguito all’interno di una transazione. Il fatto che il secondo inserimento fallisca deve quindi annullare anche l’inserimento del primo prodotto e quello della relativa categoria [cat1]. È proprio questo che vogliamo verificare;
  • righe 119-121: un metodo generico in grado di visualizzare la stringa jSON di qualsiasi elemento di tipo T. La serializzazione jSON è controllata dal mappatore passato come parametro;
  • righe 124-128: un metodo analogo, questa volta per un elenco di elementi di tipo T;

L'esecuzione della classe [Main] fornisce i seguenti risultati (esclusi i log di 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é
  • righe 4-17: le categorie e i prodotti inseriti nella tabella;
  • righe 18-19: una categoria con i relativi prodotti;
  • righe 20-21: un prodotto con la relativa categoria;
  • righe 22-26: aggiornamento del prezzo di alcuni prodotti. Alla riga 24 si nota che i prezzi sono effettivamente aumentati del 10%;
  • righe 27-36: aggiunta della categoria [cat1] con due prodotti con lo stesso nome. Si nota che la tabella è identica prima (righe 28-29) e dopo l’aggiunta (righe 35-36), a dimostrazione del fatto che tutti gli inserimenti della transazione sono stati effettivamente annullati;
  • righe 31-34: l’eccezione verificatasi durante l’inserimento del secondo prodotto, che ha causato il fallimento dell’intera transazione;

11.3.10. Il test unitario JUnit

  

La classe [Test01] è la seguente:


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 {

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

    // filtri 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() {
        // si pulisce il database prima di ogni test
        log("Vidage de la base de données", 1);
        // si svuota la tabella [CATEGORIES] - di conseguenza, la tabella [PRODUITS] verrà svuotata
        dao.deleteAllCategories();
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // si compilano le tabelle
        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);
        }
        // Aggiunta della categoria - per effetto a cascata verranno inseriti anche i prodotti
        categories = dao.addCategories(categories);
    }

    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // elenco delle categorie
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories();
        affiche(categories, jsonMapperCategorieWithoutProduits);
        // elenco dei prodotti
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits();
        affiche(produits, jsonMapperProduitWithoutCategorie);
        // alcune verifiche
        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());
    }
...
    // -------------- metodi privati
    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;
    }

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

    // visualizzazione di un elenco di elementi di tipo T
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }

    private static void log(String message, int mode) {
        // visualizza messaggio
        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) {
        // titolo
        System.out.println(String.format("%s : ", title));
        // messaggi
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }

}
  • riga 27: il test unitario è configurato dalla classe [AppConfig] già presentata al paragrafo 11.3.8;
  • righe 32-33: inserimento di un riferimento sul livello [DAO];
  • righe 36-50: inserimento dei cinque mappatori jSON;
  • righe 60-71: dopo aver svuotato il database (riga 57), si popola il database con 2 categorie contenenti ciascuna 5 prodotti. Questo metodo viene eseguito prima di ogni test a causa dell'annotazione [@Before] della riga 52;
  • righe 75-93: visualizza il contenuto del database;
  • righe 95-101: richiede una categoria con i relativi prodotti, identificata dal nome;
  • righe 103-109: richiede una categoria senza i relativi prodotti, identificata dal nome;
  • righe 111-120: richiede un prodotto con la relativa categoria, identificato dal numero;
  • righe 122-130: richiede un prodotto senza la relativa categoria, identificato dal numero;
  • righe 133-184: metodi privati condivisi dai diversi test;

Lavoro da svolgere: eseguire il test. Deve avere esito positivo.


11.3.11. Gestione dei log

I log dell’applicazione console o del test JUnit sono configurati dal seguente file [logback.xml]:

  

Il file deve chiamarsi [logback.xml] e trovarsi nel Classpath del progetto. A tal fine, è stato inserito qui nella cartella [src/main/resources], che fa parte del Classpath. Il suo contenuto è il seguente:


<configuration> 

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- agli encoder viene assegnato per impostazione predefinita il tipo
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- controllo del livello dei log -->
  <root level="info"> <!-- info, debug, avviso -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>
  • riga 12: il tag [<root level="info">] visualizza i log di livello [info]. Al posto di [info], è possibile inserire:
    • [debug]: è il livello di log più dettagliato. Si consiglia di utilizzarlo durante la fase di debug del progetto poiché contiene log molto interessanti sugli scambi client/server. È un modo per capire cosa succede “dietro le quinte”;
    • [off]: nessun log;
    • [warn]: un livello intermedio di log in cui Spring segnala anomalie che non sono necessariamente errori. È necessario esaminarle se non si ottiene il risultato previsto;

Compito da svolgere: impostare il livello alla riga 12 su [debug], quindi eseguire il test unitario. Osservare la differenza nei log.


11.3.12. Generazione dell'archivio Maven del progetto

Per installare l’archivio del progetto nel repository locale di Maven, procedere come segue [1-3]:

L'archivio verrà generato utilizzando le credenziali presenti nel file [pom.xml]:


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

Il percorso del repository locale Maven è indicato nella configurazione di Eclipse:

 

È quindi possibile verificare la corretta installazione dell'artefatto Maven:

 

D'ora in poi, un altro progetto Maven locale potrà utilizzare questo archivio.