Skip to content

11. [Cours]: Gestão de bases de dados relacionais com Spring Data

Palavras-chave: arquitetura multicamadas, Spring, injeção de dependências, API JPA (Java Persistence API), Spring Data.

Vamos implementar a camada [DAO] do TD com o [Spring Data], um ramo do ecossistema Spring. O [Spring Data] baseia-se numa camada JPA (Java Persistence API) que permite à camada [DAO] manipular objetos em vez de comandos SQL. Em última análise, a camada [DAO] ignora que está a interagir com uma base de dados. Conhece apenas a interface da camada [Spring Data].

Vamos primeiro explorar o [Spring Data] através de dois exemplos.

11.1. Support

  • em [1], a pasta [support / chap-11] contém três projetos Eclipse;
  • em [2], o script SQL que permite criar a base de dados de exemplo deste capítulo;

11.2. Exemplo 1

No site do Spring existem vários tutoriais para dar os primeiros passos com o Spring [http://spring.io/guides]. Vamos utilizar um deles para apresentar o Spring Data. Para tal, utilizamos o Spring Tool Suite (STS).

  • em [1], importamos um dos tutoriais de [spring.io/guides];
  • em [2], selecionamos o tutorial [Accessing Data Jpa] que mostra como aceder a uma base de dados com o Spring Data;
  • No [3], escolhe-se um projeto configurado pelo Maven;
  • em [4], o tutorial pode ser apresentado de duas formas: [initial], que é uma versão em branco que se preenche seguindo o tutorial, ou [complete], que é a versão final do tutorial. Escolhemos esta última;
  • em [5], é possível optar por visualizar o tutorial num navegador;
  • em [6], o projeto final.

11.2.1. A configuração Maven do projeto

As dependências Maven do projeto estão configuradas no ficheiro [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>
        <!-- utilizar UTF-8 para tudo -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • linhas 5-9: definem um projeto pai do Maven. É este que define a maior parte das dependências do projeto. Estas podem ser suficientes, caso em que não se adicionam mais, ou não, caso em que se adicionam as dependências em falta;
  • linhas 12-15: definem uma dependência do [spring-boot-starter-data-jpa]. Este artefacto contém as classes do Spring Data;
  • linhas 16-19: definem uma dependência do SGBD e do H2, que permitem criar e gerir bases de dados em memória.

Vejamos as classes fornecidas por estas dependências:

São muitas:

  • algumas pertencem ao ecossistema Spring (as que começam por «spring»);
  • outras pertencem ao ecossistema Hibernate (hibernate, jboss), cuja implementação JPA é aqui utilizada;
  • outras são bibliotecas de testes (junit, hamcrest);
  • outras são bibliotecas de registos (log4j, logback, slf4j);

Vamos mantê-las todas. Para uma aplicação em produção, seria necessário manter apenas as que são necessárias.

Na linha 26 do ficheiro [pom.xml], encontra-se a linha:


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

Esta linha está relacionada com as seguintes linhas:


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

Nas linhas 6 a 9, o plugin [spring-boot-maven-plugin] permite gerar o ficheiro JAR executável da aplicação. A linha 26 do ficheiro [pom.xml] indica, então, a classe executável desse ficheiro JAR.

11.2.2. A camada [JPA]

O acesso à base de dados é feito através de uma camada [JPA], Java Persistence API:

  

A aplicação é básica e gere clientes [Customer]. A classe [Customer] faz parte da camada [JPA] e é a seguinte:


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

}

Um cliente tem um identificador [id], um nome próprio [firstName] e um apelido [lastName]. Cada instância [Customer] representa uma linha de uma tabela da base de dados.

  • linha 8: anotação JPA que faz com que a persistência das instâncias [Customer] (Create, Read, Update, Delete) venha a ser gerida por uma implementação JPA. De acordo com as dependências do Maven, verifica-se que é utilizada a implementação JPA / Hibernate;
  • linhas 11-12: anotações JPA que associam o campo [id] à chave primária da tabela [Customer]. A linha 12 indica que a implementação JPA utilizará o método de geração da chave primária específico do SGBD utilizado, neste caso o H2;

Não existem outras anotações para JPA. Serão, então, utilizados valores por predefinição:

  • a tabela [Customer] terá o nome da classe, ou seja, [Customer];
  • as colunas desta tabela terão o nome dos campos da classe: [id, firstName, lastName], tendo em conta que as maiúsculas e minúsculas não são distinguidas no nome de uma coluna da tabela;

Note-se que, em nenhum momento, a implementação JPA utilizada é referida pelo nome.

11.2.3. A camada [Spring Data]

A classe [CustomerRepository] implementa a camada de acesso à tabela [Customer]. O seu código é o seguinte:

  

package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}

Trata-se, portanto, de uma interface e não de uma classe (linha 7). Ela estende a interface [CrudRepository], uma interface do Spring Data (linha 5). Esta interface é definida por dois tipos: o primeiro é o tipo dos elementos geridos, neste caso o tipo [Customer]; o segundo é o tipo da chave primária dos elementos geridos, neste caso um tipo [Long]. A interface [CrudRepository] é a seguinte:


package org.springframework.data.repository;

import java.io.Serializable;

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

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

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

    T findOne(ID id);

    boolean exists(ID id);

    Iterable<T> findAll();

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

    long count();

    void delete(ID id);

    void delete(T entity);

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

    void deleteAll();
}

Esta interface define as operações CRUD (Criar – Ler – Atualizar – Eliminar) que podem ser realizadas num tipo JPA T:

  • linha 8: o método save permite persistir uma entidade T na base de dados. Este método persiste a entidade com a chave primária que lhe foi atribuída pelo SGBD. Permite também atualizar uma entidade T identificada pela sua chave primária id. A escolha de uma ou outra ação depende do valor da chave primária id: se este for nulo, é realizada a operação de persistência; caso contrário, é realizada a operação de atualização;
  • linha 10: o mesmo, mas para uma lista de entidades;
  • linha 12: o método findOne permite recuperar uma entidade T identificada pela sua chave primária id;
  • linha 22: o método delete permite eliminar uma entidade T identificada pela sua chave primária id;
  • linhas 24-28: variantes do método [delete];
  • linha 16: o método [findAll] permite recuperar todas as entidades T persistidas;
  • linha 18: o mesmo, mas limitado às entidades cuja lista de identificadores foi passada;

Voltemos à interface [CustomerRepository]:


package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}
  • a linha 9 permite recuperar um [Customer] pelo seu nome [lastName];

