Skip to content

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

Parole chiave: architettura multilivello, Spring, iniezione di dipendenze, JPA (Java Persistence API), Spring Data.

Implementeremo il livello [DAO] dell'esercizio utilizzando [Spring Data], un componente dell'ecosistema Spring. [Spring Data] si basa su un livello JPA (Java Persistence API) che consente al livello [DAO] di manipolare oggetti anziché istruzioni SQL. In definitiva, il livello [DAO] non è consapevole di interagire con un database. Conosce solo l'interfaccia del livello [Spring Data].

Esploreremo innanzitutto [Spring Data] attraverso due esempi.

11.1. Supporto

  • In [1], la cartella [support / chap-11] contiene tre progetti Eclipse;
  • in [2], lo script SQL per la creazione del database di esempio per questo capitolo;

11.2. Esempio 1

Il sito web di Spring offre 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], selezioniamo il tutorial [Accessing Data Jpa], che illustra come accedere a un database utilizzando Spring Data;
  • In [3], selezioniamo un progetto configurato da Maven;
  • in [4], il tutorial è disponibile 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>
        <!-- use UTF-8 for everything -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • righe 5–9: definiscono un progetto Maven padre. Questo progetto definisce la maggior parte delle dipendenze del progetto. Potrebbero essere sufficienti, nel qual caso non vengono aggiunte dipendenze aggiuntive, oppure potrebbero non esserlo, nel qual caso vengono aggiunte le dipendenze mancanti;
  • righe 12–15: definiscono una dipendenza da [spring-boot-starter-data-jpa]. Questo artefatto contiene le classi Spring Data;
  • Righe 16–19: definiscono una dipendenza dal DBMS H2, che consente di creare e gestire database in memoria.

Diamo un'occhiata alle classi fornite da queste dipendenze:

Ce ne sono molte:

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

Le terremo tutte. Per un'applicazione di produzione, dovrebbero essere mantenute solo quelle necessarie.

Alla riga 26 del file [pom.xml], troviamo la riga:


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

Questa riga è collegata alle seguenti righe:


<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 [spring-boot-maven-plugin] consente di generare il JAR eseguibile dell'applicazione. La riga 26 del file [pom.xml] specifica quindi la classe eseguibile di questo JAR.

11.2.2. Il livello [JPA]

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

  

L'applicazione è semplice e gestisce le entità [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 ID [id], un nome [firstName] e un cognome [lastName]. Ogni istanza [Customer] rappresenta una riga in una tabella del database.

  • riga 8: annotazione JPA che garantisce che la persistenza delle istanze [Customer] (Creazione, Lettura, Aggiornamento, Eliminazione) sarà gestita da un'implementazione JPA. In base alle dipendenze Maven, possiamo vedere 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 DBMS in uso, in questo caso H2;

Non ci sono altre annotazioni JPA. Verranno quindi utilizzati i valori predefiniti:

  • la tabella [Customer] prenderà il nome dalla classe, ovvero [Customer];
  • le colonne di questa tabella prenderanno il nome dai campi della classe: [id, firstName, lastName], tenendo presente che nei nomi delle colonne della tabella non viene fatta distinzione tra maiuscole e minuscole;

Si noti che l'implementazione JPA utilizzata non viene mai denominata.

11.2.3. Il livello [Spring Data]

La classe [CustomerRepository] implementa il livello di accesso per la 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 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 possono essere eseguite su un tipo JPA T:

  • riga 8: il metodo save consente di salvare un'entità T nel database. Salva l'entità utilizzando la chiave primaria assegnatale dal DBMS. Consente inoltre di aggiornare un'entità T identificata dal suo ID di chiave primaria. La scelta tra queste due azioni dipende dal valore dell'ID della chiave primaria: se è nullo, viene eseguita l'operazione di salvataggio; altrimenti, viene eseguita l'operazione di aggiornamento;
  • riga 10: come sopra, ma per un elenco di entità;
  • riga 12: il metodo findOne recupera un'entità T identificata dal suo id della chiave primaria;
  • riga 22: il metodo delete consente di eliminare un'entità T identificata dal suo id della chiave primaria;
  • righe 24–28: varianti del metodo [delete];
  • riga 16: il metodo [findAll] recupera tutte le entità T persistenti;
  • riga 18: come sopra, ma limitato alle entità per le quali è stato fornito un elenco di 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 [Cliente] in base al suo [cognome];

E questo è tutto per il livello [DAO]. Non esiste una classe di implementazione per l'interfaccia precedente. Viene generata in fase di esecuzione da [Spring Data]. I metodi dell'interfaccia [CrudRepository] vengono implementati automaticamente. Per i metodi aggiunti all'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 utilizzando la seguente query JPQL (Java Persistence Query Language):

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

Pertanto, il tipo T deve avere un campo denominato [qualcosa]. Di conseguenza, il metodo

List<Customer> findByLastName(String lastName);

