6. Spring Data JPA Hibernate
6.1. Introduction
Vamos retomar a base de dados [dbproduitscategories] gerida pelo projeto [spring-jdbc-04] e implementar as duas interfaces [IDao<Categorie>, IDao<Produit>] definidas nesse projeto. Isto vai permitir-nos várias coisas:
- comparar os códigos de implementação;
- utilizar a mesma camada de testes;
- comparar o desempenho das duas implementações;
![]() |
- a camada [JDBC] é implementada pelo projeto [mysql-config-jdbc] analisado no parágrafo 3.3;
Passamos agora às outras camadas.
6.2. Configuração do ambiente de trabalho
Com o STS, importe o projeto [mysl-config-jpa-hibernate] [1], que se encontra na pasta [<exemples>/spring-database-config/mysql/eclipse] [2]:
![]() |
Este projeto configura a camada [Spring JPA Hibernate] do projeto. Cada implementação JPA tem o seu próprio projeto de configuração.
Em seguida, importe o projeto [spring-jpa-generic] [1] que se encontra na pasta [<exemples>/spring-database-generic/spring-jpa] [2]:
![]() |
Feito isto, reinicie o ambiente Maven (Alt-F5) de todos os projetos presentes em [Package Explorer]:
![]() |
Em seguida, para verificar o ambiente de trabalho, execute a configuração de execução denominada [spring-jpa-generic-JUnitTestDao-hibernate]:
![]() |
Esta configuração executa o teste [JUnitTestDao]. Este teste deve ser bem-sucedido:
![]() |
6.3. O projeto de configuração da camada JPA
![]() |
Este projeto tem como função configurar a camada JPA da arquitetura abaixo:
![]() |
6.3.1. Configuração do Maven
O projeto é um projeto Maven e está configurado pelo seguinte ficheiro [pom.xml]:
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jpa</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>configuration mysql openjpa</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- dependências variáveis ********************************************** -->
<!-- JPA fornecedor -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
</dependency>
<!-- dependências constantes ********************************************** -->
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<!-- Spring Context -->
<!-- configuração herdada JDBC -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</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>
- linhas 5-7: o artefacto Maven gerado por este projeto. Os projetos de configuração das outras implementações JPA (Eclipselink e OpenJpa) utilizarão este mesmo artefacto. Isto significa que apenas um destes projetos pode estar ativo de cada vez. Por isso, deve evitar-se ter todos eles presentes no [Package Explorer]. Basta apenas um;
- linhas 10-14: o projeto Maven pai, que define a versão da maioria das dependências necessárias ao projeto;
- linhas 19-22: a biblioteca Hibernate;
- linhas 25-28: a biblioteca Spring Data;
- linhas 32-34: o projeto de configuração da camada JPA baseia-se no projeto de configuração da camada JDBC, que define, entre outras coisas, o controlador JDBC do SGBD utilizado e as coordenadas da base de dados a utilizar;
- linhas 35-39: o projeto de configuração da camada JDBC inclui a biblioteca [Spring JDBC], que aqui é substituída pela biblioteca [Spring Data JPA]. Por isso, recomenda-se que não seja incluída nas dependências do projeto. No entanto, se permanecer, isso não causará erros;
No final, as dependências do projeto são as seguintes:
![]() |
6.3.2. Configuração do Spring
![]() |
A classe [ConfigJpa] configura o projeto Spring:
package generic.jpa.config;
import javax.persistence.EntityManagerFactory;
import generic.jdbc.config.ConfigJdbc;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
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;
@Configuration
@Import({ ConfigJdbc.class })
public class ConfigJpa {
// o provedor JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
hibernateJpaVendorAdapter.setGenerateDdl(true);
return hibernateJpaVendorAdapter;
}
// pacotes de entidades JPA
public final static String[] ENTITIES_PACKAGES = { "generic.jpa.entities.dbproduitscategories" };
// fonte de dados
@Bean
public DataSource dataSource() {
// fonte de dados TomcatJdbc
DataSource dataSource = new DataSource();
// configuração de acesso JDBC
dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
// ligações abertas inicialmente
dataSource.setInitialSize(5);
// resultado
return dataSource;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan(ENTITIES_PACKAGES);
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 18: a classe é uma classe de configuração do Spring;
- linha 19: importa os beans definidos pela classe de configuração [ConfigJdbc], que foi utilizada para configurar o projeto Spring [mysql-config-jdbc]. Trata-se dos filtros jSON;
- linhas 23-30: definem a implementação JPA utilizada, neste caso a implementação do Hibernate (linha 25);
- linha 26: é possível optar por mostrar ou não as operações SQL executadas pela implementação Hibernate;
- linha 27: indica-se ao Hibernate o SGBD ligado. Esta configuração é importante. Permite que o Hibernate utilize o dialeto SQL do SGBD MySQL, incluindo a sua parte proprietária. Além disso, fornece-lhe informações sobre os tipos SQL e os objetos de SGBD que poderá utilizar. É esta capacidade da implementação JPA de se adaptar a um SGBD específico que lhe confere uma grande portabilidade entre SGBD;
- linha 28: O Hibernate pode ou não gerar as tabelas da base de dados de destino a partir das entidades JPA que encontrar. Esta geração só ocorre se as tabelas estiverem ausentes. Se já estiverem presentes, nada é feito. Utilizaremos esta capacidade de gerar tabelas quando apresentarmos como foram gerados os scripts SQL para a geração das diferentes bases de dados utilizadas neste documento;
- linha 33: o pacote no qual se encontram as entidades JPA da base [dbproduitscategories];
- linhas 36-49: a fonte de dados [tomcat-jdbc] associada à base de dados [dbproduitscategories];
- linhas 52-60: o bean denominado [entityManagerFactory] (deve ter este nome) é o bean que irá criar o objeto [EntityManager], que gere o contexto de persistência JPA. Todas as operações JPA passam por ele. A utilização de [Spring Data JPA] faz com que nunca venhamos a utilizar este objeto diretamente. No entanto, temos de o configurar. Ele precisa de saber o seguinte:
- a implementação JPA utilizada (linha 55);
- a fonte de dados utilizada (linha 57);
- as entidades JPA dessa fonte (linha 56);
- linha 58: inicializa o EntityManager com estas informações;
- linha 59: devolve o singleton [entityManagerFactory];
- linhas 63-68: definem o gestor de transações. Deve chamar-se [transactionManager];
- linha 65: é criado um gestor de transações JPA;
- linha 66: este é ligado à fonte de dados da linha 37 através do bean [entityManagerFactory] (linhas 53 e 57);
Apenas o bean das linhas 23-30 depende da implementação JPA utilizada. Os restantes beans dependem, por sua vez, deste.
6.3.3. As entidades da camada [JPA]
![]() |
![]() |
A base de dados de destino é a base [dbproduitscategories], com as suas duas tabelas [CATEGORIES] e [PRODUITS]. Vimos que esta base de dados possui também outras três tabelas, [USERS, ROLES, USERS_ROLES], que serão utilizadas para proteger o serviço web a ser implementado na Internet. Por enquanto, ignoraremos estas tabelas. Recorde-se, a título de referência, a estrutura das tabelas [CATEGORIES] e [PRODUITS]:
A tabela [PRODUITS] é a seguinte:
![]() |
- [ID]: a chave primária autoincrementada da tabela [2];
- [NOM]: o nome único do produto [4];
- [PRIX]: o preço do produto;
- [DESCRIPTION]: a descrição do produto;
- [VERSIONING] é o número de versão do produto. A sua versão inicial é 1 [3]. Sempre que o produto for alterado, o seu número de versão será incrementado pelo código que explora a tabela;
- [CATEGORIE_ID]: a chave estrangeira na tabela [CATEGORIES] para indicar a categoria a que o produto pertence;
![]() |
- em [1-3], a chave estrangeira [CATEGORIE_ID] da tabela [PRODUITS]. Esta chave visa a coluna [ID] da tabela [CATEGORIES] [4-5];
- quando uma categoria é eliminada, todos os produtos a ela associados são igualmente eliminados ([6]). É importante referir este ponto, uma vez que é utilizado na construção da camada [DAO], que explora a base de dados [dbproduitscategories];
A tabela [CATEGORIES] das categorias é a seguinte:
![]() |
- [ID]: chave primária autoincrementada;
- [VERSIONING]: número de versão da categoria;
- [NOM]: nome único da categoria;
Vamos agora descrever as entidades JPA, [Produit] e [Categorie], que correspondem às tabelas [PRODUITS] e [CATEGORIES].
![]() |
6.3.3.1. A interface [AbstractCoreEntity]
A interface [AbstractCoreEntity] é implementada pelas entidades JPA, [Categorie] e [Produit]:
package generic.jpa.entities.dbproduitscategories;
public interface AbstractCoreEntity {
// getters e setters dos campos [id], [version], [entityType]
public Long getId();
public void setId(Long id);
public Long getVersion();
public void setVersion(Long version);
public enum EntityType {
PROXY, POJO
}
public EntityType getEntityType();
public void setEntityType(EntityType entityType);
}
Esta interface, implementada pelas duas entidades JPA, serve simplesmente para listar os métodos para ler/escrever os campos [id], [version] e [entityType] dessas entidades. A função do campo [entityType] será explicada posteriormente;
6.3.3.2. A entidade JPA [Produit]
A classe [Produit] é a entidade JPA associada a uma linha da tabela [PRODUITS]:
![]() |
package generic.jpa.entities.dbproduitscategories;
import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = ConfigJdbc.TAB_PRODUITS)
@JsonFilter("jsonFilterProduit")
public class Produit implements AbstractCoreEntity {
// propriedades
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = ConfigJdbc.TAB_JPA_ID)
protected Long id;
@Version
@Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
protected Long version;
@Transient
protected EntityType entityType = EntityType.POJO;
@Transient
@JsonIgnore
protected String simpleClassName = getClass().getSimpleName();
// propriedades
@Column(name = ConfigJdbc.TAB_PRODUITS_NOM, unique = true, length = 30, nullable = false)
private String nom;
@Column(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID, insertable = false, updatable = false, nullable = false)
private Long idCategorie;
@Column(name = ConfigJdbc.TAB_PRODUITS_PRIX, nullable = false)
private double prix;
@Column(name = ConfigJdbc.TAB_PRODUITS_DESCRIPTION, length = 100)
private String description;
// a categoria
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID)
private Categorie categorie;
// fabricantes
public Produit() {
}
public Produit(Long id, Long version, String nom, Long idCategorie, double prix, String description,
Categorie categorie) {
this.id = id;
this.version = version;
this.nom = nom;
this.idCategorie = idCategorie;
this.prix = prix;
this.description = description;
this.categorie = categorie;
}
// assinatura
public String toString() {
return String.format("[id=%s, version=%s, nom=%s, prix=10.2f, desc=%s, idCategorie=%s]", id, version, nom, prix,
description, idCategorie);
}
// ------------------------------------------------------------
// redefinição de [equals] e [hashcode]
@Override
public int hashCode() {
Long id = getId();
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractCoreEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractCoreEntity other = (AbstractCoreEntity) entity;
Long id = getId();
Long otherId = other.getId();
return id != null && otherId != null && id.equals(otherId);
}
// getters e setters
...
public void setCategorie(Categorie categorie) {
// tipo da entidade
if (entityType == EntityType.PROXY) {
throw new ProxyException(1005, new RuntimeException(
"On ne peut changer la catégorie d'un produit de type [PROXY]"), simpleClassName);
}
this.categorie = categorie;
}
}
- linha 21: a anotação [@Entity] torna a classe [Produit] uma entidade gerida pela camada [JPA]. Também se pode escrever [@Entity(name="MonProduit")], o que atribui o nome [MonProduit] à entidade. Na ausência desta informação, o nome da entidade é o nome da classe, neste caso [Produit]. Esta nomenclatura torna-se necessária quando, entre as entidades, existem duas classes de pacotes diferentes com o mesmo nome;
- linha 22: a anotação [@Table(name = "PRODUITS")] indica que a classe [Produit] é a imagem objeto de uma linha da tabela [PRODUITS] da base de dados;
- linha 23: o nome do filtro jSON a aplicar à entidade. Veremos que a propriedade [categorie] da linha 58 nem sempre está disponível. É, portanto, necessário excluí-la da representação jSON do objeto. Para tal, precisamos de um filtro. É, portanto, num filtro denominado [jsonFilterCategorie] que indicaremos se queremos ou não a propriedade [categorie];
- linha 26: a anotação [@Id] define o campo anotado como o campo associado à chave primária da tabela da linha 19;
- linha 27: a anotação [@GeneratedValue(strategy = GenerationType.IDENTITY)] define o modo de geração automática da chave primária na tabela [PRODUITS]. É o atributo [strategy] que o define. Existem diferentes modos:

A estratégia [IDENTITY] não está disponível para todos os SGBD. Entre os seis SGBD testados, esta estratégia esteve disponível para os SGBD e [MySQL 5, PostgreSQL 9.4, SQL Server 2014, DB2 Express-C10.5]. Para os outros dois [Oracle Express 11g Release 2, Firebird 2.5.4], foi necessário utilizar a estratégia [SEQUENCE]. Para a portabilidade entre implementações JPA, não se deve utilizar a estratégia [AUTO], que deixa à discrição da implementação JPA a escolha da estratégia de geração da chave primária. Assim, com MySQL 5 e a estratégia [AUTO]:
- O Hibernate escolhe a estratégia [IDENTITY] com o modo [AUTO_INCREMENT] para a chave primária;
- EclipseLink escolhe a estratégia [TABLE], que cria uma tabela denominada, por predefinição, [SEQUENCE], à qual é necessário consultar para obter as chaves primárias.
Em suma, a estrutura da base de dados gerida por estas duas implementações, JPA, não é a mesma. Se tiver sido gerada pelo Hibernate, não será utilizável pelo EclipseLink e vice-versa.
- linha 28: a anotação [@Column(name="ID"] define o nome da coluna da tabela [PRODUITS] a associar ao campo [id];
- linha 29: utiliza-se o tipo [Long] em vez de [long] para a chave primária. Com efeito, as chaves primárias [null] têm um significado específico para JPA. Por isso, é preferível utilizar aqui um tipo de objeto em vez de um tipo simples;
- linha 31: 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);
- linha 36: o tipo da entidade. Haverá dois: POJO e PROXY. Por predefinição, a instância gerada será um POJO (Plain Old Java Object). Em alguns casos, as instâncias [Produit] recuperadas da base de dados serão do tipo [PROXY]. Isto acontecerá quando a propriedade [Categorie categorie] da linha 58 não tiver sido inicializada com uma categoria devido ao atributo [fetch = FetchType.LAZY] da linha 56. Neste caso, as implementações JPA que serão testadas diferem:
- [Hibernate, OpenJPA]: aceder à categoria de um produto do tipo [PROXY] provoca uma exceção. O Hibernate utiliza o termo «proxy» para designar uma instância JPA obtida no modo [LAZY]. Foi por isso que utilizei este termo para designar este tipo de entidade;
- [EclipseLink]: aceder à categoria de um produto do tipo [PROXY] provoca a pesquisa dessa categoria na base de dados e não ocorre qualquer exceção;
Como pretendia ter uma camada de testes independente da implementação JPA utilizada, quis saber o tipo de cada entidade: POJO ou PROXY. Foi por isso que adicionei o campo [entityType] às entidades JPA;
- linha 35: a anotação [@Transient] indica que a implementação JPA deve ignorar este campo. Com efeito, este não existe nas tabelas do SGBD;
- linha 40: a classe [Produit] lança uma exceção do tipo [ProxyException] que requer o nome da classe;
- linha 38: tal como anteriormente, indica-se que a implementação JPA deve ignorar este campo;
- linha 39: a anotação [@JsonIgnore] indica que o serializador/deserializador jSON de uma instância [Produit] deve ignorar este campo;
- linha 43: a anotação [@Column] associa o campo [nom] à coluna [NOM] da tabela [PRODUITS]. Quando o campo tem o mesmo nome que a coluna associada (sem distinção entre maiúsculas e minúsculas), a anotação [@Column] pode ser omitida. Seria esse o caso aqui. Os atributos [unique = true, length = 30, nullable = false] só são utilizados quando a implementação JPA tem de gerar a tabela [CATEGORIES] a partir da entidade [Produit]. Estes serão traduzidos pelos atributos SQL e [UNIQUE, VARCHAR(30), NOT NULL], que fazem com que a coluna [NOM] tenha, no máximo, 30 caracteres, seja única na tabela e não possa ter o valor NULL;
- linhas 46-47: o campo [idCategorie] está associado à coluna [CATEGORIE_ID]. Voltaremos a abordar os seus atributos mais adiante;
- linhas 49-50: o campo [prix] está associado à coluna [PRIX];
- linhas 52-53: o campo [description] está associado à coluna [DESCRIPTION];
- linhas 56-58: a categoria do produto;
- linha 56: a anotação [@ManyToOne] indica que a coluna da anotação da linha 57, [@JoinColumn(name = "CATEGORIE_ID")], é uma chave estrangeira da tabela [PRODUITS] daentidade [Produit] na tabela [CATEGORIES] associada à entidade da linha 58. Esta anotação deve referir-se a uma entidade JPA. Assim, a classe da linha 58 deve ser uma entidade JPA;
- linha 56: a anotação [fetch = FetchType.LAZY] determina que, quando se recupera um produto da tabela [PRODUITS], a sua categoria (linha 58) não seja recuperada imediatamente (carregamento diferido). Esta é então obtida aquando da primeira chamada ao método [getCategorie]. Para tal, durante a execução, a camada JPA complementa o método inicial [getCategorie] (que se limita a devolver o campo categorie) com uma chamada ao SGBD para ir buscar a categoria — uma técnica denominada «proxying». As implementações JPA diferem na forma como implementam esta característica, tal como referimos anteriormente. 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 na linha 23. A coluna de junção [CATEGORIE_ID] da tabela [PRODUITS] é atualizada automaticamente aquando da inserção ou atualização do produto. Recebe o valor de [categorie.getId()], sendo que [categorie] é o campo da linha 58. A especificação JPA impõe que esta coluna de junção não possa ser atualizada por qualquer outro meio. Além disso, impõe os atributos [insertable = false, updatable = false] da linha 46, que fazem com que a coluna [CATEGORIE_ID] (ou seja, a coluna de junção) associada ao campo [idCategorie] não possa ser alterada pelo campo [idCategorie]. Apenas será possível a transferência da coluna [CATEGORIE_ID] para o campo [idCategorie];
- linhas 91-104: a igualdade entre as entidades [Produit] é definida como sendo a igualdade entre as suas chaves primárias [id];
- linhas 108-115: para tornar a nossa camada de testes portátil, iremos gerir de forma uniforme as entidades [PROXY] das três implementações JPA e [Hibernate, EclipseLink, OpenJpa]. Para um tipo [Produit] do tipo [PROXY], impediremos a alteração do valor do campo [categorie]. A classe [ProxyException] é a seguinte:
![]() |
package generic.jpa.infrastructure;
import generic.jdbc.infrastructure.UncheckedException;
public class ProxyException extends UncheckedException {
private static final long serialVersionUID = 7278276670314994574L;
public ProxyException() {
}
public ProxyException(int code, Throwable e, String simpleClassName) {
super(code, e, simpleClassName);
}
}
Para concluir a análise desta entidade, é importante referir que as anotações e os seus atributos são utilizados em dois casos bem distintos:
- para criar as tabelas da base de dados;
- para as explorar. Neste caso, a implementação JPA espera encontrar as tabelas tal como ela própria as teria gerado. Por conseguinte, não é possível associar à entidade [Produit] anterior qualquer tabela [PRODUITS]. Esta deve possuir, no mínimo (podendo ter outras), as características da tabela [PRODUITS] que a própria teria gerado. Ao trabalhar com a JPA, o ideal é partir de uma base de dados vazia, na qual se deixa a JPA gerar as tabelas. Abordaremos esta geração um pouco mais adiante. O script SQL fornecido para o SGBD MySQL foi gerado a partir das tabelas geradas pelo JPA.
Todos os atributos da entidade [Produit] são utilizados para a geração da tabela [PRODUITS]. Quando isto é feito, os atributos de geração, tais como [unique = true, length = 30, nullable = false], deixam de ser utilizados durante a exploração das tabelas.
6.3.3.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 generic.jpa.entities.dbproduitscategories;
import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = ConfigJdbc.TAB_CATEGORIES)
@JsonFilter("jsonFilterCategorie")
public class Categorie implements AbstractCoreEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = ConfigJdbc.TAB_JPA_ID)
protected Long id;
@Version
@Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
protected Long version;
@Transient
protected EntityType entityType = EntityType.POJO;
@Transient
@JsonIgnore
protected String simpleClassName = getClass().getSimpleName();
// propriedades
@Column(name = ConfigJdbc.TAB_CATEGORIES_NOM, unique = true, length = 30, nullable = false)
private String nom;
// produtos associados
@OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
private List<Produit> produits;
// fabricantes
public Categorie() {
}
public Categorie(Long id, Long version, String nom, List<Produit> produits) {
this.id = id;
this.version = version;
this.nom = nom;
this.produits = produits;
}
// assinatura
public String toString() {
return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
}
// métodos
public void addProduit(Produit produit) {
// tipo da entidade
if (entityType == EntityType.PROXY) {
throw new ProxyException(1004, new RuntimeException(
"On ne peut ajouter de produits à une catégorie de type [PROXY]"), simpleClassName);
}
// adição de um produto
if (produits == null) {
produits = new ArrayList<Produit>();
}
if (produit != null) {
// adiciona-se o produto
produits.add(produit);
// define-se a sua categoria
produit.setCategorie(this);
produit.setIdCategorie(this.id);
}
}
// ------------------------------------------------------------
// redefinição de [equals] e [hashcode]
@Override
public int hashCode() {
Long id = getId();
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractCoreEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractCoreEntity other = (AbstractCoreEntity) entity;
Long id = getId();
Long otherId = other.getId();
return id != null && otherId != null && id.equals(otherId);
}
// getters e setters
...
}
- linha 24: a classe é uma entidade JPA;
- linha 25: associada à tabela [CATEGORIES];
- linha 26: a representação jSON da entidade [Categorie] é controlada pelo filtro denominado [jsonFilterCategorie]. Este deverá ser configurado antes de qualquer pedido de representação jSON da entidade. O filtro [jsonFilterCategorie] será utilizado para excluir ou não da representação jSON da entidade [Categorie] o campo [produits] da linha 40;
- linhas 29-32: o campo [id] está associado à chave primária [ID] da tabela [CATEGORIES]. O modo de geração selecionado é o modo [IDENTITY], pelo que o modo [AUTO_INCREMENT] se aplica a MySQL;
- linhas 34-36: o campo [version] está associado à coluna de controlo de versões [VERSIONING] da tabela [CATEGORIES];
- linhas 38-39: o tipo da entidade [Categorie];
- linhas 41-43: o nome simples da classe [Categorie];
- linhas 46-47: o campo [nom] está ligado à coluna [NOM] da tabela [CATEGORIES]. Atribuem-se-lhe os atributos JPA e [unique = true, length = 30, nullable=false] para que, aquando da geração da tabela [CATEGORIES], a coluna [NOM] tenha os atributos SQL e [UNIQUE, VARCHAR(30), NOT NULL];
- linhas 50-51: os produtos que pertencem à categoria;
- linha 50: 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 51. É possível indicar cascatas parciais com as constantes [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE];
- linha 50: o atributo [fetch = FetchType.LAZY] determina que, quando se recupera uma categoria da tabela [CATEGORIES], os seus produtos não sejam recuperados imediatamente. Estes são, então, recuperados na primeira chamada ao método [getProduits]. Para tal, durante a execução, a camada JPA complementa o método inicial [getProduits] (que se limita a devolver o campo produits) com uma chamada ao SGBD para ir buscar os produtos da categoria. 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 26, que nos permitirá indicar se queremos ou não essa propriedade, e o tipo da entidade na linha 39;
- linhas 71-88: o método [addProduit] permite adicionar um produto à categoria;
- linhas 73-76: para uniformizar a gestão dos proxies entre diferentes implementações JPA, decidiu-se que não era possível adicionar produtos a uma entidade [Categorie] do tipo PROXY;
- linhas 92-112: duas entidades [Categorie] serão consideradas iguais se tiverem a mesma chave primária [id];
6.3.4. O ficheiro [persistence.xml]
![]() |
As aplicações JPA devem definir determinadas propriedades do fornecedor JPA utilizado, bem como as entidades JPA a utilizar, num ficheiro [META-INF/persistence.xml] presente no Classpath da aplicação. No exemplo acima, foi colocado na pasta [src/main/resources], que faz efetivamente parte do Classpath de um projeto Eclipse. Quando se utiliza o JPA em conjunto com o Spring, algumas informações que deveriam constar no ficheiro [persistence.xml] são colocadas noutro local, nas classes de configuração do Spring. Numa aplicação Spring JPA, é o Spring que controla o JPA. Com o Spring JPA Hibernate, o ficheiro [persistence.xml] pode ser reduzido à sua forma mais simples:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
<persistence-unit name="dummy-persistence-unit" transaction-type="RESOURCE_LOCAL" />
</persistence>
- linhas 1-5: um ficheiro [persistence.xml] deve ter uma baliza raiz <persistence>. Os atributos da baliza da linha 2 não serão utilizados nesta aplicação;
- um ficheiro de persistência pode definir uma ou mais unidades de persistência com a baliza <persistence-unit> (linha 4). Uma unidade de persistência gere o acesso a uma base de dados específica. Se a aplicação gerir duas bases de dados em simultâneo, terá duas unidades de persistência;
- linha 4: uma unidade de persistência tem um nome [attribut name], suporta um tipo de transação [attribut transaction-type], possui propriedades e define as entidades associadas às tabelas da base de dados gerida pela unidade de persistência. Aqui, como os acessos à base de dados serão geridos por [Spring JPA Hibernate], estas duas últimas informações podem ser colocadas noutro local. Existem dois tipos de transação:
- [RESOURCE_LOCAL]: as transações são geridas pela própria aplicação. É o caso aqui, em que será o Spring a gerir as transações;
- [JTA] (Transação Java API): é o contentor EJB (Enterprise Java Bean) que executa a aplicação que irá gerir automaticamente as transações com base nas anotações Java encontradas no código. Não estamos nessa configuração aqui;
Veremos mais adiante que o conteúdo deste ficheiro [persistence.xml] depende da implementação JPA utilizada.
6.4. O projeto [spring-jpa-generic]
Recorde-se o que pretendemos fazer. Pretendemos implementar a seguinte arquitetura:
![]() |
na qual a camada [DAO] implementaria a interface [IDao<Produit>, IDao<Categorie>] analisada no capítulo 4. Trata-se de comparar duas implementações desta interface:
- uma construída com o Spring JDBC;
- a outra construída com o Spring JPA;
Na arquitetura acima:
- a camada [JDBC] é implementada pelo projeto [mysql-config-jdbc], analisado no parágrafo 3.3;
- a camada [JPA] é implementada pelo projeto [mysql-config-jpa-hibernate], analisado no parágrafo 6.3;
O projeto [spring-jpa-generic] assegura a implementação das camadas [DAO] e [Spring Data].
![]() |
6.4.1. Configuração do Maven
O projeto [spring-jpa-generic] é um projeto Maven configurado pelo seguinte ficheiro [pom.xml]:
<?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>dvp.spring.database</groupId>
<artifactId>spring-jpa-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-jpa-generic</name>
<description>démo spring data avec tables de catégories et de produits</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- configuração JPA do SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jpa</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</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>
- linhas 22-26: o projeto tem apenas uma única dependência, a do projeto que configura a camada [JPA] da aplicação e que acabámos de analisar. Trata-se de uma aplicação genérica:
- alteramos o SGBD alterando o projeto de configuração da camada [JDBC];
- alteramos a implementação JPA alterando o projeto de configuração da camada [JPA];
No final, as dependências são as seguintes:
![]() |
6.4.2. Configuração Spring
![]() |
A classe [AppConfig] configura o projeto Spring:
package spring.data.config;
import generic.jpa.config.ConfigJpa;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories(basePackages = { "spring.data.repositories" })
@Configuration
@ComponentScan(basePackages = { "spring.data.dao" })
@Import({ ConfigJpa.class })
public class AppConfig {
}
- linha 11: a classe é uma classe de configuração do Spring;
- linha 10: a anotação [@EnableJpaRepositories] serve para indicar os pacotes que contêm as interfaces [CrudRepository] do Spring Data. Isto torna-os componentes Spring que podem ser injetados noutros componentes Spring;
- linha 12: a anotação [@ComponentScan] indica que o pacote [spring.data.dao] deve ser analisado para procurar componentes Spring. Serão encontrados os componentes [DaoCategorie] e [DaoProduit];
- linha 13: os beans da classe de configuração [ConfigJpa] são importados. Nela encontrar-se-á o bean da implementação JPA utilizada (Hibernate, Eclipselink, OpenJpa), a fonte de dados a utilizar, o EntityManager que irá gerir as operações JPA, o gestor de transações;
6.4.3. A camada [Spring Data]
![]() |
![]() |
6.4.3.1. A interface [CategoriesRepository]
A interface [CategoriesRepository] gere os acessos à tabela [CATEGORIES]:
package spring.data.repositories;
import generic.jpa.entities.dbproduitscategories.Categorie;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
public interface CategoriesRepository extends CrudRepository<Categorie, Long> {
// categoria com os seus produtos
@Query("select c from Categorie c left join fetch c.produits where c.id=?1")
public Categorie getLongCategorieById(Long id);
@Query("select c from Categorie c left join fetch c.produits where c.nom=?1")
public Categorie getLongCategorieByName(String nom);
@Query("select c from Categorie c where c.nom in ?1")
public List<Categorie> getShortCategoriesByName(Iterable<String> names);
@Query("select c from Categorie c where c.id in ?1")
public List<Categorie> getShortCategoriesById(Iterable<Long> ids);
@Query("select distinct c from Categorie c left join fetch c.produits where c.id in ?1")
public List<Categorie> getLongCategoriesById(List<Long> names);
@Query("select distinct c from Categorie c left join fetch c.produits where c.nom in ?1")
public List<Categorie> getLongCategoriesByName(List<String> names);
@Query("select c from Categorie c")
public List<Categorie> getAllShortCategories();
@Query("select distinct c from Categorie c left join fetch c.produits")
public List<Categorie> getAllLongCategories();
}
- linha 10: a interface [CrudRepository] foi utilizada e explicada no parágrafo 5.1.3. Recorde-se que:
- o primeiro tipo de parâmetro da interface é a entidade JPA, gerida para os acessos CRUD (findOne, findAll, guardar, eliminar, deleteAll),
- o segundo tipo de parâmetro da interface é o da chave primária da entidade JPA, neste caso um inteiro [Long];
Os métodos da interface são implementados por consultas JPQL (Java Persistence Query Language). Esta consulta consulta 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;
Tomemos o exemplo das linhas 31-32: o método da linha 32 devolve todas as categorias da base de dados na sua versão abreviada. É implementado pela consulta JPQL (Java Persistence Query Language) da linha 31, que se assemelha bastante à sua equivalente SQL. Para aprofundar o conhecimento sobre a JPQL, pode-se consultar a [ref2] (ver parágrafo 1.2).
Os métodos da interface [CategoriesRepository] são os seguintes:
- linhas 13-14: o método [getLongCategorieById] devolve a versão completa de uma categoria referenciada pela sua chave primária [id], ou seja, a 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 16-17: o método [getLongCategorieByName] devolve a versão longa de uma categoria referenciada pelo seu nome [nom];
- linhas 19-20: o método [getShortCategoriesByName] devolve as versões curtas das categorias referenciadas pelos seus nomes. O campo [produits] destas categorias não é null. Contém a referência a um proxy (uma classe criada pela implementação JPA) cuja função é devolver os produtos da categoria quando é chamado. A sua chamada fora do contexto de persistência JPA provoca uma exceção (Hibernate e OpenJpa, mas não EclipseLink). Por este motivo, não utilizaremos o campo [produits] da versão curta de uma categoria;
- linhas 22-23: o método [getShortCategoriesById] devolve as versões curtas das categorias referenciadas pelas suas chaves primárias [id];
- linhas 25-26: o método [getLongCategoriesById] devolve as versões longas das categorias referenciadas pelas suas chaves primárias [id];
- linhas [28-29]: o método [getLongCategoriesByName] devolve as versões longas das categorias referenciadas pelos seus nomes;
- linhas 31-32: o método [getAllShortCategories] devolve as versões curtas de todas as categorias;
- linhas 34-35: o método [getAllLongCategories] devolve as versões completas de todas as categorias;
Nota: nem todas as implementações JPA aceitam a mesma sintaxe JPQL. Assim, a seguinte sintaxe é aceite pelo Hibernate e pelo EclipseLink, mas não pelo OpenJpa:
@Query("select c from Categorie c left join fetch c.produits p where c.nom=?1")
O OpenJpa não aceita o alias [p] acima referido.
6.4.3.2. A interface [ProduitsRepository]
A interface [ProduitsRepository] gere os acessos à tabela [PRODUITS]:
package spring.data.repositories;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Transactional;
@Transactional()
public interface ProduitsRepository extends CrudRepository<Produit, Long> {
// um produto com a sua categoria
@Query("select p from Produit p left join fetch p.categorie where p.id=?1")
public Produit getLongProduitById(Long id);
@Query("select p from Produit p left join fetch p.categorie where p.nom=?1")
public Produit getLongProduitByName(String nom);
@Query("select p from Produit p where p.id in ?1")
public List<Produit> getShortProduitsById(List<Long> ids);
@Query("select p from Produit p where p.nom in ?1")
public List<Produit> getShortProduitsByName(List<String> names);
@Query("select distinct p from Produit p left join fetch p.categorie where p.id in ?1")
public List<Produit> getLongProduitsById(List<Long> ids);
@Query("select distinct p from Produit p left join fetch p.categorie where p.nom in ?1")
public List<Produit> getLongProduitsByName(List<String> names);
@Query("select distinct p from Produit p left join fetch p.categorie")
public List<Produit> getAllLongProduits();
@Query("select p from Produit p")
public List<Produit> getAllShortProduits();
}
- linhas [15-16]: o método [getLongProduitById] devolve a versão completa de um produto identificado pela sua chave primária [id], ou seja, incluindo a sua categoria. Recorde-se que, na entidade [Produit], o campo [categorie] tinha o atributo [fetch = FetchType.LAZY] (carregamento diferido). Na consulta JPQL, forçamos o carregamento da categoria com a palavra-chave [fetch];
- linhas 18-19: o método [getLongProduitByName] devolve a versão longa de um produto identificado pelo seu nome;
- linhas 21-22: o método [getShortProduitsById] devolve a versão curta dos produtos identificados pela sua chave primária [id]. Nesta versão curta, o campo [categorie] não tem o valor null. Contém a referência de um proxy gerado pela implementação JPA, que, se for chamado, irá buscar a categoria do produto. Esta chamada só pode ser efetuada no contexto de persistência JPA. Efetuá-la noutro local provoca uma exceção (Hibernate e OpenJpa, mas não EclipseLink). Portanto, na camada [DAO] ou noutro local, não utilizaremos o campo [categorie] de um produto na sua versão curta. Na versão curta do produto, o campo [idCategorie] é inicializado. O seu valor é a chave primária da categoria à qual o produto pertence. Isto permite, posteriormente, solicitar essa categoria à camada [DAO] através do método [DaoCategorie. getShortCategoriesById(idCategorie)];
- linhas 24-25: o método [getShortProduitsByName] devolve a versão curta dos produtos identificados pelos seus nomes;
- linhas 27-28: o método [getLongProduitsById] devolve a versão longa dos produtos identificados pelas suas chaves primárias;
- linhas 30-31: o método [getLongProduitsByName] devolve a versão longa dos produtos identificados pelos seus nomes;
- linhas 33-34: o método [getAllLongProduits] devolve a versão longa de todos os produtos;
- linhas 36-37: o método [getAllShortProduits] devolve a versão curta de todos os produtos;
Estas interfaces serão implementadas por classes geradas pela implementação JPA no momento da execução do projeto. Estas classes são designadas por classes [proxy]. Por predefinição, os métodos da interface [CrudRepository] são executados numa transação. O facto de as interfaces [ProduitsRepository, CategoriesRepository] estenderem a classe [CrudRepository] torna-as componentes Spring. Como tal, podem ser injetadas noutros componentes Spring.
6.4.4. A camada [DAO]
![]() |
![]() |
6.4.4.1. A interface [IDao<T>]
A interface [IDao<T>] é a que já foi analisada na implementação da camada [DAO] realizada com o Spring JDBC (ver parágrafo 4.7);
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity;
import java.util.List;
public interface IDao<T extends AbstractCoreEntity> {
// lista de todas as entidades T
public List<T> getAllShortEntities();
public List<T> getAllLongEntities();
// de entidades específicas - versão curta
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
public List<T> getShortEntitiesByName(Iterable<String> names);
public List<T> getShortEntitiesByName(String... names);
// entidades específicas - versão longa
public List<T> getLongEntitiesById(Iterable<Long> ids);
public List<T> getLongEntitiesById(Long... ids);
public List<T> getLongEntitiesByName(Iterable<String> names);
public List<T> getLongEntitiesByName(String... names);
// atualização de várias entidades
public List<T> saveEntities(Iterable<T> entities);
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
// eliminação de todas as entidades
public void deleteAllEntities();
// eliminação de várias entidades
public void deleteEntitiesById(Iterable<Long> ids);
public void deleteEntitiesById(Long... ids);
public void deleteEntitiesByName(Iterable<String> names);
public void deleteEntitiesByName(String... names);
public void deleteEntitiesByEntity(Iterable<T> entities);
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}
6.4.4.2. A classe abstrata [AbstractDao]
![]() |
A classe abstrata [AbstractDao] é a classe pai das classes que implementam a camada [DAO]:
- a classe [DaoProduit], que implementa a interface [IDao<Produit>] e gere os acessos à tabela [PRODUITS];
- a classe [DaoCategorie], que implementa a interface [IDao<Categorie>] e gere o acesso à tabela [CATEGORIES];
O seu código é o descrito no parágrafo 4.8, com a seguinte diferença: nenhum método possui o atributo [@Transactional], que faz com que o método seja executado numa transação. Aqui, aproveita-se o facto de as interfaces [CrudRepository] do Spring Data serem executadas, por predefinição, numa transação.
6.4.4.3. A classe [DaoCategorie]
![]() |
A classe [DaoCategorie] implementa a interface [IDao<Categorie>] da seguinte forma:
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;
@Component
public class DaoCategorie extends AbstractDao<Categorie> {
@Autowired
private ProduitsRepository produitsRepository;
@Autowired
private CategoriesRepository categoriesRepository;
@Override
public List<Categorie> getAllShortEntities() {
try {
return setShortCategoriesType(categoriesRepository.getAllShortCategories());
} catch (Exception e) {
throw new DaoException(211, e, simpleClassName);
}
}
private List<Categorie> setShortCategoriesType(List<Categorie> categories) {
for (Categorie categorie : categories) {
categorie.setEntityType(EntityType.PROXY);
}
return categories;
}
@Override
public List<Categorie> getAllLongEntities() {
try {
return categoriesRepository.getAllLongCategories();
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
@Override
public void deleteAllEntities() {
try {
categoriesRepository.deleteAll();
} catch (Exception e) {
throw new DaoException(208, e, simpleClassName);
}
}
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
try {
return setShortCategoriesType(categoriesRepository.getShortCategoriesById(ids));
} catch (Exception e) {
throw new DaoException(203, e, simpleClassName);
}
}
@Override
protected List<Categorie> getShortEntitiesByName(List<String> names) {
try {
return setShortCategoriesType(categoriesRepository.getShortCategoriesByName(names));
} catch (Exception e) {
throw new DaoException(204, e, simpleClassName);
}
}
@Override
protected List<Categorie> getLongEntitiesById(List<Long> ids) {
try {
return categoriesRepository.getLongCategoriesById(ids);
} catch (Exception e) {
throw new DaoException(205, e, simpleClassName);
}
}
@Override
protected List<Categorie> getLongEntitiesByName(List<String> names) {
try {
return categoriesRepository.getLongCategoriesByName(names);
} catch (Exception e) {
throw new DaoException(206, e, simpleClassName);
}
}
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
...
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
try {
categoriesRepository.delete(getShortEntitiesById(ids));
} catch (Exception e) {
throw new DaoException(209, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesByName(List<String> names) {
try {
categoriesRepository.delete(getShortEntitiesByName(names));
} catch (Exception e) {
throw new DaoException(212, e, simpleClassName);
}
}
}
- linha 17: a anotação [@Component] transforma a classe [DaoCategorie] num componente Spring;
- linha 18: a classe [DaoCategorie] estende a classe [AbstractDao<Categorie>], o que faz com que implemente a interface [IDao<Categorie>];
- linhas 20-24: injeção de referências nas duas interfaces [CrudRepository] a partir de [Spring Data]. Esta injeção ocorrerá durante a instanciação dos objetos Spring, geralmente no início da execução do projeto Spring;
- todos os métodos da classe delegam a tarefa aos métodos com os mesmos nomes das interfaces [CrudRepository];
- todos os métodos que convertem as entidades na sua versão resumida indicam-no definindo o tipo da entidade como [EntityType.PROXY] (linhas 29, 63, 72);
O método [saveEntities] merece uma explicação:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// identificam-se os produtos que vão ser inseridos
List<Produit> insertedProduits = new ArrayList<Produit>();
for (Categorie categorie : categories) {
EntityType categorieType = categorie.getEntityType();
List<Produit> produits = null;
if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
for (Produit produit : produits) {
if (produit.getId() == null) {
insertedProduits.add(produit);
}
// aproveita-se para restabelecer (se necessário) a relação produto --> categoria
produit.setCategorie(categorie);
}
}
}
// guardamos as categorias/produtos
try {
categoriesRepository.save(categories);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// atualiza-se o campo [idCategorie] dos produtos inseridos
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// resultado
return categories;
}
- linha 2: as categorias passadas como parâmetros são tanto categorias a inserir ([id==null]) como a modificar ([id!=null]);
- linha 20: as categorias são guardadas com o método [categoriesRepository.save(entities)]. Nos testes, verifica-se que o campo [idCategorie] dos produtos guardados (id==null) não está preenchido. Para resolver este problema, registam-se nas linhas 4-17 os produtos que vão ser inseridos e, uma vez guardados, preenche-se o seu campo [idCategorie] (linhas 25-27);
- linhas 5-17: percorre-se a lista de categorias;
- linhas 8-16: percorre-se, para cada categoria, a respetiva lista de produtos. Aqui surge uma dificuldade. O método [saveEntities] é utilizado tanto para persistir como para modificar uma categoria. Neste último caso, a categoria pode ter sido obtida na sua versão abreviada, contendo, portanto, a referência a um método proxy no campo [produits]. A sua utilização com o Hibernate provoca, então, uma exceção, uma vez que a categoria em questão já não se encontra no contexto de persistência JPA, que foi encerrado com o fim da transação do método que recuperou as versões curtas das categorias. Utiliza-se então o campo [EntityType] da entidade [Categorie], na linha 8, para saber se é possível ou não aceder à lista de produtos da categoria;
- linha 14: associa-se o produto à sua categoria. Normalmente, isso já deveria estar feito. Mas não se sabe como este produto foi criado e se foi associado à sua categoria. Assim, para evitar quaisquer problemas (para gerir a entidade [Produit], a JPA precisa que esta faça referência à entidade [Categorie] à qual está associada), fazemos nós próprios essa associação.
Ao comparar este código com o da classe [DaoProduit] da implementação Spring JDBC (ver parágrafo 4.9) , verifica-se que a biblioteca Spring Data JPA facilita enormemente a escrita da camada [DAO].
6.4.4.4. A classe [DaoProduit]
![]() |
A classe [DaoProduit] implementa a interface [IDao<Produit>] da seguinte forma:
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;
import com.google.common.collect.Lists;
@Component
public class DaoProduit extends AbstractDao<Produit> {
@Autowired
private ProduitsRepository produitsRepository;
@Autowired
private CategoriesRepository categoriesRepository;
@Override
public List<Produit> getAllShortEntities() {
try {
return setShortProduitsType(produitsRepository.getAllShortProduits());
} catch (Exception e) {
throw new DaoException(102, e, simpleClassName);
}
}
private List<Produit> setShortProduitsType(List<Produit> produits) {
for (Produit produit : produits) {
produit.setEntityType(EntityType.PROXY);
}
return produits;
}
@Override
public List<Produit> getAllLongEntities() {
try {
return produitsRepository.getAllLongProduits();
} catch (Exception e) {
throw new DaoException(117, e, simpleClassName);
}
}
@Override
public void deleteAllEntities() {
try {
produitsRepository.deleteAll();
} catch (Exception e) {
throw new DaoException(112, e, simpleClassName);
}
}
@Override
protected List<Produit> getShortEntitiesById(List<Long> ids) {
try {
return setShortProduitsType(produitsRepository.getShortProduitsById(ids));
} catch (Exception e) {
throw new DaoException(103, e, simpleClassName);
}
}
@Override
protected List<Produit> getShortEntitiesByName(List<String> names) {
try {
return setShortProduitsType(produitsRepository.getShortProduitsByName(names));
} catch (Exception e) {
throw new DaoException(104, e, simpleClassName);
}
}
@Override
protected List<Produit> getLongEntitiesById(List<Long> ids) {
try {
return linkLongProduitsToCategories(produitsRepository.getLongProduitsById(ids));
} catch (Exception e) {
throw new DaoException(105, e, simpleClassName);
}
}
@Override
protected List<Produit> getLongEntitiesByName(List<String> names) {
try {
return linkLongProduitsToCategories(produitsRepository.getLongProduitsByName(names));
} catch (Exception e) {
throw new DaoException(106, e, simpleClassName);
}
}
private List<Produit> linkLongProduitsToCategories(List<Produit> produits) {
for (Produit produit : produits) {
Categorie categorie = produit.getCategorie();
if (categorie != null) {
produit.setCategorie(categorie);
produit.setIdCategorie(categorie.getId());
}
}
return produits;
}
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
// restabelece-se (se necessário) a ligação entre um produto e a sua categoria
for (Produit produit : entities) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// os produtos são guardados
try {
return Lists.newArrayList(produitsRepository.save(entities));
} catch (Exception e) {
throw new DaoException(111, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
try {
produitsRepository.delete(getShortEntitiesById(ids));
} catch (Exception e) {
throw new DaoException(113, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesByName(List<String> names) {
try {
produitsRepository.delete(getShortEntitiesByName(names));
} catch (Exception e) {
throw new DaoException(118, e, simpleClassName);
}
}
}
O código é semelhante ao da classe [DaoCategorie]:
- no que diz respeito às versões completas das categorias, os testes revelam que o campo [idCategorie] dos produtos não está preenchido. O método [linkLongProduitsToCategories], nas linhas 96-105, resolve este problema;
- o método [saveEntities], nas linhas 108-121, insere novos produtos ou altera produtos existentes. A camada JPA exige que cada entidade [Produit] esteja ligada a uma entidade [Categorie]. Como não se sabe se o utilizador o fez, fazemo-lo nós próprios nas linhas 110-113. Basta associar a entidade [Produit] a uma entidade [Categorie] cuja chave primária seja igual ao campo [idCategorie] da entidade [Produit]. Nos testes, verifica-se que ocorre um erro se se introduzir null como versão da categoria. Por isso, atribui-se aqui o valor 0, mas pode-se introduzir o que se quiser. Para além da chave primária, nenhum campo da entidade [Categorie] é necessário na camada JPA para inserir/alterar uma entidade [Produit];
6.4.5. A camada de testes
![]() |
![]() |
Os testes acima são idênticos aos da implementação Spring JDBC. Consulte as páginas seguintes, se necessário:
- [JUnitTestCheckArguments]: parágrafo 4.11.1;
- [JUnitTestDao]: parágrafo 4.11.2;
- [JUnitTestPushTheLimits]: parágrafo 4.11.3;
Utilizamos as seguintes configurações de execução:
![]() | ![]() |
![]() | ![]() |
Os resultados obtidos nos diferentes testes são os seguintes:
![]() | ![]() |
![]() |
No [1], o teste [JUnitTestPushTheLimits] com a implementação Spring Data JPA Hibernate e, em [2], com a implementação Spring JDBC. Verifica-se que esta última apresenta melhor desempenho. Chegamos, assim, a uma primeira conclusão: é claramente mais fácil desenvolver uma camada [DAO] com o Spring Data JPA, mas esta apresenta um desempenho inferior ao de uma implementação Spring JDBC.
O teste [JUnitTestProxies] é um teste fictício JUnit. Serve para mostrar o comportamento de cada implementação JPA face aos proxies, ou seja, as versões reduzidas das entidades:
package spring.data.tests;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.data.config.AppConfig;
import spring.data.dao.IDao;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestProxies {
// camada [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
@Before
public void clean() {
// limpa-se a base de dados antes de cada teste
log("Vidage de la base de données", 1);
// esvazia-se a tabela [CATEGORIES] e, em cadeia, a tabela [PRODUITS]
daoCategorie.deleteAllEntities();
}
@Test
public void doNothing() {
System.out.println("doNothing");
}
private List<Categorie> fill(int nbCategories, int nbProduits) {
// preenchem-se as tabelas
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < nbCategories; i++) {
Categorie categorie = new Categorie(null, null, String.format("categorie[%d]", i), null);
categorie.setProduits(new ArrayList<Produit>());
for (int j = 0; j < nbProduits; j++) {
Produit produit = new Produit(null, null, String.format("produit[%d,%d]", i, j), null,
100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
categorie.addProduit(produit);
}
categories.add(categorie);
}
// adiciona-se a categoria — em cadeia, os produtos também serão
// inseridos
daoCategorie.saveEntities(categories);
// resultado
return categories;
}
@Test
public void getShortCategoriesByName1() {
// preenchimento
fill(1, 1);
// teste
log("getShortCategoriesByName1", 1);
Categorie categorie = daoCategorie.getShortEntitiesByName(Lists.newArrayList("categorie[0]")).get(0);
System.out.println(String.format("Catégorie de type : %s", categorie.getEntityType()));
System.out.println("Catégorie :");
try {
System.out.println(categorie.getProduits().size());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getShortProduitsByName1() {
// preenchimento
fill(1, 1);
// teste
log("getShortProduitsByName1", 1);
Produit produit = daoProduit.getShortEntitiesByName(Lists.newArrayList("produit[0,0]")).get(0);
System.out.println(String.format("Produit de type : %s", produit.getEntityType()));
System.out.println("Nom de la catégorie du produit :");
try {
System.out.println(produit.getCategorie().getNom());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getLongCategoriesByName1() {
// preenchimento
fill(1, 1);
// teste
log("getLongCategoriesByName1", 1);
Categorie categorie = daoCategorie.getLongEntitiesByName(Lists.newArrayList("categorie[0]")).get(0);
System.out.println(String.format("Catégorie de type : %s", categorie.getEntityType()));
System.out.println("Catégorie :");
try {
System.out.println(categorie.getProduits().size());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getLongProduitsByName1() {
// preenchimento
fill(1, 1);
// teste
log("getLongProduitsByName1", 1);
Produit produit = daoProduit.getLongEntitiesByName(Lists.newArrayList("produit[0,0]")).get(0);
System.out.println(String.format("Produit de type : %s", produit.getEntityType()));
System.out.println("Nom de la catégorie du produit :");
try {
System.out.println(produit.getCategorie().getNom());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
private 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);
}
}
Os resultados obtidos são os seguintes:
Vidage de la base de données --------------------------------
doNothing
Vidage de la base de données --------------------------------
getShortCategoriesByName1 --------------------------------
Catégorie de type : PROXY
Catégorie :
Exception : org.hibernate.LazyInitializationException, Message : failed to lazily initialize a collection of role: generic.jpa.entities.dbproduitscategories.Categorie.produits, could not initialize proxy - no Session
Vidage de la base de données --------------------------------
getLongCategoriesByName1 --------------------------------
Catégorie de type : POJO
Catégorie :
1
Vidage de la base de données --------------------------------
getShortProduitsByName1 --------------------------------
Produit de type : PROXY
Nom de la catégorie du produit :
Exception : org.hibernate.LazyInitializationException, Message : could not initialize proxy - no Session
Vidage de la base de données --------------------------------
getLongProduitsByName1 --------------------------------
Produit de type : POJO
Nom de la catégorie du produit :
categorie[0]
Vemos aqui que, ao aceder ao campo [Categorie.produits] de uma categoria do tipo PROXY e ao campo [Produit.categorie] de um produto do tipo PROXY, ocorre uma exceção do tipo [org.hibernate.LazyInitializationException] em ambos os casos (linhas 7 e 17).



