E é tudo no que diz respeito à camada [DAO]. Não existe uma classe de implementação da interface anterior. Esta é gerada em tempo de execução pelo [Spring Data]. Os métodos da interface [CrudRepository] são implementados automaticamente. Quanto aos métodos adicionados à interface [CustomerRepository], isso depende. Voltemos à definição de [Customer]:


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

O método da linha 9 é implementado automaticamente por [Spring Data] porque faz referência ao campo [lastName] (linha 3) de [Customer]. Quando encontra um método [findBySomething] na interface a implementar, o Spring Data implementa-o através da seguinte consulta JPQL (Java Persistence Query Language):

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

É, portanto, necessário que o tipo T tenha um campo denominado [something]. Assim, o método

List<Customer> findByLastName(String lastName);

será implementado por um código semelhante ao seguinte:

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

onde [em] designa o contexto de persistência JPA. Isto só é possível se a classe [Customer] tiver um campo denominado [lastName], o que é o caso.

Em conclusão, em casos simples, o Spring Data permite-nos implementar a camada [DAO] com uma interface simples.

11.2.4. A camada [console]

  

A classe [Application] é a seguinte:


package hello;

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

@SpringBootApplication
public class Application implements CommandLineRunner {

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

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

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

        // recuperar um cliente específico através de ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();

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

}
  • linha 9: a classe implementa a interface [CommandLineRunner], que é uma interface [Spring Boot] (linha 4). Esta interface tem apenas um método, o da linha 19;
  • linha 8: @SpringBootApplication é uma anotação que agrupa várias anotações [Spring Boot]:
    • @Configuration: indica que a classe é uma classe de configuração;
    • @EnableAutoConfiguration: solicita que [Spring Boot] crie, por si próprio, um determinado número de beans com base em várias propriedades, nomeadamente o conteúdo do Classpath do projeto. Como as bibliotecas do Hibernate estão no Classpath, o bean [entityManagerFactory] será implementado com o Hibernate. Como a biblioteca do SGBD H2 se encontra no Classpath, o bean [dataSource] será implementado com o H2. No bean [dataSource], é necessário definir também o utilizador e a sua palavra-passe. Aqui, o Spring Boot utilizará o administrador predefinido do H2, que não tem palavra-passe. Como a biblioteca [spring-tx] se encontra no Classpath, será utilizado o gestor de transações do Spring;
    • @EnableWebMvc: se a biblioteca [spring-mvc] estiver no Classpath. Neste caso, é efetuada uma configuração automática para a aplicação web;
    • @ComponentScan: que indica ao Spring onde procurar os outros beans, configurações e serviços. Aqui, estes são procurados por predefinição no pacote que contém a classe marcada, ou seja, o pacote [hello]. Assim, as classes [Customer] e [CustomerRepository] serão encontradas. Como a primeira tem a anotação [@Entity], será catalogada como uma entidade a ser gerida pelo Hibernate. Como a segunda estende a interface [CrudRepository], será registada como um bean do Spring;
  • linhas 11-12: o bean [CustomerRepository] é injetado no código da classe principal;
  • linha 15: o método estático [run] da classe [SpringApplication] do projeto Spring Boot é executado. O seu parâmetro é a classe que possui uma anotação [Configuration] ou [EnableAutoConfiguration]. Tudo o que foi explicado anteriormente irá então ocorrer. O resultado é um contexto de aplicação Spring, ou seja, um conjunto de beans geridos pelo Spring;

As operações que se seguem limitam-se a utilizar os métodos do bean que implementa a interface [CustomerRepository]. Os resultados na consola são os seguintes:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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
  • linhas 1-8: o logótipo do projeto Spring Boot;
  • linha 9: a classe [hello.Application] é executada;
  • linha 10: [AnnotationConfigApplicationContext] é uma classe que implementa a interface [ApplicationContext] do Spring. Trata-se de um contentor de beans;
  • linha 11: o bean [entityManagerFactory] é implementado pela classe [LocalContainerEntityManagerFactory], uma classe do Spring;
  • linha 12: surge o [Hibernate]. Foi esta implementação, JPA, que foi escolhida;
  • linha 19: um dialeto do Hibernate é a variante SQL a utilizar com o SGBD. Aqui, o dialeto [H2Dialect] indica que o Hibernate irá trabalhar com o SGBD e o H2;
  • linhas 21-22: a base de dados é criada. A tabela [CUSTOMER] é criada. Isto significa que o Hibernate foi configurado para gerar as tabelas a partir das definições JPA; neste caso, a definição JPA da classe [Customer];
  • linhas 26-30: resultado do método [findAll] da interface;
  • linha 34: resultado do método [findOne] da interface;
  • linhas 38-39: resultados do método [findByLastName];
  • linhas 41 e seguintes: registos do encerramento do contexto Spring.

11.2.5. Configuração manual do projeto Spring Data

Duplicamos o projeto anterior no projeto [gs-accessing-data-jpa-02]:

  

Neste novo projeto, não vamos basear-nos na configuração automática feita pelo Spring Boot. Vamos fazê-la manualmente. Isto pode ser útil se as configurações predefinidas não nos servirem.

Em primeiro lugar, vamos especificar as dependências necessárias no ficheiro [pom.xml]:


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

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

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

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

    <properties>
        <!-- utilize UTF-8 para tudo -->
        <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>
  • linhas 10-14: o projeto Maven pai, cujas bibliotecas iremos utilizar;
  • linhas 18-21: o Spring Data utilizado para aceder à base de dados;
  • linhas 23-26: a implementação Hibernate da especificação JPA;
  • linhas 28-31: o SGBD H2;
  • linhas 33-36: as bases de dados são frequentemente utilizadas com conjuntos de ligações abertas, o que evita a abertura e o encerramento repetidos das ligações. Neste caso, a implementação utilizada é a de [tomcat-jdbc];

No novo projeto, a entidade [Customer] e a interface [CustomerRepository] não sofrem alterações. Vamos alterar a classe [Application], que será dividida em duas classes:

  • [Config], que será a classe de configuração:
  • [Main], que será a classe executável;
  

A classe executável [Application] é agora a seguinte:


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) {
        // instanciação do contexto Spring
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);

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

        ...

        // encerramento do contexto
        context.close();
    }
}
  • linha 9: a classe [Application] já não tem anotações de configuração;
  • linhas 3-7: note-se que já não existem importações de pacotes [Spring Boot];
  • linha 12: instanciamos os beans Spring. Obtemos o contexto do Spring que contém a referência aos beans assim criados;
  • linha 13: solicita-se uma referência ao bean do tipo [CustomerRepository];