sarà 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] si riferisce al contesto di persistenza JPA. Ciò è possibile solo se la classe [Customer] ha un campo denominato [lastName], come in questo caso.

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

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 {
        // save a couple of customers
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));
 
        // fetch all customers
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : repository.findAll()) {
            System.out.println(customer);
        }
        System.out.println();
 
        // fetch an individual customer by ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();
 
        // fetch customers by last name
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : repository.findByLastName("Bauer")) {
            System.out.println(bauer);
        }
    }
 
}
  • riga 9: la classe implementa l'interfaccia [CommandLineRunner], che è un'interfaccia [Spring Boot] (riga 4). Questa interfaccia ha un solo metodo, quello alla riga 19;
  • riga 8: @SpringBootApplication è un'annotazione che raggruppa diverse annotazioni [Spring Boot]:
    • @Configuration: indica che la classe è una classe di configurazione;
    • @EnableAutoConfiguration: indica a [Spring Boot] di creare automaticamente una serie 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 utilizzando Hibernate. Poiché la libreria del DBMS H2 si trova nel Classpath, il bean [dataSource] verrà implementato utilizzando H2. Nel bean [dataSource], dobbiamo anche definire il nome utente e la password. Qui, Spring Boot utilizzerà l'amministratore H2 predefinito, che non ha password. Poiché la libreria [spring-tx] si trova nel Classpath, verrà utilizzato il gestore delle transazioni di Spring;
    • @EnableWebMvc: se la libreria [spring-mvc] si trova nel Classpath. In questo caso, viene eseguita la configurazione automatica per l'applicazione web;
    • @ComponentScan: indica a Spring dove cercare altri bean, configurazioni e servizi. Qui, vengono cercati per impostazione predefinita nel pacchetto contenente la classe annotata, ovvero il pacchetto [hello]. In questo modo, verranno trovate le classi [Customer] e [CustomerRepository]. Poiché la prima ha 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 avrà luogo tutto ciò che è stato spiegato in precedenza. Il risultato è un contesto applicativo Spring, ovvero un insieme di bean gestiti da Spring;

Le operazioni seguenti utilizzano semplicemente i metodi del bean che implementa l'interfaccia [CustomerRepository]. L'output della console è il seguente:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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 utilizzando la classe [LocalContainerEntityManagerFactory], una classe Spring;
  • riga 12: compare [Hibernate]. Si tratta dell'implementazione JPA che è stata scelta;
  • riga 19: un dialetto Hibernate è la variante SQL da utilizzare con il DBMS. Qui, il dialetto [H2Dialect] indica che Hibernate funzionerà con il DBMS H2;
  • righe 21–22: viene creato il database. Viene creata la tabella [CUSTOMER]. Ciò significa che Hibernate è stato configurato per generare tabelle 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 dalla 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 fornita da Spring Boot. Lo configureremo manualmente. Ciò può essere utile se le configurazioni predefinite non soddisfano le nostre esigenze.

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>
        <!-- H2 Database -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
    </dependencies>
 
    <properties>
        <!-- use UTF-8 for everything -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
    <repositories>
        <repository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/libs-release</url>
        </repository>
        <repository>
            <id>org.jboss.repository.releases</id>
            <name>JBoss Maven Release Repository</name>
            <url>https://repository.jboss.org/nexus/content/repositories/releases</url>
        </repository>
    </repositories>
 
    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/libs-release</url>
        </pluginRepository>
    </pluginRepositories>
 
</project>
  • righe 10–14: il progetto Maven principale di cui useremo le librerie;
  • righe 18–21: Spring Data utilizzato per accedere al database;
  • righe 23–26: l'implementazione Hibernate della specifica JPA;
  • righe 28–31: il DBMS H2;
  • righe 33–36: i database vengono spesso utilizzati con pool di connessioni, che evitano di aprire e chiudere ripetutamente le connessioni. In questo caso, l'implementazione utilizzata è [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) {
        // instantiation Spring context
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
 
        // save a couple of customers
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));
 
        ...
 
        // closing context
        context.close();
    }
}
  • riga 9: la classe [Application] non presenta più alcuna annotazione di configurazione;
  • righe 3–7: si noti che non ci sono più importazioni del pacchetto [Spring Boot];
  • riga 12: istanziamo i bean Spring. Otteniamo il contesto Spring, che contiene i riferimenti ai bean creati;
  • riga 13: richiediamo un riferimento al bean [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 {
 
    // h2 database
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration access JDBC
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        // an initially open connection
        dataSource.setInitialSize(1);
        // result
        return dataSource;
    }
 
    // the provider JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        hibernateJpaVendorAdapter.setDatabase(Database.H2);
        return hibernateJpaVendorAdapter;
    }

    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan("entities");
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }
 
    // Transaction manager
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }
 
}
  • riga 17: l'annotazione [@EnableTransactionManagement] indica che i metodi delle interfacce [CrudRepository] devono essere eseguiti all'interno di una transazione. È stata commentata perché questo è il comportamento predefinito;
  • riga 18: l'annotazione [@EnableJpaRepositories] specifica le directory in cui si trovano le interfacce [CrudRepository] di Spring Data. Queste interfacce diventeranno componenti Spring e saranno disponibili nel contesto Spring;
  • riga 19: l'annotazione [@Configuration] rende la classe [Config] una classe di configurazione Spring;
  • riga 20: l'annotazione [@ComponentScan] elenca le directory in cui devono essere cercati i componenti Spring. I componenti Spring sono classi annotate con annotazioni Spring come @Service, @Component, @Controller, ecc. Qui non ce ne sono altri oltre a quelli definiti all'interno della classe [AppConfig], quindi l'annotazione è stata commentata;
  • righe 24–37: definiscono l'origine 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 qui può essere qualsiasi cosa. Tuttavia, deve essere denominato [dataSource] se l'EntityManagerFactory alla riga 51 è assente e definita tramite configurazione automatica;
  • 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 cosa;
  • riga 43: nessun log SQL;
  • riga 44: il database verrà creato se non esiste;
  • righe 50–58: definiscono l'EntityManagerFactory che gestirà la persistenza JPA. Il metodo deve essere denominato [entityManagerFactory];
  • riga 51: il metodo riceve due parametri dei tipi dei due bean definiti in precedenza. Questi saranno poi costruiti e iniettati da Spring come parametri del metodo;
  • riga 53: imposta l'implementazione JPA da utilizzare;
  • riga 54: specifica le directory in cui si trovano le entità JPA;
  • riga 55: imposta l'origine dati da gestire;
  • righe 61–66: il gestore delle transazioni. Il metodo deve essere denominato [transactionManager]. Riceve il bean delle righe 51–58 come parametro;
  • riga 64: il gestore delle transazioni è associato all'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, il file del database H2:

  

