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):
É, portanto, necessário que o tipo T tenha um campo denominado [something]. Assim, o método
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:
- 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:
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:
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.





