A classe [Config] que configura o projeto é a seguinte:


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 {

    // a base de dados H2
    @Bean
    public DataSource dataSource() {
        // fonte de dados TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuração de acesso JDBC
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        // uma ligação inicialmente aberta
        dataSource.setInitialSize(1);
        // resultado
        return dataSource;
    }

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

    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan("entities");
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    // Gestor de transações
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

}
  • linha 17: a anotação [@EnableTransactionManagement] indica que os métodos das interfaces [CrudRepository] devem ser executados no interior de uma transação. Foi colocada em comentário, pois é o comportamento por predefinição;
  • linha 18: a anotação [@EnableJpaRepositories] permite indicar as pastas onde se encontram as interfaces Spring Data [CrudRepository]. Estas interfaces tornar-se-ão componentes Spring e estarão disponíveis no seu contexto;
  • linha 19: a anotação [@Configuration] transforma a classe [Config] numa classe de configuração do Spring;
  • linha 20: a anotação [@ComponentScan] permite listar as pastas onde os componentes Spring devem ser procurados. Os componentes Spring são classes marcadas com anotações Spring, tais como @Service, @Component, @Controller, ... Aqui, não existem outros além dos que estão definidos na classe [AppConfig], pelo que a anotação foi colocada em comentário;
  • linhas 24-37: definem a fonte de dados, a base de dados H2. É a anotação @Bean na linha 25 que torna o objeto criado por este método um componente gerido pelo Spring. O nome do método pode ser qualquer um. No entanto, deve ser chamado [dataSource] se o EntityManagerFactory da linha 51 estiver ausente e for definido por autoconfiguração;
  • linha 30: a base de dados chamar-se-á [demo] e será gerada na pasta do projeto;
  • linhas 40-47: definem a implementação JPA utilizada, neste caso uma implementação do Hibernate. O nome do método pode ser qualquer um;
  • linha 43: sem registos SQL;
  • linha 44: a base de dados será criada caso não exista;
  • linhas 50-58: definem o EntityManagerFactory que irá gerir a persistência do JPA. O método deve chamar-se obrigatoriamente [entityManagerFactory];
  • linha 51: o método recebe dois parâmetros com o tipo dos dois beans definidos anteriormente. Estes serão então criados e, em seguida, injetados pelo Spring como parâmetros do método;
  • linha 53: define a implementação JPA utilizada;
  • linha 54: define as pastas onde se encontram as entidades JPA;
  • linha 55: define a fonte de dados a gerir;
  • linhas 61-66: o gestor de transações. O método deve chamar-se obrigatoriamente [transactionManager]. Recebe como parâmetro o bean das linhas 51-58;
  • linha 64: o gestor de transações é associado ao EntityManagerFactory;

Os métodos anteriores podem ser definidos em qualquer ordem.

A execução do projeto produz os mesmos resultados. Surge um novo ficheiro na pasta do projeto, o da base de dados H2:

  

11.2.6. Criação de um arquivo executável

Para criar um arquivo executável do projeto, pode-se proceder da seguinte forma:

  • em [1]: cria-se uma configuração de execução;
  • em [2]: do tipo [Java Application]
  • em [3]: indica o projeto a executar (utilize o botão Browse);
  • em [4]: indica a classe a executar;
  • em [5]: o nome da configuração de execução – pode ser qualquer um;
  • em [6]: exporta-se o projeto;
  • em [7]: sob a forma de um arquivo executável JAR;
  • em [8]: indica o caminho e o nome do ficheiro executável a criar;
  • em [9]: o nome da configuração de execução criada em [5];
  • em [10], o arquivo criado;

Feito isto, abre-se um terminal na pasta que contém o arquivo executável:

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

O ficheiro executável é executado da seguinte forma:


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

Os resultados obtidos no terminal são os seguintes:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder para mais detalhes.
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. Exemplo 2

11.3.1. Introdução

Vamos retomar o exemplo da tabela de produtos que utilizámos para apresentar o API JDBC e criar a seguinte arquitetura:

A base de dados [dbintrospringjpa] tem duas tabelas: [PRODUITS] e [CATEGORIES]. A tabela [CATEGORIES] é a seguinte:

 
  • [ID]: chave primária no modo AUTO_INCREMENT;
  • [VERSION]: número de versão do registo;
  • [NOM]: nome da categoria — único;

A tabela [PRODUITS] é a seguinte:

 
  • [ID]: chave primária no modo AUTO_INCREMENT;
  • [VERSION]: n.º de versão do registo;
  • [NOM]: nome de um produto — único;
  • [ID_CATEGORIE]: n.º da sua categoria — chave estrangeira no campo [CATEGORIES.ID];
  • [PRIX]: o seu preço;
  • [DESCRIPTION]: uma descrição do produto;

Tarefa a realizar: crie a base de dados [dbintrospringdata] com o script SQL [dbintrospringdata.sql] do suporte:


11.3.2. Criação do projeto Maven

Para criar um esqueleto de projeto Spring Data, pode-se proceder da seguinte forma:

  • em [1], cria-se um novo projeto;
  • em [2]: do tipo [Spring Starter Project];
  • o projeto gerado será um projeto Maven. Em [3], indica-se o nome do grupo do projeto;
  • no [4]: indica-se o nome do artefacto (um jar, neste caso) que será criado durante a compilação do projeto;
  • em [5]: o nome do projeto no Eclipse – pode ser qualquer um (não tem de ser idêntico a [4]);
  • em [7]: indica-se que se vai criar um projeto com uma camada [JPA] com o SGBD MySQL. As dependências necessárias para esse projeto serão então incluídas no ficheiro [pom.xml];
  • no [8], indique o nome da pasta do projeto;
  • no ficheiro [9], concluir o assistente;
  • em [10]: o projeto criado;

O ficheiro [pom.xml] inclui as dependências necessárias para um projeto 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/> <!-- pesquisar o pai no repositório -->
    </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>
  • linhas 14-19: o projeto pai do Maven — define um grande número de bibliotecas com as respetivas versões — estas bibliotecas são utilizadas como dependências do Maven sem especificar a sua versão;
  • linhas 28-31: a dependência necessária para o JPA – irá incluir o [Spring Data];
  • linhas 32-36: a dependência do controlador JDBC do MySQL;
  • linhas 37-41: as dependências necessárias para os testes JUnit integrados com o Spring;

A classe executável [Application] não faz nada, mas está pré-configurada:


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);
    }
}
  • a anotação [@SpringBootApplication] torna a classe uma classe de autoconfiguração do projeto;