11.2.6. Creazione di un archivio eseguibile

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

  • in [1]: creare una configurazione di runtime;
  • in [2]: di tipo [Applicazione Java]
  • in [3]: specificare il progetto da eseguire (utilizzare il pulsante Sfoglia);
  • in [4]: specificare la classe da eseguire;
  • in [5]: il nome della configurazione di esecuzione — può essere qualsiasi cosa;
  • in [6]: esporta il progetto;
  • in [7]: come archivio JAR eseguibile;
  • in [8]: specifica il percorso e il nome del file eseguibile da creare;
  • in [9]: il nome della configurazione di runtime creata in [5];
  • in [10], l'archivio creato;

Una volta fatto ciò, apri 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 come segue:


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

I risultati visualizzati 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 for further details.
mars 10, 2015 5:27:20 PM org.hibernate.jpa.internal.util.LogHelper logPersistenceUnitInformation
INFO: HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
mars 10, 2015 5:27:20 PM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {4.3.8.Final}
mars 10, 2015 5:27:20 PM org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
mars 10, 2015 5:27:20 PM org.hibernate.cfg.Environment buildBytecodeProvider
INFO: HHH000021: Bytecode provider name : javassist
mars 10, 2015 5:27:22 PM org.hibernate.annotations.common.reflection.java.JavaReflectionManager <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
mars 10, 2015 5:27:22 PM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
mars 10, 2015 5:27:22 PM org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory <init>
INFO: HHH000397: Using ASTQueryTranslatorFactory
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000228: Running hbm2ddl schema update
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000102: Fetching database metadata
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000396: Updating schema
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000232: Schema update complete
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

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

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

11.3. Esempio 2

11.3.1. Introduzione

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

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

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

La tabella [PRODUCTS] è la seguente:

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

Compito: creare il database [dbintrospringdata] utilizzando lo script SQL [dbintrospringdata.sql] presente nei materiali di supporto:


11.3.2. Creazione del progetto Maven

Per creare un modello di progetto Spring Data, segui questi passaggi:

  • In [1], crea un nuovo progetto;
  • In [2], selezionare il tipo [Spring Starter Project];
  • Il progetto generato sarà un progetto Maven. In [3], specificare il nome del gruppo di progetto;
  • In [4], specificare il nome dell'artefatto (un file JAR in questo caso) che verrà creato al momento della compilazione del progetto;
  • in [5]: il nome del progetto Eclipse – può essere qualsiasi cosa (non deve necessariamente coincidere con [4]);
  • in [7]: specificare che si sta creando un progetto con un livello [JPA] utilizzando il DBMS MySQL. Le dipendenze richieste per un progetto di questo tipo saranno quindi incluse nel file [pom.xml];
  • in [8], inserisci 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/> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>demo.IntroSpringData01Application</start-class>
        <java.version>1.7</java.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
</project>
  • righe 14–19: il progetto Maven principale — definisce un gran numero di librerie con le relative versioni — utilizziamo queste librerie come dipendenze Maven senza specificarne le versioni;
  • righe 28–31: la dipendenza richiesta per JPA – includerà [Spring Data];
  • righe 32–36: la dipendenza dal driver JDBC di MySQL;
  • righe 37–41: le dipendenze richieste per i test JUnit integrati con Spring;

La classe eseguibile [Application] non fa nulla 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 configurazione automatica del progetto;

La classe di test [ApplicationTests] non fa nulla 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 potrà così beneficiare di tutti i bean definiti in questo file;
  • riga 8: l'annotazione [@RunWith] consente l'integrazione di Spring con JUnit: la classe può essere eseguita come test JUnit. [@RunWith] è un'annotazione JUnit (riga 4), mentre la classe [SpringJUnit4ClassRunner] è una classe Spring (riga 6);

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

11.3.3. Il progetto Eclipse

