6. Spring Data JPA Hibernate
6.1. Introduction
Vamos retomar o banco de dados [dbproduitscategories] gerenciado pelo projeto [spring-jdbc-04] e implementar as duas interfaces [IDao<Categorie>, IDao<Produit>] definidas nesse projeto. Isso nos permitirá 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 demais 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 possui seu próprio projeto de configuração.
Em seguida, importe o projeto [spring-jpa-generic] [1], localizado na pasta [<exemples>/spring-database-generic/spring-jpa] [2]:
![]() |
Feito isso, reinicialize 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]:
![]() |
Essa configuração executa o teste [JUnitTestDao]. Esse 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 arquivo [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 provedor -->
<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 artefato Maven gerado por este projeto. Os projetos de configuração das outras implementações JPA (Eclipselink e OpenJpa) utilizarão esse mesmo artefato. Isso significa que apenas um desses projetos pode estar ativo a qualquer momento. Portanto, deve-se evitar 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 driver JDBC do SGBD utilizado e as coordenadas do banco de dados a ser usado;
- 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]. Portanto, recomenda-se não incluí-la nas dependências do projeto. Se ela 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);
// conexõ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();
}
// Gerenciador 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: ela importa os beans definidos pela classe de configuração [ConfigJdbc], que foi usada 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 exibir ou não as operações SQL executadas pela implementação do Hibernate;
- linha 27: indica-se ao Hibernate o SGBD conectado. Essa configuração é importante. Ela permite que o Hibernate utilize o dialeto SQL do SGBD MySQL, incluindo sua parte proprietária. Além disso, isso fornece informações sobre os tipos SQL e os objetos de SGBD que ele poderá utilizar. É essa capacidade da implementação JPA de se adaptar a um SGBD específico que lhe confere grande portabilidade entre SGBD;
- linha 28: O Hibernate pode ou não gerar as tabelas do banco de dados de destino a partir das entidades JPA que encontrar. Essa geração só ocorre se as tabelas estiverem ausentes. Se elas já estiverem presentes, nada é feito. Utilizaremos essa capacidade de gerar tabelas quando apresentarmos como foram gerados os scripts SQL para a geração dos diversos bancos de dados utilizados neste documento;
- linha 33: o pacote no qual se encontram as entidades JPA do banco de dados [dbproduitscategories];
- linhas 36-49: a fonte de dados [tomcat-jdbc] vinculada ao banco de dados [dbproduitscategories];
- linhas 52-60: o bean denominado [entityManagerFactory] (deve ter esse nome) é o bean que criará o objeto [EntityManager], responsável por gerenciar o contexto de persistência JPA. Todas as operações JPA passam por ele. O uso de [Spring Data JPA] faz com que nunca venhamos a utilizar esse objeto diretamente. No entanto, precisamos configurá-lo. Ele precisa 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 essas informações;
- linha 59: retorna o singleton [entityManagerFactory];
- linhas 63-68: definem o gerenciador de transações. Ele deve se chamar [transactionManager];
- linha 65: é criado um gerenciador de transações JPA;
- linha 66: ele é vinculado à fonte de dados da linha 37 por meio do bean [entityManagerFactory] (linhas 53 e 57);
Apenas o bean das linhas 23 a 30 depende da implementação JPA utilizada. Os demais beans, por sua vez, dependem dele.
6.3.3. As entidades da camada [JPA]
![]() |
![]() |
O banco de dados de destino é o [dbproduitscategories], com suas duas tabelas [CATEGORIES] e [PRODUITS]. Vimos que ela também possui outras três tabelas, [USERS, ROLES, USERS_ROLES], que serão utilizadas para proteger o serviço web a ser implantado na web. Por enquanto, ignoraremos essas tabelas. Vale lembrar a estrutura das tabelas [CATEGORIES] e [PRODUITS]:
A tabela [PRODUITS] é a seguinte:
![]() |
- [ID]: a chave primária autoincrementada da tabela [2];
- [NOM]: o nome exclusivo do produto [4];
- [PRIX]: o preço do produto;
- [DESCRIPTION]: a descrição do produto;
- [VERSIONING] é o número de versão do produto. Sua versão inicial é 1 [3]. Sempre que o produto for modificado, seu número de versão será incrementado pelo código que opera a tabela;
- [CATEGORIE_ID]: a chave estrangeira na tabela [CATEGORIES] para indicar a categoria à qual o produto pertence;
![]() |
- em [1-3], a chave estrangeira [CATEGORIE_ID] da tabela [PRODUITS]. Ela tem como alvo a coluna [ID] da tabela [CATEGORIES] [4-5];
- quando uma categoria é excluída, todos os produtos a ela vinculados também são excluídos ([6]). É importante observar esse ponto, pois ele é utilizado na construção da camada [DAO], que utiliza a base [dbproduitscategories];
A tabela [CATEGORIES] de categorias é a seguinte:
![]() |
- [ID]: chave primária autoincrementada;
- [VERSIONING]: número da versão da categoria;
- [NOM]: nome exclusivo da categoria;
Descreveremos agora as entidades JPA, [Produit] e [Categorie], que são imagens das 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);
}
Essa interface, implementada pelas duas entidades JPA, serve simplesmente para listar os métodos para ler/gravar 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;
// 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 gerenciada pela camada [JPA]. Também é possível escrever [@Entity(name="MonProduit")], o que atribui o nome [MonProduit] à entidade. Na ausência dessa informação, o nome da entidade é o nome da classe, neste caso, [Produit]. Essa nomenclatura torna-se necessária quando, entre as entidades, há duas classes de pacotes diferentes que possuem 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] do banco de dados;
- linha 23: o nome do filtro jSON a ser aplicado à entidade. Veremos que a propriedade [categorie] da linha 58 nem sempre está disponível. Nesse caso, é preciso excluí-la da representação jSON do objeto. Para isso, precisamos de um filtro. Assim, será em um filtro chamado [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, ela estava 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 adotar a estratégia [AUTO], que deixa a escolha da estratégia de geração da chave primária a critério da implementação JPA. 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 chamada, por padrão, [SEQUENCE], à qual é necessário consultar para obter as chaves primárias.
No final das contas, a estrutura do banco de dados gerenciada por essas duas implementações JPA não é a mesma. Se ela 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 ser associada ao campo [id];
- linha 29: utiliza-se o tipo [Long] em vez de [long] para a chave primária. De fato, as chaves primárias [null] têm um significado específico para JPA. Portanto, é 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 controle de versão. A implementação JPA incrementará esse número de versão sempre que a entidade for modificada. Esse número serve para impedir a atualização simultânea da entidade por dois usuários diferentes: dois usuários, U1 e U2, leem a entidade E com um número de versão igual a V1. U1 modifica E e grava essa modificação no banco de dados: o número de versão passa então para V1+1. U2, por sua vez, modifica E e grava essa modificação no banco de dados: ele receberá uma exceção, pois possui uma versão (V1) diferente daquela no banco de dados (V1+1);
- linha 36: o tipo da entidade. Teremos dois: POJO e PROXY. Por padrão, a instância gerada será um POJO (Plain Old Java Object). Em alguns casos, as instâncias [Produit] recuperadas do banco de dados serão do tipo [PROXY]. Isso ocorrerá 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. Nesse caso, as implementações JPA que serão testadas diferem:
- [Hibernate, OpenJPA]: acessar a categoria de um produto do tipo [PROXY] gera uma exceção. O Hibernate utiliza o termo “proxy” para designar uma instância JPA obtida no modo [LAZY]. É por isso que utilizei esse termo para designar esse tipo de entidade;
- [EclipseLink]: acessar a categoria de um produto do tipo [PROXY] faz com que essa categoria seja pesquisada no banco de dados e não ocorre nenhuma exceção;
Como eu queria ter uma camada de testes independente da implementação JPA utilizada, desejei saber o tipo de cada entidade: POJO ou PROXY. Por isso, adicionei o campo [entityType] às entidades JPA;
- linha 35: a anotação [@Transient] indica que a implementação JPA deve ignorar esse campo. De fato, ele 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: assim como anteriormente, indica-se que a implementação JPA deve ignorar esse campo;
- linha 39: a anotação [@JsonIgnore] indica que o serializador/desserializador jSON de uma instância [Produit] deve ignorar esse 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. Esse seria o caso aqui. Os atributos [unique = true, length = 30, nullable = false] são utilizados apenas quando a implementação JPA precisa gerar a tabela [CATEGORIES] a partir da entidade [Produit]. Eles 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á vinculado à coluna [CATEGORIE_ID]. Voltaremos a seus atributos um pouco 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. Essa anotação deve referir-se a uma entidade JPA. Portanto, a classe da linha 58 deve ser uma entidade JPA;
- linha 56: a anotação [fetch = FetchType.LAZY] determina que, ao recuperar um produto da tabela [PRODUITS], sua categoria (linha 58) não seja recuperada imediatamente (carregamento diferido). Ela é, então, obtida na primeira chamada ao método [getCategorie]. Para isso, durante a execução, a camada JPA complementa o método inicial [getCategorie] (que se limita a retornar o campo categorie) com uma chamada ao SGBD para buscar a categoria — uma técnica chamada “proxying”. As implementações JPA diferem na implementação dessa característica, conforme mencionado anteriormente. Esse atributo não é obrigatório. A implementação JPA utilizada tem o direito de ignorá-lo. É 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 durante a inserção ou atualização do produto. Ela recebe o valor de [categorie.getId()], sendo que [categorie] é o campo da linha 58. A especificação JPA determina que essa coluna de junção não possa ser atualizada por nenhum outro meio. Além disso, ela 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 modificada 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 a igualdade entre suas chaves primárias [id];
- linhas 108-115: para tornar nossa camada de testes portátil, vamos tratar de maneira uniforme as entidades [PROXY] das três implementações JPA e [Hibernate, EclipseLink, OpenJpa]. Para um tipo [Produit] do tipo [PROXY], proibiremos 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 observar que as anotações e seus atributos são utilizados em dois casos bem distintos:
- para criar as tabelas do banco de dados;
- para utilizá-las. Nesse caso, a implementação JPA espera encontrar as tabelas exatamente como ela mesma as teria gerado. Portanto, não é possível associar à entidade [Produit] anterior qualquer tabela [PRODUITS]. É necessário que esta tenha, no mínimo (pode ter outras), as características da tabela [PRODUITS] que ela mesma teria gerado. Ao trabalhar com a JPA, o ideal é partir de uma base vazia, na qual se permite que a JPA gere as tabelas. Abordaremos essa geração um pouco mais adiante. O script SQL fornecido para o SGBD e o 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 isso é feito, atributos de geração como [unique = true, length = 30, nullable = false] não são mais 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]:
![]() |
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;
// construtores
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);
// definindo 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 filtro deverá ser configurado antes de qualquer solicitação 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 escolhido é o modo [IDENTITY]; portanto, o modo [AUTO_INCREMENT] para MySQL;
- linhas 34-36: o campo [version] está vinculado à coluna de controle de versão [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á vinculado à coluna [NOM] da tabela [CATEGORIES]. Atribuem-se a ele os atributos JPA e [unique = true, length = 30, nullable=false] para que, na 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 encontramos 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 em uma @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, ao recuperar uma categoria da tabela [CATEGORIES], seus produtos não sejam recuperados imediatamente. Eles são recuperados, então, na primeira chamada ao método [getProduits]. Para isso, durante a execução, a camada JPA complementa o método [getProduits] inicial (que se limita a retornar o campo produits) com uma chamada ao SGBD para buscar os produtos da categoria. Esse 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 desejamos 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 padronizar o gerenciamento de proxies entre diferentes implementações do JPA, decidimos que não seria 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 arquivo [persistence.xml]
![]() |
As aplicações JPA devem definir certas propriedades do provedor JPA utilizado, bem como as entidades JPA a serem utilizadas, em um arquivo [META-INF/persistence.xml] presente no Classpath da aplicação. No exemplo acima, ele foi colocado na pasta [src/main/resources], que de fato faz parte do Classpath de um projeto Eclipse. Ao utilizar o JPA em conjunto com o Spring, algumas informações que deveriam estar no arquivo [persistence.xml] são colocadas em outro local, nas classes de configuração do Spring. Em uma aplicação Spring JPA, é o Spring que controla o JPA. Com o Spring JPA Hibernate, o arquivo [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 arquivo [persistence.xml] deve ter uma tag raiz <persistence>. Os atributos da tag da linha 2 não serão utilizados nesta aplicação;
- um arquivo de persistência pode definir uma ou mais unidades de persistência com a tag <persistence-unit> (linha 4). Uma unidade de persistência gerencia o acesso a um banco de dados específico. Se a aplicação gerenciar dois bancos de dados simultaneamente, ela terá duas unidades de persistência;
- linha 4: uma unidade de persistência tem o nome [attribut name], suporta um tipo de transação [attribut transaction-type], possui propriedades e define as entidades associadas às tabelas do banco de dados gerenciado pela unidade de persistência. Aqui, como os acessos ao banco de dados serão gerenciados por [Spring JPA Hibernate], essas duas últimas informações podem ser colocadas em outro lugar. Existem dois tipos de transação:
- [RESOURCE_LOCAL]: as transações são gerenciadas pela própria aplicação. É o caso aqui, em que o Spring será responsável pelo gerenciamento das transações;
- [JTA] (Transação Java API): é o contêiner EJB (Enterprise Java Bean) que executa a aplicação e que gerenciará 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 desse arquivo [persistence.xml] depende da implementação JPA utilizada.
6.4. O projeto [spring-jpa-generic]
Vamos relembrar o que queremos fazer. Queremos implementar a seguinte arquitetura:
![]() |
na qual a camada [DAO] implementaria a interface [IDao<Produit>, IDao<Categorie>] estudada no capítulo 4. Trata-se de comparar duas implementações dessa 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 arquivo [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 possui apenas uma única dependência, que é do projeto que configura a camada [JPA] do aplicativo e que acabamos de analisar. Trata-se de um aplicativo genérico:
- alteramos o SGBD modificando o projeto de configuração da camada [JDBC];
- para alterar a implementação JPA, basta alterar 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. Isso os torna componentes do Spring que podem ser injetados em outros componentes do Spring;
- linha 12: a anotação [@ComponentScan] indica que o pacote [spring.data.dao] deve ser pesquisado em busca de componentes Spring. Serão encontrados os componentes [DaoCategorie] e [DaoProduit];
- linha 13: os beans da classe de configuração [ConfigJpa] são importados. Nela, será encontrado o bean da implementação JPA utilizada (Hibernate, Eclipselink, OpenJpa), a fonte de dados a ser utilizada, o EntityManager que irá gerenciar as operações JPA, o gerenciador de transações;
6.4.3. A camada [Spring Data]
![]() |
![]() |
6.4.3.1. A interface [CategoriesRepository]
A interface [CategoriesRepository] gerencia 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 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. Vale lembrar que:
- o primeiro tipo de parâmetro da interface é a entidade JPA, gerenciada para acessos CRUD (findOne, findAll, salvar, excluir, 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). Essa consulta busca as entidades JPA. Nessa consulta:
- as tabelas são substituídas por suas entidades JPA associadas;
- as colunas são substituídas por campos das entidades JPA utilizadas na consulta;
Tomemos como exemplo as linhas 31-32: o método da linha 32 retorna todas as categorias do banco de dados em sua versão abreviada. Ele é implementado pela consulta JPQL (Java Persistence Query Language) da linha 31, que se assemelha bastante à sua contraparte SQL. Para aprofundar o conhecimento sobre JPQL, pode-se consultar [ref2] (ver parágrafo 1.2).
Os métodos da interface [CategoriesRepository] são os seguintes:
- linhas 13-14: o método [getLongCategorieById] retorna a versão completa de uma categoria referenciada por sua chave primária [id], ou seja, a categoria com seus produtos. Lembramos que, na entidade [Categorie], o campo [produits] possuía 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] retorna a versão longa de uma categoria referenciada pelo nome [nom];
- linhas 19-20: o método [getShortCategoriesByName] retorna as versões curtas das categorias referenciadas por seus nomes. O campo [produits] dessas categorias não é null. Ele contém a referência a um proxy (uma classe criada pela implementação JPA) cuja função é retornar os produtos da categoria quando for chamado. Sua chamada fora do contexto de persistência JPA provoca uma exceção (Hibernate e OpenJpa, mas não EclipseLink). Por esse motivo, não utilizaremos o campo [produits] da versão resumida de uma categoria;
- linhas 22-23: o método [getShortCategoriesById] retorna as versões curtas das categorias referenciadas por suas chaves primárias [id];
- linhas 25-26: o método [getLongCategoriesById] retorna as versões longas das categorias referenciadas por suas chaves primárias [id];
- linhas [28-29]: o método [getLongCategoriesByName] retorna as versões longas das categorias referenciadas por seus nomes;
- linhas 31-32: o método [getAllShortCategories] retorna as versões curtas de todas as categorias;
- linhas 34-35: o método [getAllLongCategories] retorna as versões completas de todas as categorias;
Observação: nem todas as implementações JPA aceitam a mesma sintaxe JPQL. Assim, a seguinte sintaxe é aceita 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] mencionado acima.
6.4.3.2. A interface [ProduitsRepository]
A interface [ProduitsRepository] gerencia 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 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] retorna a versão completa de um produto identificado por sua chave primária [id], ou seja, incluindo sua categoria. Lembramos que, na entidade [Produit], o campo [categorie] possuía 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] retorna a versão completa de um produto identificado por seu nome;
- linhas 21-22: o método [getShortProduitsById] retorna a versão curta dos produtos identificados por sua chave primária [id]. Nessa versão curta, o campo [categorie] não tem o valor null. Ele contém a referência de um proxy gerado pela implementação JPA, que, se for chamado, irá buscar a categoria do produto. Essa chamada só pode ser feita no contexto de persistência JPA. Fê-la em outro lugar gera uma exceção (Hibernate e OpenJpa, mas não EclipseLink). Portanto, na camada [DAO] ou em qualquer outro lugar, não utilizaremos o campo [categorie] de um produto em sua versão curta. Na versão curta do produto, o campo [idCategorie] é inicializado. Seu valor é a chave primária da categoria à qual o produto pertence. Isso permite, posteriormente, solicitar essa categoria à camada [DAO] por meio do método [DaoCategorie. getShortCategoriesById(idCategorie)];
- linhas 24-25: o método [getShortProduitsByName] retorna a versão resumida dos produtos identificados por seus nomes;
- linhas 27-28: o método [getLongProduitsById] retorna a versão longa dos produtos identificados por suas chaves primárias;
- linhas 30-31: o método [getLongProduitsByName] retorna a versão completa dos produtos identificados por seus nomes;
- linhas 33-34: o método [getAllLongProduits] retorna a versão completa de todos os produtos;
- linhas 36-37: o método [getAllShortProduits] retorna a versão curta de todos os produtos;
Essas interfaces serão implementadas por classes geradas pela implementação JPA no momento da execução do projeto. Essas classes são chamadas de classes [proxy]. Por padrão, os métodos da interface [CrudRepository] são executados em uma transação. O fato de as interfaces [ProduitsRepository, CategoriesRepository] estenderem a classe [CrudRepository] faz com que sejam componentes Spring. Nesse sentido, elas podem ser injetadas em outros componentes Spring.
6.4.4. A camada [DAO]
![]() |
![]() |
6.4.4.1. A interface [IDao<T>]
A interface [IDao<T>] é a mesma já analisada na implementação da camada [DAO] feita 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 resumida
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);
// exclusão de todas as entidades
public void deleteAllEntities();
// remoçã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 gerencia os acessos à tabela [PRODUITS];
- a classe [DaoCategorie], que implementa a interface [IDao<Categorie>] e gerencia os acessos à tabela [CATEGORIES];
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 em uma transação. Aqui, aproveita-se o fato de que as interfaces [CrudRepository] do Spring Data são executadas, por padrão, em uma transação.
6.4.4.3. A classe [DaoCategorie]
![]() |
A classe [DaoCategorie] implementa a interface [IDao<Categorie>] da seguinte maneira:
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] em um componente Spring;
- linha 18: a classe [DaoCategorie] estende a classe [AbstractDao<Categorie>], o que faz com que ela implemente a interface [IDao<Categorie>];
- linhas 20-24: injeção de referências nas duas interfaces [CrudRepository] e [Spring Data]. Essa 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 para sua versão resumida indicam isso 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) {
// identificamos os produtos que serão 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);
}
// aproveitamos para restabelecer (se necessário) a relação produto --> categoria
produit.setCategorie(categorie);
}
}
}
// persistimos 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 serem inseridas ([id==null]) quanto a serem modificadas ([id!=null]);
- linha 20: as categorias são persistidas com o método [categoriesRepository.save(entities)]. Nos testes, constata-se que o campo [idCategorie] dos produtos persistidos (id==null) não está preenchido. Para resolver esse problema, registramos nas linhas 4 a 17 os produtos que serão inseridos e, uma vez persistidos, preenchemos seu campo [idCategorie] (linhas 25 a 27);
- linhas 5 a 17: percorremos a lista de categorias;
- linhas 8 a 16: percorre-se, para cada categoria, sua lista de produtos. Aqui há uma dificuldade. O método [saveEntities] é utilizado tanto para persistir quanto para modificar uma categoria. Neste último caso, a categoria pode ter sido obtida em sua versão abreviada, contendo, portanto, a referência a um método proxy no campo [produits]. Usá-lo com o Hibernate provoca, então, uma exceção, pois a categoria em questão não está mais no contexto de persistência JPA, que foi fechado com o término da transação do método que retornou as versões curtas das categorias. Utiliza-se, então, o campo [EntityType] da entidade [Categorie], na linha 8, para verificar se é possível ou não acessar a lista de produtos da categoria;
- linha 14: associamos o produto à sua categoria. Normalmente, isso já deveria estar feito. Mas não sabemos como esse produto foi criado e se ele foi associado à sua categoria. Portanto, para evitar qualquer problema (para gerenciar a entidade [Produit], a JPA precisa que esta faça referência à entidade [Categorie] à qual está vinculada), fazemos essa vinculação nós mesmos.
Ao comparar esse código com o da classe [DaoProduit] da implementação Spring JDBC (ver parágrafo 4.9) , percebe-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 maneira:
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 sua categoria
for (Produit produit : entities) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// os produtos são salvos
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]:
- para as 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 a 105, corrige esse problema;
- o método [saveEntities], nas linhas 108 a 121, insere novos produtos ou modifica produtos existentes. A camada JPA exige que cada entidade [Produit] esteja vinculada a uma entidade [Categorie]. Como não sabemos se o usuário fez isso, nós mesmos realizamos essa vinculação nas linhas 110 a 113. Basta vincular a entidade [Produit] a uma entidade [Categorie] cuja chave primária seja igual ao campo [idCategorie] da entidade [Produit]. Nos testes, percebemos que ocorre um erro se inserirmos null como versão da categoria. Por isso, atribuímos aqui o valor 0, mas é possível inserir o que quisermos. Além da chave primária, nenhum campo da entidade [Categorie] é necessário na camada JPA para inserir/modificar 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 a seguir, 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 diversos 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. Percebe-se que esta última apresenta melhor desempenho. Chegamos, portanto, a uma primeira conclusão: é claramente mais fácil desenvolver uma camada [DAO] com Spring Data JPA, mas ela apresenta desempenho inferior ao de uma implementação Spring JDBC.
O teste [JUnitTestProxies] é um teste fictício JUnit. Ele serve para mostrar o comportamento de cada implementação JPA em relação aos proxies, ou seja, as versões resumidas 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 o banco 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) {
// preenche-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);
}
// adição da categoria — em sequência, 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 acessar o campo [Categorie.produits] de uma categoria do tipo PROXY e o 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).



