A classe de testes [ApplicationTests] não faz nada, mas está pré-configurada:


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

}
  • linha 9: a anotação [@SpringApplicationConfiguration] permite utilizar o ficheiro de configuração [Application]. A classe de teste beneficiará assim de todos os beans que forem definidos por este ficheiro;
  • linha 8: a anotação [@RunWith] permite a integração do Spring com o JUnit: a classe poderá ser executada como um teste JUnit. [@RunWith] é uma anotação JUnit (linha 4), enquanto a classe [SpringJUnit4ClassRunner] é uma classe Spring (linha 6);

Agora que temos um esqueleto de aplicação JPA, podemos completá-lo para escrever o projeto da camada de persistência associada à base de dados dos produtos.

11.3.3. O projeto Eclipse

Vamos desenvolver o projeto anterior da seguinte forma:

  
  • [AppConfig.java]: a classe de configuração do projeto Spring;
  • [Main.java]: a classe executável do projeto;
  • [IDao.java]: a interface da camada [DAO];
  • [Dao.java]: a classe de implementação da camada [DAO];
  • [AbstractEntity.java]: a classe pai das classes [Produit] e [Categorie];
  • [Produit.java]: classe associada a uma linha da tabela [PRODUITS] da base de dados;
  • [Categorie.java]: classe associada a uma linha da tabela [CATEGORIES] da base de dados;
  • [ProduitsRepository]: a interface Spring Data de acesso à tabela [PRODUITS];
  • [CategoriesRepository]: a interface Spring Data de acesso à tabela [CATEGORIES];
  • [pom.xml]: o ficheiro de configuração do projeto Maven;

Este projeto implementa a seguinte arquitetura:

A camada [DAO] apenas vê a camada implementada por [Spring Data].

11.3.4. Configuração do Maven

O ficheiro [pom.xml] do projeto Maven é o seguinte:


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

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

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

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

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

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>

Esta configuração é a utilizada e explicada no parágrafo 11.2.5. Acrescentamos as seguintes bibliotecas:

  • linhas 42-49: uma biblioteca jSON utilizada pelo método [toString] da classe [Produit];
  • linhas 51-55: a biblioteca [Google Guava], que fornece métodos utilitários para gerir coleções de elementos. Será utilizada pela classe [Dao], que implementa a camada [DAO];
  • linhas 56-67: as bibliotecas necessárias para os testes JUnit;
  • linhas 69-72: uma biblioteca de registos;
  • linhas 81-86: os plugins Maven necessários para o projeto;

11.3.5. As entidades da camada [JPA]

Camada

[DAO]

Camada

[console]

Camada

[JPA]

Piloto

[JDBC]

Calça

[Spring Data]

Spring 4

SGBD

  

11.3.5.1. A classe [AbstractEntity]

A classe [AbstractEntity] é a seguinte:


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

    // construtores
    public AbstractEntity() {

    }

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

    // redefinição de [equals] e [hashcode]
    @Override
    public int hashCode() {
        return (id != null ? id.hashCode() : 0);
    }

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

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

Esta classe tem como objetivo fornecer uma classe-pai às entidades JPA, encapsulando num único local as propriedades [id, version] (linhas 19, 22) comuns às duas entidades [Produit] e [Categorie] ligadas à base de dados. Estas propriedades estão associadas às colunas [ID, VERSION] das tabelas (linhas 18, 21).

  • linha 13: a anotação [@MappedSuperclass] indica que a classe é uma classe pai das entidades JPA;
  • linha 16: a anotação [@Id] indica que o campo [id] (pode ter outro nome) está associado à chave primária de uma tabela;
  • linha 17: a anotação [@GeneratedValue(strategy=GenerationType.IDENTITY)] define o modo de geração das chaves primárias. O modo [GenerationType.IDENTITY] irá utilizar, juntamente com MySQL, o modo [AUTO_INCREMENT]. Com outro SGBD, este modo utilizaria outro método. A vantagem é que o programador não precisa de se preocupar com isso e que o seu código permanece válido independentemente do SGBD utilizado;
  • linha 18: a anotação [@Column] indica a coluna associada ao campo. Quando esta anotação não está presente, o JPA assume que a coluna tem o mesmo nome que o campo. É o que acontece aqui. Por isso, não seria necessário incluir esta anotação;
  • linha 20: a anotação [@Version] indica que o campo [version] está associado a uma coluna de controlo de versões. A implementação JPA irá incrementar este número de versão sempre que a entidade for alterada. Este número serve para impedir a atualização simultânea da entidade por dois utilizadores diferentes: dois utilizadores, U1 e U2, leem a entidade E com um número de versão igual a V1. U1 altera E e grava essa alteração na base de dados: o número de versão passa então para V1+1. U2, por sua vez, altera E e grava essa alteração na base de dados: receberá uma exceção, pois possui uma versão (V1) diferente da que consta na base de dados (V1+1);
  • linhas 35-52: redefinição dos métodos [hashCode] e [equals]. Por predefinição, [obj1.equals(obj2)] tem o valor «true» se [obj1==obj2] for «true», ou seja, se ob1 e obj2 forem dois ponteiros iguais. Se se pretender comparar os objetos apontados em vez dos próprios ponteiros, é necessário redefinir o método [equals] e o método [hashCode]. Este último deve devolver o mesmo valor para dois objetos que o método [equals] considere iguais;
  • linhas 42-51: dois objetos do tipo [AbstractEntity] ou derivados serão considerados iguais se as suas chaves primárias [id] forem iguais;
  • linhas 35-38: o método [hashCode] devolve efetivamente o mesmo valor para dois objetos [AbstractEntity] idênticos e, portanto, com a mesma chave primária [id];
  • linhas 55-63: o método [toString] devolve a cadeia jSon do objeto [this]. Se este objeto designar uma classe filha, este método devolverá então a cadeia jSON da classe filha. Isto dispensa-nos de criar um método [toString] nas classes filhas;

11.3.5.2. A entidade JPA [Produit]

A classe [Produit] é uma entidade JPA associada a uma linha da tabela [PRODUITS]:

 

package spring.data.entities;

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

import com.fasterxml.jackson.annotation.JsonFilter;

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

    // propriedades
    @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;

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

    // construtores
    public Produit() {

    }

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

    // getters e setters