Amplieremo il progetto precedente come segue:

  
  • [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 [Product] e [Category];
  • [Product.java]: classe associata a una riga nella tabella [PRODUCTS] nel database;
  • [Category.java]: classe associata a una riga nella tabella [CATEGORIES] del database;
  • [ProductsRepository]: l'interfaccia Spring Data per l'accesso alla tabella [PRODUCTS];
  • [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] per il 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>
        <!-- library jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- Google Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- log library -->
        <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 nella sezione 11.2.5. Aggiungiamo le seguenti librerie:

  • righe 42–49: una libreria JSON utilizzata dal metodo [toString] della classe [Product];
  • 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 logging;
  • righe 81–86: i plugin Maven necessari per il progetto;

11.3.5. Entità del livello [JPA]

Livello [DAO] Livello [Console] Livello [JPA] Driver [JDBC] Livello [Spring Data] Spring 4 DBMS

  

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 {
    // properties
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    protected Long id;
    @Version
    @Column(name = "VERSION")
    protected Long version;
 
    // manufacturers
    public AbstractEntity() {
 
    }
 
    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }
 
    // redefine [equals] and [hashcode]
    @Override
    public int hashCode() {
        return (id != null ? id.hashCode() : 0);
    }
 
    @Override
    public boolean equals(Object entity) {
        if (!(entity instanceof AbstractEntity)) {
            return false;
        }
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return id != null && this.id.longValue() == other.id.longValue();
    }
 
    // signature jSON
    public String toString() {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
 
    // getters and setters
....
}
 

Lo scopo di questa classe è fornire una classe padre per le entità JPA incapsulando in un unico punto le proprietà [id, version] (righe 19, 22) comuni alle entità [Product] e [Category] collegate al database. Queste proprietà sono collegate 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 nome diverso) è associato alla chiave primaria di una tabella;
  • riga 17: l'annotazione [@GeneratedValue(strategy=GenerationType.IDENTITY)] imposta la modalità di generazione della chiave primaria. La modalità [GenerationType.IDENTITY] utilizzerà la modalità [AUTO_INCREMENT] con MySQL. Con un altro DBMS, questa modalità utilizzerebbe un metodo diverso. Il vantaggio è che lo sviluppatore non deve preoccuparsene e il suo codice rimane valido indipendentemente dal DBMS utilizzato;
  • riga 18: l'annotazione [@Column] specifica la colonna associata al campo. Quando questa annotazione non è presente, JPA presume che la colonna abbia lo stesso nome del campo. È il caso qui. Pertanto, avremmo potuto omettere 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à viene modificata. Questo numero viene utilizzato per impedire aggiornamenti simultanei 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 questa modifica nel database: il numero di versione diventa quindi V1+1. A sua volta, U2 modifica E e salva questa modifica nel database: riceverà un'eccezione perché la sua versione (V1) differisce da quella nel database (V1+1);
  • righe 35–52: ridefinizione dei metodi [hashCode] e [equals]. Per impostazione predefinita, [obj1.equals(obj2)] restituisce true se [obj1 == obj2], ovvero se obj1 e obj2 sono due puntatori uguali. Se vogliamo confrontare gli oggetti a cui puntano i puntatori piuttosto che i puntatori stessi, dobbiamo sovrascrivere 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 di tipi 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 che hanno quindi la stessa chiave primaria [id];
  • righe 55-63: il metodo [toString] restituisce la stringa JSON dell'oggetto [this]. Se questo oggetto fa riferimento a una classe figlia, questo metodo restituirà la stringa JSON della classe figlia. Ciò elimina la necessità di creare un metodo [toString] nelle classi figlie;

11.3.5.2. L'entità JPA [Product]

La classe [Product] è un'entità JPA associata a una riga nella tabella [PRODUCTS]:

 

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 {
 
    // properties
    @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;
 
    // the category
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CATEGORIE_ID")    
    private Categorie categorie;
 
    // manufacturers
    public Produit() {
 
    }
 
    public Produit(String nom, double prix, String description) {
        this.nom = nom;
        this.prix = prix;
        this.description = description;
    }
 
    // getters and setters
...
}
  • riga 12: l'annotazione [@Entity] rende la classe [Product] un'entità gestita dal livello [JPA];
  • Riga 13: l'annotazione [@Table(name = "PRODUCTS")] indica che la classe [Product] rappresenta una riga nella tabella [PRODUCTS] del database;
  • Riga 14: il nome del filtro JSON da applicare all'entità. Vedremo che la proprietà [categorie] alla riga 13 non è sempre disponibile. Deve quindi essere esclusa dalla rappresentazione JSON dell'oggetto. Per farlo, abbiamo bisogno di un filtro. Specificheremo quindi se vogliamo o meno la proprietà [categorie] in un filtro denominato [jsonFilterCategorie];
  • riga 18: l'annotazione [@Column] associa il campo [nom] alla colonna [NOM] nella tabella [PRODUITS]. Quando il campo ha lo stesso nome della colonna associata, l'annotazione [@Column] può essere omessa. Sarebbe il caso qui;
  • righe 31–33: la categoria del prodotto;
  • riga 31: l'annotazione [@ManyToOne] indica che la colonna a cui fa riferimento l'annotazione alla riga 32 [@JoinColumn(name = "CATEGORIE_ID")] è una chiave esterna dalla tabella [PRODUCTS] dell'entità [Product] alla tabella [CATEGORIES] associata all'entità alla riga 33. Questa annotazione deve essere applicata a un'entità JPA. Pertanto, la classe alla riga 33 deve essere un'entità JPA;
  • Riga 31: L'annotazione [fetch = FetchType.LAZY] specifica che quando un prodotto viene recuperato dalla tabella [PRODUCTS], la sua categoria (riga 33) non viene recuperata immediatamente (caricamento pigro). Viene quindi ottenuta durante la prima chiamata al metodo [getCategory]. Questo attributo non è obbligatorio. L'implementazione JPA utilizzata può ignorarlo. È proprio perché la proprietà [category] 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 potenzia il metodo [getCategory] iniziale (che restituisce semplicemente il campo category) effettuando una chiamata al DBMS per recuperare la categoria. Affinché ciò funzioni, la connessione al DBMS inizialmente utilizzata per recuperare il prodotto deve essere ancora aperta; in caso contrario, si verifica un'eccezione.

11.3.5.3. L'entità JPA [Category]

La classe [Category] è un'entità JPA associata a una riga nella 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 {
 
    // properties
    @Column(name = "NOM")
    private String nom;
 
    // related products
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
    public Set<Produit> produits = new HashSet<Produit>();
 
    // manufacturers
    public Categorie() {
 
    }
 
    public Categorie(String nom) {
        this.nom = nom;
    }
 
    // methods
    public void addProduit(Produit produit) {
        // we add the product
        produits.add(produit);
        // set your category
        produit.setCategorie(this);
    }
 
    // getters and setters