...
}
  • linha 12: a anotação [@Entity] torna a classe [Produit] uma entidade gerida pela camada [JPA];
  • linha 13: a anotação [@Table(name = "PRODUITS")] indica que a classe [Produit] corresponde à imagem de uma linha da tabela [PRODUITS] da base de dados;
  • linha 14: o nome do filtro jSON a aplicar à entidade. Veremos que a propriedade [categorie] da linha 13 nem sempre está disponível. Nesse caso, é necessário excluí-la da representação jSON do objeto. Para tal, precisamos de um filtro. Assim, é num filtro denominado [jsonFilterCategorie] que indicaremos se pretendemos ou não a propriedade [categorie];
  • linha 18: a anotação [@Column] associa o campo [nom] à coluna [NOM] da tabela [PRODUITS]. Quando o campo tem o mesmo nome que a coluna associada, a anotação [@Column] pode ser omitida. Seria esse o caso aqui;
  • linhas 31-33: a categoria do produto;
  • linha 31: a anotação [@ManyToOne] indica que a coluna da anotação da linha 32, [@JoinColumn(name = "CATEGORIE_ID")], é uma chave estrangeira da tabela [PRODUITS] daentidade [Produit] na tabela [CATEGORIES] associada à entidade da linha 33. Esta anotação deve referir-se a uma entidade JPA. Assim, a classe da linha 33 deve ser uma entidade JPA;
  • linha 31: a anotação [fetch = FetchType.LAZY] determina que, quando se recupera um produto da tabela [PRODUITS], a sua categoria (linha 33) não seja recuperada imediatamente (carregamento diferido). Esta é então obtida aquando da primeira chamada ao método [getCategorie]. Este atributo não é obrigatório. A implementação JPA utilizada pode ignorá-lo. É precisamente porque a propriedade [categorie] pode estar presente ou não que introduzimos o filtro jSON da linha 14. As implementações JPA existentes (Hibernate, Eclipselink, OpenJPA) não tratam esta anotação da mesma forma. O Hibernate complementa o método [getCategorie] inicial (que se limita a retornar o campo categorie) com uma chamada ao SGBD para obter a categoria. Para que isso seja possível, é necessário que a ligação ao SGBD, utilizada inicialmente para obter o produto, ainda esteja aberta; caso contrário, ocorre uma exceção.

11.3.5.3. A entidade JPA [Categorie]

A classe [Categorie] é uma entidade JPA associada a uma linha da tabela [CATEGORIES]:

 

O seu código é o seguinte:


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 {

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

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

    // construtores
    public Categorie() {

    }

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

    // métodos
    public void addProduit(Produit produit) {
        // adiciona-se o produto
        produits.add(produit);
        // define-se a sua categoria
        produit.setCategorie(this);
    }

    // getters e setters
...
}
  • linhas 21-22: o nome da categoria;
  • linhas 25-26: os produtos desta categoria;
  • linha 25: a anotação [@OneToMany] é a relação inversa da relação [@ManyToOne] que encontrámos na entidade [Produit]. O atributo [mappedBy = "categorie"] indica o campo da entidade [Produit] anotado pela relação inversa [@ManyToOne]. O atributo [cascade = { CascadeType.ALL }] determina que as operações (persist, merge, remove) realizadas numa @Entity [Categorie] sejam propagadas em cascata para as [produits] da linha 26. É possível indicar cascatas parciais com as constantes [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE];
  • linha 25: o atributo [fetch = FetchType.LAZY] determina que, quando se recupera uma categoria da tabela [CATEGORIES], os seus produtos não sejam recuperados imediatamente. Serão recuperados na primeira chamada ao método [getProduits]. As implementações existentes de JPA (Hibernate, Eclipselink, OpenJPA) não tratam esta anotação da mesma forma. O Hibernate complementa o método [getProduits] inicial (que se limita a devolver o campo produits) com uma chamada ao SGBD para ir buscar os produtos da categoria. Para que isso seja possível, é necessário que a ligação ao SGBD, utilizada inicialmente para obter a categoria, ainda esteja aberta. Este atributo é obrigatório. A implementação JPA não pode ignorá-lo. Como a propriedade [produits] pode ou não ser inicializada, introduzimos o filtro jSON na linha 17, que nos permitirá indicar se queremos ou não essa propriedade;
  • linha 26: o tipo [Set] é uma interface. O tipo [HashSet] é uma classe que implementa essa interface. Ela implementa uma coleção de elementos denominada ensemble. Um conjunto não pode conter dois objetos idênticos. Aqui, os objetos são do tipo [Produit]. Assim, no conjunto, não poderá haver dois objetos idênticos. Como o método [equals] da classe pai [AbstractEntity] foi redefinido para determinar que dois produtos são idênticos se tiverem a mesma chave primária, então o campo [produits] não poderá conter dois produtos com a mesma chave primária;
  • linhas 38-43: o método [addProduit] permite adicionar um produto à categoria;

11.3.6. A camada [Spring Data]

Camada

[DAO]

Camada

[console]

Camada

[JPA]

Casaco

[JDBC]

Calças

[Spring Data]

Primavera 4

SGBD

  

A interface [CategoriesRepository] gere o acesso à tabela [CATEGORIES]:


package spring.data.repositories;

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

import spring.data.entities.Categorie;

public interface CategoriesRepository extends CrudRepository<Categorie, Long> {

    // categoria com os seus produtos
    @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);

    // uma categoria sem os seus produtos, identificada pelo seu nome
    public Categorie findByNom(String nom);
}
  • linha 8: a interface [CrudRepository] foi utilizada e explicada no parágrafo 11.2.3. Recorde-se que:
    • o primeiro tipo da interface é a entidade JPA, gerida para os acessos CRUD (findOne, findAll, guardar, eliminar, deleteAll),
    • o segundo tipo é o da chave primária da entidade JPA, neste caso um inteiro [Long];
  • linha 12: o método da linha 12 é implementado pela consulta JPQL (Java Persistence Query Language) da linha 11. Esta consulta recupera as entidades JPA. Numa consulta deste tipo:
    • as tabelas são substituídas pelas respetivas entidades JPA associadas;
    • as colunas são substituídas por campos das entidades JPA utilizadas na consulta;
  • linha 11: a consulta JPQL devolve uma categoria com os seus produtos. Recorde-se que, na entidade [Categorie], o campo [produits] tinha o atributo [fetch = FetchType.LAZY] (carregamento diferido). Na consulta JPQL, forçamos o carregamento dos produtos com a palavra-chave [fetch]. O parâmetro ?1 da consulta será substituído, na execução, pelo valor do primeiro parâmetro do método da linha 12, ou seja, pelo parâmetro [Long id];
  • linhas 14-15: um método semelhante para uma categoria identificada pelo seu nome;
  • linha 18: o método [findByNom] será automaticamente implementado por [Spring Data], uma vez que o tipo [Category] possui um campo [nom];

A interface [ProduitsRepository] gere os acessos à tabela [PRODUITS]:


package spring.data.repositories;

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

import spring.data.entities.Produit;

public interface ProduitsRepository extends CrudRepository<Produit, Long> {

    // um produto com a sua categoria
    @Query("select p from Produit p left join fetch p.categorie c where p.id=?1")
    public Produit getProduitByIdWithCategorie(Long id);

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

    // um produto sem a sua categoria, identificado pelo seu nome
    public Produit findByNom(String nom);
}

As explicações são as mesmas que para a interface [CategoriesRepository].

Estas interfaces serão implementadas por classes geradas pela [Spring Data] no momento da execução do projeto. A estas classes chama-se [proxy]. Por predefinição, os métodos da classe de implementação são executados numa transação. O facto de estas interfaces estenderem a classe [CrudRepository] torna-as componentes Spring.

11.3.7. A camada [DAO]

Camada

[DAO]

Camada

[console]

Camada

[JPA]

Casaco

[JDBC]

Calças

[Spring Data]

Primavera 4

SGBD

  

A interface [IDao] da camada [DAO] é a seguinte:


package spring.data.dao;

import java.util.List;

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

public interface IDao {

    // inserção de uma lista de produtos
    public List<Produit> addProduits(List<Produit> produits);

    // eliminação de todos os produtos
    public void deleteAllProduits();

    // atualização de uma lista de produtos
    public List<Produit> updateProduits(List<Produit> produits);

    // obter todos os produtos
    public List<Produit> getAllProduits();

    // inserção de uma lista de categorias
    public List<Categorie> addCategories(List<Categorie> categories);

    // eliminação de todas as categorias
    public void deleteAllCategories();

    // atualização de uma lista de categorias
    public List<Categorie> updateCategories(List<Categorie> categories);

    // obter todas as categorias
    public List<Categorie> getAllCategories();

    // um produto específico, com ou sem a respetiva categoria
    public Produit getProduitByIdWithoutCategorie(Long idProduit);

    public Produit getProduitByIdWithCategorie(Long idProduit);

    public Produit getProduitByNameWithCategorie(String nom);

    public Produit getProduitByNameWithoutCategorie(String nom);

    // uma categoria específica com ou sem os respetivos produtos
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie);

    public Categorie getCategorieByIdWithProduits(Long idCategorie);

    public Categorie getCategorieByNameWithProduits(String nom);

    public Categorie getCategorieByNameWithoutProduits(String nom);
}

Adotou-se aqui a regra de que qualquer método que altere os objetos que tem como parâmetros de entrada deve, em seguida, devolvê-los no seu resultado. A razão para esta regra foi explicada no parágrafo 4.2: permite que uma camada e o seu cliente se encontrem em duas JVM separadas e, assim, funcionem em modo cliente/servidor.

A implementação [Dao] desta interface é a seguinte:


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

}
  • linha 16: a anotação [@Component] torna a classe [Dao] um componente Spring;
  • linhas 19-23: injeção das referências nas duas interfaces [CrudRepository] da [Spring Data]. Esta injeção ocorrerá durante a instanciação dos objetos Spring, geralmente no início da execução do projeto Spring;
  • note-se, nas linhas 28 e 46, que o método [save] da interface [produitsRepository] é utilizado tanto para a inserção como para a atualização de produtos. O método [Spring Data] utiliza a chave primária do produto para determinar se deve efetuar uma inserção ou uma atualização. Se a chave primária for [null], será uma inserção; caso contrário, será uma atualização;
  • linha 82: utiliza-se o método [Lists.newArrayList] da biblioteca Guava para obter uma lista de produtos. O método [produitsRepository.findAll()] devolve um tipo [Iterable<Produit>];
  • linha 28: o método [produitsRepository.save(produits)] devolve um tipo [Iterable<Produit>]. O mesmo se aplica às outras operações [save] da classe;

Na classe [Dao] acima referida, as exceções que podem ocorrer estão encapsuladas no seguinte tipo [DaoException]:


package spring.data.dao;

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

// classe de exceção para a aplicação «Eleições»
// a exceção não é controlada

public class DaoException extends RuntimeException implements Serializable {

    // série ID
    private static final long serialVersionUID = 1L;

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

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

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

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

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

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

    // lista de mensagens de erro de uma exceção
    private List<String> getErreursForException(Throwable th) {
        // recupera-se a lista de mensagens de erro da exceção
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // recupera-se a mensagem apenas se esta for !=null e não estiver vazia
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // causa seguinte
            cause = cause.getCause();
        }
        return erreurs;
    }

    // getters e setters
...
}
  • linha 10: a classe deriva da classe [RuntimeException] e é, portanto, uma exceção não controlada;
  • linha 16: um código de erro;
  • linha 17: uma lista de mensagens de erro, as associadas à pilha de exceções que provocaram a [DaoException];
  • linhas 59-76: o método privado [getMessagesForException] permite obter a lista de mensagens de erro associadas às exceções da pilha de exceções. É, de facto, possível empilhar exceções com os seguintes construtores da classe Exception:
    • Exception(String message, Throwable cause): cria uma exceção com uma mensagem e a exceção que se pretende encapsular;
    • Exception(Throwable cause): cria uma exceção com a exceção que se pretende encapsular;

O tipo [Throwable] é a classe pai da classe [Exception]. Se os construtores anteriores forem executados repetidamente, a exceção final conterá, então, várias exceções. Diz-se que se tem uma pilha de exceções.

  • A última causa de uma exceção e1 é obtida através da expressão [e1.getCause()];
  • a penúltima causa de uma exceção e1 é obtida através da expressão [e1.getCause().getCause()];
  • continua-se assim até se obter [getCause()==null];

11.3.8. Configuração do projeto Spring

  

A classe [DaoConfig] configura a camada [DAO]:


package spring.data.config;

import javax.persistence.EntityManagerFactory;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

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

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

    // a fonte de dados [tomcat-jdbc]
    @Bean
    public DataSource dataSource() {
        // fonte de dados TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuração de acesso JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // uma ligação inicialmente aberta
        dataSource.setInitialSize(1);
        // resultado
        return dataSource;
    }

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

    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(packagesToScan());
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    // Gestor de transações
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

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

}