...
}
  • righe 21-22: il nome della categoria;
  • righe 25-26: i prodotti in questa categoria;
  • riga 25: l'annotazione [@OneToMany] rappresenta la relazione inversa della relazione [@ManyToOne] che abbiamo incontrato nell'entità [Product]. L'attributo [mappedBy = "category"] specifica il campo nell'entità [Product] annotato dalla relazion e inversa [@ManyToOne]. L'attributo [cascade = { CascadeType.ALL }] specifica che le operazioni (persist, merge, remove) eseguite su un'entità @Entity [Category] devono propagarsi a cascata ai [products] alla 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 una categoria viene recuperata dalla tabella [CATEGORIES], i suoi prodotti non vengono recuperati immediatamente. Verranno recuperati durante la prima chiamata al metodo [getProduits]. Le implementazioni JPA esistenti (Hibernate, Eclipselink, OpenJPA) non gestiscono questa annotazione allo stesso modo. Hibernate potenzia il metodo [getProduits] iniziale (che restituisce semplicemente il campo products) effettuando una chiamata al DBMS per recuperare i prodotti della categoria. Affinché ciò sia possibile, la connessione al DBMS inizialmente utilizzata per recuperare la categoria deve essere ancora aperta. Questo attributo è obbligatorio. L'implementazione JPA non può ignorarlo. Poiché la proprietà [products] può essere inizializzata o meno, abbiamo introdotto il filtro JSON alla riga 17, che ci permette di specificare se vogliamo o meno questa proprietà;
  • Riga 26: Il tipo [Set] è un'interfaccia. Il tipo [HashSet] è una classe che implementa questa interfaccia. Implementa una raccolta di elementi chiamata set. Un set non può contenere due oggetti identici. Qui, gli oggetti sono di tipo [Product]. Pertanto, all'interno del set, non possiamo avere due oggetti identici. Poiché il metodo [equals] della classe padre [AbstractEntity] è stato sovrascritto per stabilire che due prodotti sono identici se hanno la stessa chiave primaria, il campo [products] non può contenere due prodotti con la stessa chiave primaria;
  • righe 38–43: il metodo [addProduct] consente di aggiungere un prodotto alla categoria;

11.3.6. Il livello [Spring Data]

Livello [DAO] Livello [Console] Livello [JPA] Driver [JDBC] Livello [Spring Data] Spring 4DBMS

  

L'interfaccia [CategoriesRepository] gestisce l'accesso 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> {
 
    // categorie avec ses produits
    @Query("select c from Categorie c left join fetch c.produits p where c.id=?1")
    public Categorie getCategorieByIdWithProduits(Long id);
 
    @Query("select c from Categorie c left join fetch c.produits p where c.nom=?1")
    public Categorie getCategorieByNameWithProduits(String nom);
 
    // une catégorie sans ses produits désignée par son nom
    public Categorie findByNom(String nom);
}
  • Riga 8: L'interfaccia [CrudRepository] è stata utilizzata e spiegata nella Sezione 11.2.3. Ricordiamo che:
    • il primo tipo dell'interfaccia è l'entità JPA gestita per le operazioni CRUD (findOne, findAll, save, delete, deleteAll),
    • il secondo tipo è la chiave primaria dell'entità JPA, in questo caso un numero intero [Long];
  • riga 12: il metodo alla riga 12 è implementato dalla query JPQL (Java Persistence Query Language) alla riga 11. Questa query recupera le entità JPA. In tale query:
    • le tabelle sono sostituite dalle entità JPA associate;
    • le colonne sono sostituite dai campi delle entità JPA utilizzate nella query;
  • riga 11: la query JPQL restituisce una categoria insieme ai suoi prodotti. Ricordiamo che nell'entità [Category], il campo [products] aveva l'attributo [fetch = FetchType.LAZY] (caricamento pigro). Nella query JPQL, forziamo il caricamento dei prodotti utilizzando la parola chiave [fetch]. Il parametro ?1 della query verrà sostituito in fase di esecuzione dal valore del primo parametro del metodo alla riga 12, ovvero il parametro [Long id];
  • Righe 14–15: un metodo simile per una categoria identificata dal suo nome;
  • riga 18: il metodo [findByName] verrà implementato automaticamente da [Spring Data] poiché il tipo [Category] ha un campo [name];

L'interfaccia [ProductsRepository] gestisce l'accesso alla tabella [PRODUCTS]:


package spring.data.repositories;
 
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
 
import spring.data.entities.Produit;
 
public interface ProduitsRepository extends CrudRepository<Produit, Long> {
 
    // un produit avec sa catégorie
    @Query("select p from Produit p left join fetch p.categorie c where p.id=?1")
    public Produit getProduitByIdWithCategorie(Long id);
 
    @Query("select p from Produit p left join fetch p.categorie c where p.nom=?1")
    public Produit getProduitByNameWithCategorie(String nom);
 
    // un produit sans sa catégorie désigné par son nom
    public Produit findByNom(String nom);
}

Le spiegazioni sono le stesse dell'interfaccia [CategoriesRepository].