Uma configuração semelhante foi abordada e explicada no parágrafo 11.2.5. Adicionámos as seguintes anotações Spring:

  • linha 17: a anotação [@EnableJpaRepositories] serve para indicar os pacotes onde se encontram as interfaces [CrudRepository] e [Spring Data];
  • linha 18: a classe é uma classe de configuração do Spring. Esta informação é importante. Se a removemos, o projeto continua a funcionar. No entanto, mais adiante no documento, quando criarmos projetos baseados neste, alguns deles deixarão de funcionar se a anotação da linha 18 for removida;
  • linha 19: a anotação [@ComponentScan] indica os pacotes onde se encontram os objetos Spring. Trata-se das classes anotadas com [@Component, @Service, @Controller, ...]. Aqui, o componente Spring [Dao] será encontrado e instanciado;
  • linhas 73-76: definimos um bean que representa a matriz de pacotes a analisar para encontrar entidades JPA. Isto permitirá que um projeto que importe a classe [DaoConfig] redefina este bean e, assim, altere os pacotes a analisar na linha 59. Mais adiante no documento, iremos abordar esta questão;

A classe [AppConfig] configura todo o projeto:


package spring.data.config;

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

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

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

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

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

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

    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // mapeador jSON
        ObjectMapper mapper = new ObjectMapper();
        // filtros
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // resultado
        return mapper;
    }
}
  • linha 11: a classe é uma classe de configuração do Spring;
  • linha 12: que importa os beans definidos pela classe [DaoConfig] que acabámos de ver;
  • a camada [console] utiliza mapeadores jSON que são definidos aqui;
  • linhas 14-64: definem cinco mapeadores jSON;
  • linhas 15-18: o filtro jSON [jsonMapper] não tem filtros;
  • linhas 20-30: o filtro jSON [jsonMapperCategorieWithProduits] permite serializar/deserializar um objeto [Categorie] com os seus produtos;
  • linhas 32-42: o filtro jSON [jsonMapperProduitWithCategorie] permite serializar/deserializar um objeto [Produit] com a sua categoria;
  • linhas 43-53: o filtro jSON [jsonMapperCategorieWithoutProduits] permite serializar/deserializar um objeto [Categorie] sem os seus produtos;
  • linhas 55-64: o filtro jSON [jsonMapperProduitWithoutCategorie] permite serializar/deserializar um objeto [Produit] sem a sua categoria;

Note-se que, ao criar um filtro jSON para uma entidade T, é necessário configurar não só o filtro da entidade T, mas também os das entidades Ti que esta possa conter.

11.3.9. A camada [console]

Camada

[DAO]

Camada

[console]

Camada

[JPA]

Casaco

[JDBC]

Calças

[Spring Data]

Spring 4

SGBD

  