Queste interfacce saranno implementate da classi generate da [Spring Data] all'esecuzione del progetto. Tali classi sono chiamate [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] Livello [JPA] Driver [JDBC] Livello [Spring Data] Spring 4 DBMS

  

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 {

    // insert product list
    public List<Produit> addProduits(List<Produit> produits);
 
    // removal of all products
    public void deleteAllProduits();
 
    // product list update
    public List<Produit> updateProduits(List<Produit> produits);
 
    // all products obtained
    public List<Produit> getAllProduits();
 
    // inserting a list of categories
    public List<Categorie> addCategories(List<Categorie> categories);
 
    // delete all categories
    public void deleteAllCategories();
 
    // updating a list of categories
    public List<Categorie> updateCategories(List<Categorie> categories);
 
    // obtaining all categories
    public List<Categorie> getAllCategories();
 
    // a specific product with or without its category
    public Produit getProduitByIdWithoutCategorie(Long idProduit);
 
    public Produit getProduitByIdWithCategorie(Long idProduit);
 
    public Produit getProduitByNameWithCategorie(String nom);
 
    public Produit getProduitByNameWithoutCategorie(String nom);
 
    // a particular category with or without its products
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie);
 
    public Categorie getCategorieByIdWithProduits(Long idCategorie);
 
    public Categorie getCategorieByNameWithProduits(String nom);
 
    public Categorie getCategorieByNameWithoutProduits(String nom);
}

Qui abbiamo adottato la regola secondo cui qualsiasi metodo che modifica gli oggetti passati come parametri di input deve restituirli nel proprio risultato. Il motivo di questa regola è stato spiegato nella Sezione 4.2: essa consente a un livello e al suo client di risiedere in due JVM separate e quindi di operare in una configurazione 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 di riferimenti nelle due interfacce [CrudRepository] da [Spring Data]. Questa iniezione avviene durante l'istanziazione degli oggetti Spring, in genere all'inizio dell'esecuzione del progetto Spring;
  • Si noti nelle righe 28 e 46 che il metodo [save] dell'interfaccia [productsRepository] viene utilizzato sia per l'inserimento che per l'aggiornamento dei prodotti. [Spring Data] utilizza la chiave primaria del prodotto per determinare se eseguire un inserimento o un aggiornamento. Se la chiave primaria è [null], si tratterà di un inserimento; altrimenti, si tratterà di un aggiornamento;
  • Riga 82: utilizziamo il metodo [Lists.newArrayList] della libreria Guava per ottenere un elenco di prodotti. Il metodo [productsRepository.findAll()] restituisce un tipo [Iterable<Product>];
  • riga 28: il metodo [productsRepository.save(products)] restituisce un [Iterable<Product>]. Lo stesso vale per le altre operazioni [save] nella 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;
 
// exception class for the Elections application
// the exception is uncontrolled
 
public class DaoException extends RuntimeException implements Serializable {
 
    // serial ID
    private static final long serialVersionUID = 1L;
 
    // local fields
    private int code;
    private List<String> erreurs;
 
    // manufacturers
    public DaoException() {
        super();
    }
 
    public DaoException(int code, Throwable e) {
        // parent
        super(e);
        // local
        this.code = code;
        this.erreurs = getErreursForException(e);
    }
 
    public DaoException(int code, String message, Throwable e) {
        // parent
        super(message, e);
        // local
        this.code = code;
        this.erreurs = getErreursForException(e);
    }
 
    public DaoException(int code, String message) {
        // parent
        super(message);
        // local
        this.code = code;
        List<String> erreurs = new ArrayList<>();
        erreurs.add(message);
        this.erreurs = erreurs;
    }
 
    public DaoException(int code, List<String> erreurs) {
        // parent
        super();
        // local
        this.code = code;
        this.erreurs = erreurs;
    }
 
    // list of exception error messages
    private List<String> getErreursForException(Throwable th) {
        // retrieve the list of exception error messages
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // the message is retrieved only if it is !=null and not blank
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // next cause
            cause = cause.getCause();
        }
        return erreurs;
    }
 
    // getters and setters
...
}
  • riga 10: la classe estende la classe [RuntimeException] ed è quindi un'eccezione non gestita;
  • riga 16: un codice di errore;
  • riga 17: un elenco di messaggi di errore associati allo stack di eccezioni che ha causato la [DaoException];
  • righe 59–76: il metodo privato [getMessagesForException] recupera l'elenco dei messaggi di errore associati alle eccezioni 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 da incapsulare;
    • Exception(Throwable cause): crea un'eccezione contenente l'eccezione da incapsulare;

Il tipo [Throwable] è la classe padre della classe [Exception]. Se i costruttori precedenti vengono eseguiti ripetutamente, l'eccezione finale conterrà più eccezioni. Questo viene definito stack di eccezioni.

  • L'ultima causa di un'eccezione e1 si ottiene con l'espressione [e1.getCause()];
  • La penultima causa di un'eccezione e1 si ottiene utilizzando l'espressione [e1.getCause().getCause()];
  • questo processo continua fino a quando non si ottiene [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 {
 
    // constants
    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" };
 
    // the [tomcat-jdbc] data source
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration access JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // an initially open connection
        dataSource.setInitialSize(1);
        // result
        return dataSource;
    }
 
    // the provider JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        return hibernateJpaVendorAdapter;
    }
 
    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(packagesToScan());
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

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