A classe [Main] é a seguinte:


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 {
            // instanciação do contexto Spring
            context = new AnnotationConfigApplicationContext(AppConfig.class);
            ObjectMapper jsonMapperCategorieWithProduits = context.getBean("jsonMapperCategorieWithProduits",
                    ObjectMapper.class);
            ObjectMapper jsonMapperProduitWithCategorie = context.getBean("jsonMapperProduitWithCategorie",
                    ObjectMapper.class);
            ObjectMapper jsonMapperCategorieWithoutProduits = context.getBean("jsonMapperCategorieWithoutProduits",
                    ObjectMapper.class);
            ObjectMapper jsonMapperProduitWithoutCategorie = context.getBean("jsonMapperProduitWithoutCategorie",
                    ObjectMapper.class);
            IDao dao = context.getBean(IDao.class);
            // --------------------------------------------------------------------------------------
            // esvazia-se a base de dados
            log("Vidage de la base de données", 1);
            // esvaziar a tabela [CATEGORIES] - em cadeia, a tabela [PRODUITS] será esvaziada
            dao.deleteAllCategories();
            // --------------------------------------------------------------------------------------
            log("Remplissage de la base", 1);
            // preenchimento das tabelas
            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);
            }
            // adição da categoria - por efeito em cadeia, os produtos também serão inseridos
            dao.addCategories(categories);
            // --------------------------------------------------------------------------------------
            log("Affichage de la base", 1);
            // lista de categorias
            log("Liste des catégories", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            // lista de produtos
            log("Liste des produits", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
            // categoria 1 com os seus produtos
            Categorie categorie = dao.getCategorieByNameWithProduits("categorie1");
            log("Catégorie 1 avec ses produits", 2);
            affiche(categorie, jsonMapperCategorieWithProduits);
            // o produto [produit14] com a sua categoria
            Produit p = dao.getProduitByNameWithCategorie("produit14");
            log("Produit [produit14] avec sa catégorie", 2);
            affiche(p, jsonMapperProduitWithCategorie);
            // --------------------------------------------------------------------------------------
            log("Mise à jour du prix des produits de [categorie1]", 1);
            log("Produits de la catégorie [categorie1] avant la mise à jour", 2);
            Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
            Set<Produit> produits = categorie1.getProduits();
            affiche(categorie1, jsonMapperCategorieWithProduits);
            for (Produit produit : produits) {
                produit.setPrix(1.1 * produit.getPrix());
            }
            dao.updateProduits(Lists.newArrayList(produits));
            log("Produits de la catégorie [categorie1] après la mise à jour", 2);
            affiche(dao.getCategorieByNameWithProduits("categorie1"), jsonMapperCategorieWithProduits);
            // --------------------------------------------------------------------------------------
            log("Vidage de la base de données", 1);
            // esvazia-se a tabela [CATEGORIES] - em cadeia, a tabela [PRODUITS] será esvaziada
            dao.deleteAllCategories();
            // visualização da base de dados
            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);
            // é feita a inserção
            categorie = new Categorie("cat1");
            categorie.addProduit(new Produit("x", 1.0, ""));
            categorie.addProduit(new Produit("x", 1.0, ""));
            // adição da categoria — por efeito em cascata, os produtos também serão inseridos
            try {
                dao.addCategories(Lists.newArrayList(categorie));
            } catch (DaoException e) {
                System.out.println(e);
            }
            // verificação
            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) {
                // concluído
                context.close();
            }
        }
        System.out.println("Travail terminé");
    }

    // exibição de um elemento do tipo T
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }

    // exibição de uma lista de elementos do tipo T
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }

    private static void log(String message, int mode) {
        // exibe mensagem
        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);
    }
}
  • linha 25: instanciação dos beans Spring a partir da classe de configuração [AppConfig];
  • linhas 26-33: recuperação das referências aos mapeadores jSON. Utiliza-se a seguinte assinatura do método [ApplicationContext].getBean:
    • [ApplicationContext].getBean(String id, Class classe): que se utiliza quando existem vários beans do tipo [classe]. Neste caso, especifica-se o identificador do bean solicitado. Se este tiver sido definido com a anotação [@Bean], o seu identificador é o nome do método anotado. Se tiver sido definido com a anotação [@Bean(« identifiant »], o seu identificador é o indicado na anotação;
  • linha 34: recuperação de uma referência na camada [DAO];
  • linhas 37-39: esvaziamento da base de dados. Esvazia-se a tabela das categorias (linha 39). Porque escrevemos:

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

quando uma categoria é eliminada, todos os produtos a ela associados são também eliminados;

  • linhas 43-53: preenchimento da tabela com 2 categorias de 5 produtos cada. Na linha 50, a inserção das duas categorias provocará, ao mesmo tempo, a inserção dos respetivos produtos, sempre porque escrevemos [cascade = { CascadeType.ALL }];
  • linha 58: exibimos as categorias. Utilizamos o mapeador jSON [jsonMapperCategorieWithoutProduits] para exibir as categorias sem os respetivos produtos. Com efeito, o método [dao.getAllCategories()] apresenta as categorias sem os respetivos produtos (carregamento diferido);
  • linha 61: exibem-se os produtos sem a respetiva categoria. Com efeito, o método [dao.getAllProduits()] apresenta os produtos sem a respetiva categoria (carregamento diferido);
  • linhas 63-65: exibem a categoria com o nome [categorie1] com os seus produtos (eager loading);
  • linhas 67-69: exibem um produto com a respetiva categoria;
  • linhas 71-81: aumentam em 10% todos os preços dos produtos da categoria [categorie1];
  • linhas 91-101: adiciona-se uma categoria com dois produtos com o mesmo nome. No entanto, na tabela [PRODUITS], existe uma restrição de unicidade na coluna [NOM]. A inserção do segundo produto será, portanto, rejeitada e será lançada uma exceção. No entanto, o método [dao.addProduits] é executado numa transação. O facto de a segunda inserção falhar deve, portanto, anular também a inserção do primeiro produto, bem como a da sua categoria [cat1]. É isso que queremos verificar;
  • linhas 119-121: um método genérico capaz de exibir a cadeia jSON de qualquer elemento do tipo T. A serialização jSON é controlada pelo mapeador passado como parâmetro;
  • linhas 124-128: um método análogo, desta vez para uma lista de elementos do tipo T;

A execução da classe [Main] produz os seguintes resultados (excluindo os registos do 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é
  • linhas 4-17: as categorias e os produtos inseridos na tabela;
  • linhas 18-19: uma categoria com os seus produtos;
  • linhas 20-21: um produto com a sua categoria;
  • linhas 22-26: atualização do preço de alguns produtos. Na linha 24, verifica-se que os preços aumentaram efetivamente 10%;
  • linhas 27-36: adição da categoria [cat1] com dois produtos com o mesmo nome. Vê-se que a tabela está igual antes (linhas 28-29) e depois da adição (linhas 35-36), o que demonstra que todas as inserções da transação foram efetivamente anuladas;
  • linhas 31-34: a exceção que ocorreu durante a inserção do segundo produto e que fez com que toda a transação falhasse;

11.3.10. O teste unitário JUnit

  

A classe [Test01] é a seguinte:


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 {

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

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

    @Before
    public void cleanAndFill() {
        // limpa-se a base de dados antes de cada teste
        log("Vidage de la base de données", 1);
        // esvazia-se a tabela [CATEGORIES] — em cadeia, a tabela [PRODUITS] será esvaziada
        dao.deleteAllCategories();
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // preenchem-se as tabelas
        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);
        }
        // adição da categoria — por efeito em cadeia, os produtos também serão inseridos
        categories = dao.addCategories(categories);
    }

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

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

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

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

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

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

    // exibição de um elemento do tipo T
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }

    // exibição de uma lista de elementos do tipo T
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }

    private static void log(String message, int mode) {
        // exibe mensagem
        String toPrint = null;
        switch (mode) {
        case 1:
            toPrint = String.format("%s --------------------------------", message);
            break;
        case 2:
            toPrint = String.format("-- %s", message);
            break;
        }
        System.out.println(toPrint);
    }

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

}
  • linha 27: o teste unitário é configurado pela classe [AppConfig], já apresentada no parágrafo 11.3.8;
  • linhas 32-33: inserção de uma referência na camada [DAO];
  • linhas 36-50: injeção dos cinco mapeadores jSON;
  • linhas 60-71: após esvaziar a base de dados (linha 57), esta é preenchida com 2 categorias, contendo cada uma 5 produtos. Este método é executado antes de cada teste devido à anotação [@Before] da linha 52;
  • linhas 75-93: apresenta o conteúdo da base de dados;
  • linhas 95-101: solicita uma categoria com os seus produtos, categoria identificada pelo seu nome;
  • linhas 103-109: solicita uma categoria sem os seus produtos, categoria identificada pelo seu nome;
  • linhas 111-120: solicita um produto com a sua categoria, produto identificado pelo seu n.º;
  • linhas 122-130: solicita um produto sem a sua categoria, produto identificado pelo seu n.º;
  • linhas 133-184: métodos privados partilhados pelos diferentes testes;

Tarefa a realizar: execute o teste. Deve ser bem-sucedido.


11.3.11. Gestão de registos

Os registos da aplicação de consola ou do teste JUnit são configurados pelo seguinte ficheiro [logback.xml]:

  

O ficheiro deve chamar-se [logback.xml] e estar no Classpath do projeto. Para tal, foi colocado aqui na pasta [src/main/resources], que faz parte do Classpath. O seu conteúdo é o seguinte:


<configuration> 

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- Aos codificadores é atribuído, por predefinição, o tipo
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- controlo do nível dos registos -->
  <root level="info"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>
  • linha 12: a tag [<root level="info">] apresenta os registos de nível [info]. Em vez de [info], pode-se colocar:
    • [debug]: este é o nível mais detalhado de registos. Recomenda-se a sua utilização durante a fase de depuração do projeto, uma vez que contém registos muito interessantes sobre as interações cliente/servidor. É uma forma de compreender o que se passa «por baixo do capô»;
    • [off]: sem registos;
    • [warn]: um nível intermédio de registos em que o Spring apresenta anomalias que, no entanto, não são necessariamente erros. É necessário analisá-las caso não se obtenha o resultado esperado;

Tarefa a realizar: altere o nível na linha 12 para [debug] e, em seguida, execute o teste unitário. Observe a diferença nos registos.


11.3.12. Geração do arquivo Maven do projeto

Para instalar o arquivo do projeto no repositório local do Maven, proceda da seguinte forma [1-3]:

O arquivo será gerado com os identificadores encontrados no ficheiro [pom.xml]:


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

A localização do repositório local do Maven pode ser encontrada na configuração do Eclipse:

 

É então possível verificar se o artefacto Maven foi instalado corretamente:

 

A partir de agora, outro projeto Maven local poderá utilizar este arquivo.