Una configurazione simile è stata discussa e spiegata nella Sezione 11.2.5. Abbiamo aggiunto le seguenti annotazioni Spring:

  • riga 17: l'annotazione [@EnableJpaRepositories] viene utilizzata per indicare i pacchetti in cui si trovano le interfacce [CrudRepository] di [Spring Data];
  • riga 18: la classe è una classe di configurazione Spring. Questa informazione è importante. Se la rimuoviamo, il progetto continua a funzionare. Tuttavia, più avanti nel documento, quando realizzeremo progetti che si basano su questo, alcuni di essi non funzioneranno più se l’annotazione alla riga 18 viene rimossa;
  • riga 19: l'annotazione [@ComponentScan] specifica i pacchetti in cui si trovano gli oggetti Spring. Si tratta delle classi annotate con [@Component, @Service, @Controller, ...]. Qui verrà individuato e istanziato il componente Spring [Dao];
  • Righe 73–76: Abbiamo definito un bean che rappresenta l'array di pacchetti da scansionare alla ricerca di entità JPA. Ciò consentirà a un progetto che importa la classe [DaoConfig] di ridefinire questo bean e quindi di modificare i pacchetti scansionati (riga 59). Incontreremo questo problema più avanti nel documento;

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 {
    // filters jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }
 
    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // result
        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 mappatore JSON [jsonMapper] non ha filtri;
  • righe 20–30: il filtro JSON [jsonMapperCategoryWithProducts] consente di serializzare/deserializzare un oggetto [Category] insieme ai suoi prodotti;
  • righe 32–42: il filtro JSON [jsonMapperProductWithCategory] permette di serializzare/deserializzare un oggetto [Product] con la sua categoria;
  • righe 43-53: il filtro JSON [jsonMapperCategorieWithoutProduits] consente di serializzare/deserializzare un oggetto [Categorie] senza i suoi prodotti;
  • righe 55–64: il filtro JSON [jsonMapperProductWithoutCategory] consente di serializzare/deserializzare un oggetto [Product] senza la sua categoria;

Si noti che quando si crea un filtro JSON per un'entità T, è necessario configurare non solo il filtro per l'entità T, ma anche quelli per le entità Ti che essa potrebbe contenere.

11.3.9. Il livello [console]

Livello[DAO]Livello[console]Livello[JPA]Driver[JDBC]Livello[Spring Data]Spring 4DBMS

  

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 {
            // instantiation Spring context
            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);
            // --------------------------------------------------------------------------------------
            // empty the database
            log("Vidage de la base de données", 1);
            // table [CATEGORIES] is emptied - by cascade, table [PRODUITS] will be emptied
            dao.deleteAllCategories();
            // --------------------------------------------------------------------------------------
            log("Remplissage de la base", 1);
            // fill the tables
            List<Categorie> categories = new ArrayList<Categorie>();
            for (int i = 0; i < 2; i++) {
                Categorie categorie = new Categorie(String.format("categorie%d", i));
                for (int j = 0; j < 5; j++) {
                    categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                            String.format("desc%d%d", i, j)));
                }
                categories.add(categorie);
            }
            // add the category - the products will be cascaded in as well
            dao.addCategories(categories);
            // --------------------------------------------------------------------------------------
            log("Affichage de la base", 1);
            // list of categories
            log("Liste des catégories", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            // product list
            log("Liste des produits", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
            // category 1 with its products
            Categorie categorie = dao.getCategorieByNameWithProduits("categorie1");
            log("Catégorie 1 avec ses produits", 2);
            affiche(categorie, jsonMapperCategorieWithProduits);
            // the product [product14] with its category
            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);
            // table [CATEGORIES] is emptied - by cascade, table [PRODUITS] will be emptied
            dao.deleteAllCategories();
            // base display
            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);
            // we insert
            categorie = new Categorie("cat1");
            categorie.addProduit(new Produit("x", 1.0, ""));
            categorie.addProduit(new Produit("x", 1.0, ""));
            // add the category - the products will be cascaded in as well
            try {
                dao.addCategories(Lists.newArrayList(categorie));
            } catch (DaoException e) {
                System.out.println(e);
            }
            // check
            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) {
                // finish
                context.close();
            }
        }
        System.out.println("Travail terminé");
    }
 
    // display of a T-type element
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }
 
    // display a list of elements of type T
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }
 
    private static void log(String message, int mode) {
        // poster message
        String toPrint = null;
        switch (mode) {
        case 1:
            toPrint = String.format("%s --------------------------------", message);
            break;
        case 2:
            toPrint = String.format("-- %s", message);
            break;
        }
        System.out.println(toPrint);
    }
}
  • riga 25: istanziazione dei bean Spring dalla classe di configurazione [AppConfig];
  • righe 26–33: recupero dei riferimenti ai mappatori JSON. Utilizziamo la seguente firma del metodo [ApplicationContext].getBean:
    • [ApplicationContext].getBean(String id, Class class): che viene utilizzato quando sono presenti più bean di tipo [class]. In questo caso, specifichiamo l'identificatore del bean richiesto. Se è stato definito con l'annotazione [@Bean], il suo identificatore è il nome del metodo annotato. Se è stato definito con l'annotazione [@Bean("identifier")], il suo identificatore è quello specificato nell'annotazione;
  • riga 34: recupero di un riferimento dal livello [DAO];
  • righe 37–39: cancellazione del database. Cancelliamo la tabella delle categorie (riga 39). Poiché abbiamo 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: Popolamento della tabella con 2 categorie, ciascuna contenente 5 prodotti. Alla riga 50, l'inserimento delle due categorie inserirà contemporaneamente i loro prodotti, sempre perché abbiamo scritto [cascade = { CascadeType.ALL }];
  • riga 58: visualizziamo le categorie. Utilizziamo il mappatore JSON [jsonMapperCategorieWithoutProduits] per visualizzare le categorie senza i relativi prodotti. Infatti, il metodo [dao.getAllCategories()] restituisce le categorie senza i relativi prodotti (caricamento differito);
  • riga 61: visualizziamo i prodotti senza la loro categoria. Questo perché il metodo [dao.getAllProduits()] restituisce i prodotti senza la loro categoria (caricamento differito);
  • righe 63–65: visualizziamo la categoria denominata [categorie1] con i relativi prodotti (eager loading);
  • righe 67–69: visualizza un prodotto con la sua categoria;
  • righe 71–81: tutti i prezzi dei prodotti nella categoria [categorie1] vengono aumentati del 10%;
  • righe 91-101: aggiungono una categoria con due prodotti con lo stesso nome. Tuttavia, nella tabella [PRODUCTS], esiste un vincolo di unicità sulla colonna [NAME]. L'inserimento del secondo prodotto verrà quindi rifiutato e verrà generata un'eccezione. Tuttavia, il metodo [dao.addProducts] viene eseguito all'interno di una transazione. Il fatto che il secondo inserimento fallisca deve quindi comportare anche il rollback dell'inserimento del primo prodotto e di quello della loro categoria [cat1]. Questo è ciò che vogliamo verificare;
  • righe 119–121: un metodo generico in grado di visualizzare la stringa JSON per qualsiasi elemento di tipo T. La serializzazione JSON è controllata dal mapper passato come parametro;
  • righe 124–128: un metodo simile, questa volta per un elenco di elementi di tipo T;

L'esecuzione della classe [Main] produce 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 sua categoria;
  • righe 22–26: aggiornamento dei prezzi per alcuni prodotti. Alla riga 24, possiamo vedere che i prezzi sono effettivamente aumentati del 10%;
  • Righe 27–36: aggiunta della categoria [cat1] con due prodotti con lo stesso nome. Possiamo vedere che la tabella è la stessa prima (righe 28–29) e dopo l'aggiunta (righe 35–36), dimostrando così che tutti gli inserimenti nella 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 {
 
    // layer [DAO]
    @Autowired
    private IDao dao;
 
    // filters 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() {
        // the base is cleaned before each test
        log("Vidage de la base de données", 1);
        // table [CATEGORIES] is emptied - by cascade, table [PRODUITS] will be emptied
        dao.deleteAllCategories();
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // fill the tables
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < 2; i++) {
            Categorie categorie = new Categorie(String.format("categorie%d", i));
            for (int j = 0; j < 5; j++) {
                categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                        String.format("desc%d%d", i, j)));
            }
            categories.add(categorie);
        }
        // add the category - the products will be cascaded in as well
        categories = dao.addCategories(categories);
    }
 
    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // list of categories
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories();
        affiche(categories, jsonMapperCategorieWithoutProduits);
        // product list
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits();
        affiche(produits, jsonMapperProduitWithoutCategorie);
        // a few checks
        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());
    }
...
    // -------------- private methods
    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;
    }
 
    // display of a T-type element
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }
 
    // display a list of elements of type T
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }
 
    private static void log(String message, int mode) {
        // poster message
        String toPrint = null;
        switch (mode) {
        case 1:
            toPrint = String.format("%s --------------------------------", message);
            break;
        case 2:
            toPrint = String.format("-- %s", message);
            break;
        }
        System.out.println(toPrint);
    }
 
    private static void show(String title, List<String> messages) {
        // title
        System.out.println(String.format("%s : ", title));
        // messages
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }
 
}
  • riga 27: il test unitario è configurato dalla classe [AppConfig] già presentata nella sezione 11.3.8;
  • righe 32–33: iniezione di un riferimento al livello [DAO];
  • righe 36–50: iniezione dei cinque mappatori JSON;
  • righe 60–71: dopo aver svuotato il database (riga 57), il database viene popolato con 2 categorie, ciascuna contenente 5 prodotti. Questo metodo viene eseguito prima di ogni test grazie all'annotazione [@Before] alla riga 52;
  • righe 75–93: visualizza il contenuto del database;
  • righe 95–101: recupera una categoria insieme ai suoi prodotti, identificata dal suo nome;
  • righe 103–109: recupera una categoria senza i suoi prodotti, identificata dal suo nome;
  • righe 111–120: recupera un prodotto insieme alla sua categoria, identificato dal suo ID;
  • righe 122–130: recupera un prodotto senza la sua categoria, identificato dal suo numero;
  • righe 133–184: metodi privati condivisi dai vari test;

Lavoro da svolgere: eseguire il test. Dovrebbe superare il test.


11.3.11. Gestione dei log

I log per l'applicazione console o il test JUnit sono configurati dal seguente file [logback.xml]:

  

Il file deve essere denominato [logback.xml] e trovarsi nel classpath del progetto. Per garantire ciò, è 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"> 
    <!-- encoders are  by default assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>
 
  <!-- log level control -->
  <root level="info"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>
  • riga 12: il tag [<root level="info">] visualizza i log di livello [info]. Al posto di [info], è possibile utilizzare:
    • [debug]: questo è il livello di log più dettagliato. Si consiglia di utilizzarlo durante la fase di debug del progetto perché fornisce log molto utili sulle interazioni client/server. Questo è un modo per capire cosa sta succedendo "dietro le quinte";
    • [off]: nessun log;
    • [warn]: un livello di registrazione intermedio in cui Spring visualizza anomalie che non sono necessariamente errori. È consigliabile esaminarle se non si ottiene il risultato previsto;

Compito: 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 Maven locale, segui questi passaggi [1-3]:

L'archivio verrà generato utilizzando gli identificatori 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>

La posizione del repository Maven locale è indicata nella configurazione di Eclipse:

 

È quindi possibile verificare che l'artefatto Maven sia stato installato correttamente:

 

Ora, un altro progetto Maven locale può utilizzare questo archivio.