6. Spring Data JPA Hibernate
6.1. Introdução
Iremos utilizar a base de dados [dbproduitscategories] gerida pelo projeto [spring-jdbc-04] e implementar as duas interfaces [IDao<Category>, IDao<Product>] definidas nesse projeto. Isto permitir-nos-á fazer várias coisas:
- comparar o código 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] discutido na Secção 3.3;
Passaremos agora às outras camadas.
6.2. Configurar o ambiente de trabalho
Utilizando o STS, importe o projeto [mysql-config-jpa-hibernate] [1] localizado na pasta [<examples>/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] localizado na pasta [<examples>/spring-database-generic/spring-jpa] [2]:
![]() |
Depois de fazer isso, atualize o ambiente do Maven (Alt-F5) para todos os projetos no [Package Explorer]:
![]() |
Em seguida, para verificar o ambiente de trabalho, execute a configuração de compilação denominada [spring-jpa-generic-JUnitTestDao-hibernate]:
![]() |
Esta configuração executa o teste [JUnitTestDao]. Este teste deve ser aprovado:
![]() |
6.3. O projeto de configuração da camada JPA
![]() |
O objetivo deste projeto é configurar a camada JPA da arquitetura apresentada abaixo:
![]() |
6.3.1. Configuração do Maven
O projeto é um projeto Maven 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>
<!-- dépendances variables ********************************************** -->
<!-- JPA provider -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
</dependency>
<!-- dépendances constantes ********************************************** -->
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<!-- Spring Context -->
<!-- configuration JDBC inherited -->
<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 para as outras implementações JPA (Eclipselink e OpenJpa) utilizarão este mesmo artefacto. Isto significa que apenas um destes projetos pode estar ativo em qualquer momento. Deve, portanto, evitar que todos estejam presentes no [Package Explorer]. Apenas um é necessário;
- linhas 10–14: o projeto Maven pai que especifica as versões da maioria das dependências exigidas pelo 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 depende do projeto de configuração da camada JDBC, que define, entre outras coisas, o controlador JDBC para o SGBD que está a ser utilizado e os detalhes da ligação à base de dados;
- linhas 35–39: o projeto de configuração da camada JDBC inclui a biblioteca [Spring JDBC], que é substituída aqui pela biblioteca [Spring Data JPA]. Por isso, especificamos que não a inclua nas dependências do projeto. No entanto, se permanecer, isso não causa 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 {
// the provider JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
hibernateJpaVendorAdapter.setGenerateDdl(true);
return hibernateJpaVendorAdapter;
}
// JPA entity packages
public final static String[] ENTITIES_PACKAGES = { "generic.jpa.entities.dbproduitscategories" };
// data source
@Bean
public DataSource dataSource() {
// data source TomcatJdbc
DataSource dataSource = new DataSource();
// configuration access JDBC
dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
// initially open connections
dataSource.setInitialSize(5);
// result
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();
}
// Transaction manager
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- linha 18: a classe é uma classe de configuração Spring;
- linha 19: importa os beans definidos pela classe de configuração [ConfigJdbc] utilizada para configurar o projeto Spring [mysql-config-jdbc]. Estes são os filtros JSON;
- linhas 23–30: definem a implementação JPA utilizada, neste caso a implementação Hibernate (linha 25);
- linha 26: pode optar por exibir ou não as operações SQL executadas pela implementação do Hibernate;
- linha 27: especifica o SGBD conectado ao Hibernate. Esta configuração é importante. Permite que o Hibernate utilize o dialeto SQL do SGBD MySQL, incluindo as suas funcionalidades proprietárias. Além disso, informa-o sobre os tipos SQL e os objetos do SGBD que poderá utilizar. É esta capacidade da implementação JPA de se adaptar a um SGBD específico que lhe confere elevada portabilidade entre SGBDs;
- linha 28: o Hibernate pode ou não gerar as tabelas para a base de dados de destino a partir das entidades JPA que encontrar. Esta geração só ocorre se as tabelas estiverem ausentes. Se já existirem, nada é feito. Iremos utilizar esta capacidade de gerar tabelas quando demonstrarmos como foram criados os scripts de geração de SQL para as várias bases de dados utilizadas neste documento;
- linha 33: o pacote que contém as entidades JPA para a base de dados [dbproduitscategories];
- Linhas 36–49: A fonte de dados [tomcat-jdbc] ligada à base de dados [dbproduitscategories];
- linhas 52–60: o bean denominado [entityManagerFactory] (deve ser denominado assim) é o bean que irá criar o objeto [EntityManager], que gere o contexto de persistência JPA. Todas as operações JPA passam por ele. Como estamos a utilizar [Spring Data JPA], nunca iremos utilizar este objeto nós próprios. No entanto, precisamos 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 para esta fonte (linha 56);
- linha 58: inicializa o EntityManager com esta informação;
- linha 59: devolve o singleton [entityManagerFactory];
- linhas 63–68: definem o gestor de transações. Deve ser denominado [transactionManager];
- linha 65: é criado um gestor de transações JPA;
- linha 66: é ligado à fonte de dados da linha 37 através do bean [entityManagerFactory] (linhas 53 e 57);
Apenas o bean nas linhas 23–30 depende da implementação JPA utilizada. Os outros beans dependem, então, deste.
6.3.3. Entidades na camada [JPA]
![]() |
![]() |
A base de dados de destino é a base de dados [dbproduitscategories], com as suas duas tabelas [CATEGORIES] e [PRODUITS]. Vimos que também possui outras três tabelas [USERS, ROLES, USERS_ROLES] que serão utilizadas para proteger o serviço web a ser implementado na web. Por enquanto, vamos ignorar estas tabelas. A título de recordatório, eis a estrutura das tabelas [CATEGORIES] e [PRODUCTS]:
A tabela [PRODUCTS] é a seguinte:
![]() |
- [ID]: a chave primária autoincremental da tabela [2];
- [NAME]: o nome único do produto [4];
- [PRICE]: 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 é modificado, o seu número de versão é incrementado pelo código que opera a tabela;
- [CATEGORY_ID]: a chave estrangeira na tabela [CATEGORIES] para identificar a categoria à qual o produto pertence;
![]() |
- em [1-3], a chave estrangeira [CATEGORIE_ID] da tabela [PRODUITS]. Esta faz referência à coluna [ID] da tabela [CATEGORIES] [4-5];
- quando uma categoria é eliminada, todos os produtos a ela associados são também eliminados [6]. É importante ter isto em conta, pois é utilizado na construção da camada [DAO] que utiliza a base de dados [dbproduitscategories];
A tabela [CATEGORIES] é a seguinte:
![]() |
- [ID]: chave primária autoincremental;
- [VERSIONING]: número da versão da categoria;
- [NAME]: nome único da categoria;
Vamos agora descrever as entidades JPA [Product] e [Category], que correspondem às tabelas [PRODUCTS] e [CATEGORIES].
![]() |
6.3.3.1. A interface [AbstractCoreEntity]
A interface [AbstractCoreEntity] é implementada pelas entidades JPA [Category] e [Product]:
package generic.jpa.entities.dbproduitscategories;
public interface AbstractCoreEntity {
// getters and setters for [id], [version], [entityType] fields
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, simplesmente lista os métodos para ler e escrever os campos [id], [version] e [entityType] dessas entidades. O papel do campo [entityType] será explicado mais adiante;
6.3.3.2. A entidade JPA [Product]
A classe [Product] é a entidade JPA associada a uma linha na tabela [PRODUCTS]:
![]() |
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 {
// properties
@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();
// properties
@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;
// the category
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID)
private Categorie categorie;
// manufacturers
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;
}
// signature
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);
}
// ------------------------------------------------------------
// redefine [equals] and [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 and setters
...
public void setCategorie(Categorie categorie) {
// entity type
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 [Product] uma entidade gerida pela camada [JPA]. Também é possível escrever [@Entity(name="MyProduct")], o que atribui à entidade o nome [MyProduct]. Na ausência desta informação, o nome da entidade é o nome da classe, neste caso [Product]. Esta convenção de nomenclatura torna-se necessária quando existem duas classes de pacotes diferentes entre as entidades que partilham o mesmo nome;
- linha 22: a anotação [@Table(name = "PRODUCTS")] indica que a classe [Product] é a representação objeto de uma linha na tabela [PRODUCTS] na base de dados;
- linha 23: o nome do filtro JSON a aplicar à entidade. Veremos que a propriedade [categorie] na linha 58 nem sempre está disponível. Por isso, deve ser excluída da representação JSON do objeto. Para tal, precisamos de um filtro. Utilizaremos, portanto, um filtro denominado [jsonFilterCategorie] para especificar se queremos ou não a propriedade [categorie];
- linha 26: a anotação [@Id] torna o campo anotado o campo associado à chave primária da tabela na linha 19;
- linha 27: a anotação [@GeneratedValue(strategy = GenerationType.IDENTITY)] define o modo de geração automática para a chave primária na tabela [PRODUITS]. O atributo [strategy] determina isso. Existem diferentes modos:

A estratégia [IDENTITY] não está disponível para todos os SGBDs. Entre os seis SGBDs testados, estava disponível para [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 garantir a portabilidade entre implementações JPA, a estratégia [AUTO] não deve ser utilizada, uma vez que deixa a escolha da estratégia de geração da chave primária a cargo da implementação JPA. Assim, com o MySQL 5 e a estratégia [AUTO]:
- O Hibernate escolhe a estratégia [IDENTITY] com o modo [AUTO_INCREMENT] para a chave primária;
- O EclipseLink escolhe a estratégia [TABLE], que cria por predefinição uma tabela chamada [SEQUENCE] que deve ser consultada para recuperar as chaves primárias.
Em última análise, 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 na tabela [PRODUCTS] a ser associada ao campo [id];
- linha 29: o tipo [Long] é utilizado em vez de [long] para a chave primária. Isto deve-se ao facto de as chaves primárias [null] terem um significado específico para o JPA. Por conseguinte, é 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 versionamento. A implementação do JPA incrementará este número de versão cada vez que a entidade for modificada. Este número é utilizado para impedir atualizações simultâneas 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 modifica E e persiste esta alteração na base de dados: o número de versão passa então a V1+1. U2 modifica E por sua vez e tenta persistir esta alteração na base de dados: receberá uma exceção porque a sua versão (V1) difere da que se encontra na base de dados (V1+1);
- Linha 36: o tipo de entidade. Haverá dois: POJO e PROXY. Por predefinição, a instância de Product será um POJO (Plain Old Java Object). Em alguns casos, as instâncias [Product] recuperadas da base de dados serão do tipo [PROXY]. Isto acontecerá quando a propriedade [Category] na linha 58 não tiver sido inicializada com uma categoria devido ao atributo [fetch = FetchType.LAZY] na linha 56. Neste caso, as implementações JPA a testar diferem:
- [Hibernate, OpenJPA]: aceder à categoria de um produto do tipo [PROXY] lança uma exceção. O Hibernate utiliza o termo «proxy» para se referir a uma instância JPA obtida no modo [LAZY]. É por isso que utilizei este termo para me referir a este tipo de entidade;
- [EclipseLink]: aceder à categoria de um produto do tipo [PROXY] desencadeia uma pesquisa por essa categoria na base de dados, e não é lançada nenhuma exceção;
Como eu queria uma camada de teste independente da implementação JPA utilizada, precisava de saber o tipo de cada entidade: POJO ou PROXY. É 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. De facto, ele não existe nas tabelas do SGBD;
- linha 40: a classe [Product] lança uma [ProxyException] que requer o nome da classe;
- linha 38: tal como anteriormente, indicamos que a implementação JPA deve ignorar este campo;
- linha 39: a anotação [@JsonIgnore] indica que o serializador/deserializador JSON para uma instância [Product] deve ignorar este campo;
- linha 43: a anotação [@Column] associa o campo [name] à coluna [NAME] na tabela [PRODUCTS]. 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. Este seria o caso aqui. Os atributos [unique = true, length = 30, nullable = false] só são utilizados quando a implementação JPA gera a tabela [CATEGORIES] a partir da entidade [Product]. Serão traduzidos para os atributos SQL [UNIQUE, VARCHAR(30), NOT NULL], que garantem que a coluna [NAME] terá, no máximo, 30 caracteres, será única na tabela e não poderá ter o valor NULL;
- linhas 46–47: o campo [idCategorie] está ligado à coluna [CATEGORIE_ID]. Voltaremos a estes atributos um pouco mais tarde;
- linhas 49–50: o campo [price] está associado à coluna [PRICE];
- 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 referenciada pela anotação na linha 57 [@JoinColumn(name = "CATEGORIE_ID")] é uma chave estrangeira da tabela [PRODUITS] da entidade [Product] para a tabela [CATEGORIES] associada à entidade na linha 58. Esta anotação deve ser aplicada a uma entidade JPA. Portanto, a classe na linha 58 deve ser uma entidade JPA;
- Linha 56: A anotação [fetch = FetchType.LAZY] especifica que, quando um produto é recuperado da tabela [PRODUCTS], a sua categoria (linha 58) não é recuperada imediatamente (carregamento diferido). É então recuperada durante a primeira chamada ao método [getCategory]. Para conseguir isto, em tempo de execução, a camada JPA melhora o método inicial [getCategorie] (que simplesmente devolve o campo da categoria) fazendo uma chamada ao SGBD para ir buscar a categoria — uma técnica conhecida como «proxying». As implementações JPA diferem na forma como lidam com esta funcionalidade, tal como mencionado anteriormente. Este atributo não é obrigatório. A implementação JPA utilizada pode ignorá-lo. É porque a propriedade [categorie] pode ou não estar presente que introduzimos o filtro JSON na linha 23. A coluna de junção [CATEGORIE_ID] na tabela [PRODUITS] é atualizada automaticamente quando um produto é inserido ou atualizado. Recebe o valor de [categorie.getId()], onde [categorie] é o campo na linha 58. A especificação JPA exige que esta coluna de junção não possa ser atualizada por qualquer outro meio. Por isso, impõe os atributos [insertable = false, updatable = false] na linha 46, que garantem que a coluna [CATEGORIE_ID] (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 entidades [Product] é definida como igualdade entre as suas chaves primárias [id];
- linhas 108-115: para tornar a nossa camada de teste portátil, iremos gerir uniformemente as entidades [PROXY] nas três implementações JPA [Hibernate, EclipseLink, OpenJpa]. Para uma entidade [Product] do tipo [PROXY], iremos impedir que o valor do campo [category] seja alterado. 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 discussão sobre esta entidade, é importante referir que as anotações e os seus atributos são utilizados em dois casos distintos:
- para criar tabelas de base de dados;
- para as consultar. Neste caso, a implementação do JPA espera encontrar as tabelas exatamente como as teria gerado ela própria. Portanto, não podemos associar qualquer tabela [PRODUCTS] à entidade [Product] anterior. Ela deve ter, no mínimo (pode ter outras), as características da tabela [PRODUCTS] que o JPA teria gerado. Ao trabalhar com o JPA, a abordagem ideal é começar com uma base de dados vazia na qual o JPA gera as tabelas. Discutiremos 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 [Product] são utilizados para gerar a tabela [PRODUCTS]. Uma vez feito isto, os atributos de geração, tais como [unique = true, length = 30, nullable = false], deixam de ser utilizados ao consultar as tabelas.
6.3.3.3. A entidade [Category] do JPA
A classe [Category] é uma entidade JPA associada a uma linha na 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();
// properties
@Column(name = ConfigJdbc.TAB_CATEGORIES_NOM, unique = true, length = 30, nullable = false)
private String nom;
// related products
@OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
private List<Produit> produits;
// manufacturers
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;
}
// signature
public String toString() {
return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
}
// methods
public void addProduit(Produit produit) {
// entity type
if (entityType == EntityType.PROXY) {
throw new ProxyException(1004, new RuntimeException(
"On ne peut ajouter de produits à une catégorie de type [PROXY]"), simpleClassName);
}
// add a product
if (produits == null) {
produits = new ArrayList<Produit>();
}
if (produit != null) {
// we add the product
produits.add(produit);
// set your category
produit.setCategorie(this);
produit.setIdCategorie(this.id);
}
}
// ------------------------------------------------------------
// redefine [equals] and [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 and setters
...
}
- linha 24: a classe é uma entidade JPA;
- linha 25: associada à tabela [CATEGORIES];
- linha 26: a representação JSON da entidade [Category] é controlada pelo filtro denominado [jsonFilterCategory]. Este deve ser configurado antes de qualquer pedido de uma representação JSON da entidade. O filtro [jsonFilterCategory] será utilizado para determinar se se deve ou não incluir o campo [products] da linha 40 na representação JSON da entidade [Category];
- linhas 29–32: o campo [id] está associado à chave primária [ID] da tabela [CATEGORIES]. O modo de geração selecionado é [IDENTITY], que corresponde a [AUTO_INCREMENT] para MySQL;
- linhas 34–36: o campo [version] está ligado à coluna [VERSIONING] na tabela [CATEGORIES];
- linhas 38-39: o tipo da entidade [Categorie];
- linhas 41–43: o nome abreviado da classe [Categorie];
- Linhas 46–47: O campo [name] está associado à coluna [NAME] na tabela [CATEGORIES]. Atribuímos-lhe os atributos JPA [unique = true, length = 30, nullable = false] para que, quando a tabela [CATEGORIES] for gerada, a coluna [NAME] tenha os atributos SQL [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 [Product]. O atributo [mappedBy = "category"] especifica o campo na entidade [Product] anotado pela relação inversa [@ManyToOne]. O atributo [cascade = { CascadeType.ALL }] especifica que as operações (persist, merge, remove) realizadas numa @Entity [Category] devem propagar-se para os [products] na linha 51. Cascatas parciais podem ser especificadas utilizando as constantes [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE];
- linha 50: o atributo [fetch = FetchType.LAZY] especifica que, quando uma categoria é recuperada da tabela [CATEGORIES], os seus produtos não são recuperados imediatamente. São recuperados durante a primeira chamada ao método [getProduits]. Para conseguir isto, em tempo de execução, a camada JPA melhora o método inicial [getProduits] (que simplesmente devolve o campo products) fazendo uma chamada ao DBMS para ir buscar os produtos para a categoria. Este atributo é obrigatório. A implementação JPA não pode ignorá-lo. Como a propriedade [products] pode ou não estar inicializada, introduzimos o filtro JSON na linha 26, que nos permite especificar se queremos ou não esta propriedade e o tipo de entidade na linha 39;
- linhas 71–88: o método [addProduct] permite adicionar um produto à categoria;
- linhas 73–76: Para padronizar o tratamento de proxies em diferentes implementações JPA, decidimos que os produtos não podem ser adicionados a uma entidade [Category] do tipo PROXY;
- linhas 92–112: duas entidades [Category] sã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] localizado no classpath da aplicação. Acima, foi colocado na pasta [src/main/resources], que faz efetivamente parte do classpath de um projeto Eclipse. Ao utilizar o JPA em conjunto com o Spring, determinadas informações que deveriam constar no ficheiro [persistence.xml] são colocadas noutros locais nas classes de configuração do Spring. Numa aplicação Spring JPA, o Spring 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 tag raiz <persistence>. Os atributos da tag na linha 2 não serão utilizados nesta aplicação;
- um ficheiro de persistência pode definir uma ou mais unidades de persistência utilizando a tag <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 simultaneamente, terá duas unidades de persistência;
- Linha 4: Uma unidade de persistência tem um nome [atributo name], suporta um tipo de transação [atributo transaction-type], possui propriedades e define as entidades associadas às tabelas da base de dados geridas pela unidade de persistência. Aqui, uma vez que o acesso à base de dados será gerido pelo [Spring JPA Hibernate], estas duas últimas informações podem ser colocadas noutro local. Existem dois tipos de transações:
- [RESOURCE_LOCAL]: as transações são geridas pela própria aplicação. É o caso aqui, onde o Spring irá gerir as transações;
- [JTA] (Java Transaction API): o contentor EJB (Enterprise Java Bean) que executa a aplicação gere automaticamente as transações com base nas anotações Java presentes no código. Não estamos a utilizar esta configuração neste caso;
Veremos mais tarde que o conteúdo deste ficheiro [persistence.xml] depende da implementação JPA utilizada.
6.4. O projeto [spring-jpa-generic]
Vamos recapitular o que pretendemos fazer. Pretendemos implementar a seguinte arquitetura:
![]() |
na qual a camada [DAO] implementaria a interface [IDao<Product>, IDao<Category>] estudada no Capítulo 4. O objetivo é comparar duas implementações desta interface:
- uma construída com Spring JDBC;
- a outra construída com Spring JPA;
Na arquitetura acima:
- a camada [JDBC] é implementada pelo projeto [mysql-config-jdbc] discutido na Secção 3.3;
- a camada [JPA] é implementada pelo projeto [mysql-config-jpa-hibernate] discutido na Secção 6.3;
O projeto [spring-jpa-generic] é responsável pela 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>
<!-- configuration JPA of 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 dependência, que é do projeto que configura a camada [JPA] da aplicação, que acabámos de examinar. Esta é 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 do 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] é utilizada para designar 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 verificado em busca de componentes Spring. Os componentes [DaoCategory] e [DaoProduct] serão encontrados;
- linha 13: os beans da classe de configuração [ConfigJpa] são importados. Estes incluem o bean para a implementação JPA que está a ser utilizada (Hibernate, Eclipselink, OpenJpa), a fonte de dados a ser utilizada, o EntityManager que irá gerir as operações JPA e o gestor de transações;
6.4.3. A camada [Spring Data]
![]() |
![]() |
6.4.3.1. A interface [CategoriesRepository]
A interface [CategoriesRepository] gere o acesso à 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> {
// categorie avec ses produits
@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 na Secção 5.1.3. Recorde-se que:
- o primeiro tipo de parâmetro da interface é a entidade JPA gerida para operações CRUD (findOne, findAll, save, delete, deleteAll),
- o segundo tipo de parâmetro da interface é a chave primária da entidade JPA, neste caso um inteiro [Long];
Os métodos da interface são implementados através de consultas JPQL (Java Persistence Query Language). Estas consultas têm como alvo entidades JPA. Numa consulta deste tipo:
- as tabelas são substituídas pelas entidades JPA associadas;
- as colunas são substituídas pelos campos das entidades JPA utilizadas na consulta;
Tomemos o exemplo das linhas 31–32: o método na linha 32 recupera todas as categorias da base de dados na sua forma abreviada. É implementado pela consulta JPQL (Java Persistence Query Language) na linha 31, que se assemelha muito à sua contraparte SQL. Para uma compreensão mais aprofundada do JPQL, consulte [ref2] (ver secção 1.2).
Os métodos da interface [CategoriesRepository] são os seguintes:
- Linhas 13-14: O método [getLongCategoryById] devolve a versão longa de uma categoria referenciada pela sua chave primária [id], ou seja, a categoria juntamente com os seus produtos. Recorde-se que, na entidade [Category], o campo [products] tinha o atributo [fetch = FetchType.LAZY] (carregamento diferido). Na consulta JPQL, forçamos o carregamento dos produtos utilizando a palavra-chave [fetch]. O parâmetro ?1 da consulta será substituído em tempo de execução pelo valor do primeiro parâmetro do método na linha 12, ou seja, o parâmetro [Long id];
- linhas 16–17: o método [getLongCategoryByName] devolve a versão longa de uma categoria referenciada pelo seu nome [name];
- linhas 19–20: o método [getShortCategoriesByName] devolve as versões curtas das categorias referenciadas pelos seus nomes. O campo [products] destas categorias não é nulo. Contém uma referência a um proxy (uma classe criada pela implementação JPA) cuja função é recuperar os produtos da categoria quando chamado. Chamá-lo fora do contexto de persistência JPA lança uma exceção (Hibernate e OpenJPA, mas não EclipseLink). Por este motivo, não utilizaremos o campo [products] 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 suportam 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.
6.4.3.2. A interface [ProductsRepository]
A interface [ProductsRepository] gere o acesso à tabela [PRODUCTS]:
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> {
// un produit avec sa catégorie
@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 [getLongProductById] devolve a versão longa de um produto identificado pela sua chave primária [id], incluindo a sua categoria. Recorde-se que, na entidade [Product], o campo [category] tinha o atributo [fetch = FetchType.LAZY] (carregamento diferido). Na consulta JPQL, forçamos o carregamento da categoria utilizando a palavra-chave [fetch];
- linhas 18-19: o método [getLongProductByName] 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 [category] não é nulo. Contém uma referência a um proxy gerado pela implementação JPA, que, se chamado, irá buscar a categoria do produto. Esta chamada só pode ser feita dentro do contexto de persistência JPA. Fazê-lo 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 [category] de um produto na sua versão curta. Na versão resumida do produto, o campo [idCategorie] é inicializado. O seu valor é a chave primária da categoria à qual o produto pertence. Isto permite-nos recuperar posteriormente esta categoria a partir da 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 [getLongProductsByName] devolve a versão longa dos produtos identificados pelos seus nomes;
- linhas 33-34: o método [getAllLongProducts] devolve a versão longa de todos os produtos;
- linhas 36-37: o método [getAllShortProducts] devolve a versão resumida de todos os produtos;
Estas interfaces serão implementadas por classes geradas pela implementação JPA em tempo de execução. Tais classes são chamadas de classes [proxy]. Por predefinição, os métodos da interface [CrudRepository] são executados dentro de uma transação. O facto de as interfaces [ProductsRepository] e [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 abordada na implementação da camada [DAO] utilizando o Spring JDBC (ver secção 4.7);
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity;
import java.util.List;
public interface IDao<T extends AbstractCoreEntity> {
// list of all T entities
public List<T> getAllShortEntities();
public List<T> getAllLongEntities();
// special entities - short version
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);
// special entities - long version
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);
// update of several entities
public List<T> saveEntities(Iterable<T> entities);
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
// delete all entities
public void deleteAllEntities();
// deletion of multiple entities
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 o acesso à tabela [PRODUITS];
- a classe [DaoCategorie], que implementa a interface [IDao<Categorie>] e gere o acesso à tabela [CATEGORIES];
O seu código é o descrito na Secção 4.8, com a seguinte diferença menor: nenhum método possui o atributo [@Transactional], o que faz com que o método seja executado dentro de uma transação. Aqui, aproveitamos o facto de as interfaces [CrudRepository] do Spring Data serem executadas dentro de uma transação por predefiniçã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] torna a classe [DaoCategorie] um componente Spring;
- linha 18: a classe [DaoCategorie] estende a classe [AbstractDao<Categorie>], o que significa que implementa a interface [IDao<Categorie>];
- linhas 20–24: injeção de referências às duas interfaces [CrudRepository] do [Spring Data]. Esta injeção ocorre quando os objetos Spring são instanciados, normalmente no início da execução do projeto Spring;
- Todos os métodos da classe delegam o trabalho aos métodos com os mesmos nomes nas interfaces [CrudRepository];
- Todos os métodos que devolvem entidades na sua forma abreviada indicam isso definindo o tipo de entidade como [EntityType.PROXY] (linhas 29, 63, 72);
O método [saveEntities] merece uma explicação:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// on note les produits qui vont être insérés
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);
}
// on en profite pour rétablir (si besoin est) la relation produit --> categorie
produit.setCategorie(categorie);
}
}
}
// on persiste les catégories / produits
try {
categoriesRepository.save(categories);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// on met à jour le champ [idCategorie] des produits insérés
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// résultat
return categories;
}
- linha 2: as categorias passadas como parâmetros são tanto categorias a inserir [id==null] como categorias a atualizar [id!=null];
- linha 20: persistimos as categorias utilizando o método [categoriesRepository.save(entities)]. Durante os testes, observamos que o campo [idCategorie] dos produtos guardados (id==null) não é preenchido. Para resolver este problema, indicamos nas linhas 4–17 os produtos a inserir e, uma vez guardados, preenchemos o seu campo [idCategorie] (linhas 25–27);
- linhas 5–17: percorremos a lista de categorias;
- linhas 8–16: para cada categoria, percorremos a sua lista de produtos. Aqui reside uma dificuldade. O método [saveEntities] é utilizado tanto para persistir como para modificar uma categoria. Neste último caso, a categoria pode ter sido recuperada na sua versão curta, contendo assim uma referência a um método proxy no campo [products]. Utilizá-lo com o Hibernate provoca então uma exceção, porque a categoria que está a ser utilizada já não se encontra no contexto de persistência JPA, que foi encerrado no final da transação do método que recuperou as versões curtas das categorias. Utilizamos então o campo [EntityType] da entidade [Category] na linha 8 para determinar se podemos ou não aceder à lista de produtos da categoria;
- linha 14: associamos o produto à sua categoria. Normalmente, isto já deveria estar feito. Mas não sabemos como este produto foi criado nem se já foi associado à sua categoria. Assim, para evitar quaisquer problemas de « » (para gerir a entidade [Product], o JPA exige que esta faça referência à entidade [Category] à qual está associada), estabelecemos nós próprios esta associação.
Ao comparar este código com o da classe [DaoProduit] na implementação Spring JDBC (ver secção 4.9), podemos ver que a biblioteca Spring Data JPA simplifica consideravelmente a escrita da camada [DAO].
6.4.4.4. A classe [ProductDao]
![]() |
A classe [DaoProduct] implementa a interface [IDao<Product>] 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) {
// re-establish (if necessary) the link between a product and its category
for (Produit produit : entities) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// we persist products
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 longas das categorias, os testes mostram 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 modifica os existentes. A camada JPA exige que cada entidade [Product] esteja ligada a uma entidade [Category]. Como não sabemos se o utilizador já o fez, fazemo-lo nós próprios nas linhas 110–113. Tudo o que precisamos de fazer é ligar o [Product] a uma entidade [Category] cuja chave primária corresponda ao campo [idCategory] do [Product]. Durante os testes, verificamos que ocorre um erro se definirmos a versão da categoria como nula. Por isso, aqui definimo-la como 0, mas podemos defini-la como quisermos. Além da chave primária, nenhum campo da entidade [Category] é exigido pela camada JPA para inserir ou atualizar uma entidade [Product];
6.4.5. A camada de teste
![]() |
![]() |
Os testes acima são idênticos aos da implementação JDBC do Spring. Consulte as páginas seguintes, se necessário:
- [JUnitTestCheckArguments]: secção 4.11.1;
- [JUnitTestDao]: secção 4.11.2;
- [JUnitTestPushTheLimits]: secção 4.11.3;
Utilizamos as seguintes configurações de teste:
![]() | ![]() |
![]() | ![]() |
Os resultados obtidos nos vários testes são os seguintes:
![]() | ![]() |
![]() |
Em [1], o teste [JUnitTestPushTheLimits] com a implementação Spring Data JPA Hibernate e, em [2], com a implementação Spring JDBC. Podemos ver que esta última apresenta melhor desempenho. Chegamos, portanto, a uma conclusão inicial: é significativamente mais fácil desenvolver uma camada [DAO] com Spring Data JPA, mas esta apresenta um desempenho inferior ao de uma implementação Spring JDBC.
O teste [JUnitTestProxies] é um teste JUnit fictício. Serve para demonstrar como cada implementação JPA se comporta ao lidar com 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 {
// layer [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
@Before
public void clean() {
// the base is cleaned before each test
log("Vidage de la base de données", 1);
// we empty table [CATEGORIES] and cascade table [PRODUITS]
daoCategorie.deleteAllEntities();
}
@Test
public void doNothing() {
System.out.println("doNothing");
}
private List<Categorie> fill(int nbCategories, int nbProduits) {
// fill the tables
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);
}
// adding the category - by cascading the products will also be
// inserted
daoCategorie.saveEntities(categories);
// result
return categories;
}
@Test
public void getShortCategoriesByName1() {
// filling
fill(1, 1);
// test
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() {
// filling
fill(1, 1);
// test
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() {
// filling
fill(1, 1);
// test
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() {
// filling
fill(1, 1);
// test
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) {
// poster message
String toPrint = null;
switch (mode) {
case 1:
toPrint = String.format("%s --------------------------------", message);
break;
case 2:
toPrint = String.format("-- %s", message);
break;
}
System.out.println(toPrint);
}
}
Os resultados 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]
Aqui podemos ver que, ao aceder ao campo [Categorie.produits] de uma categoria do tipo PROXY e ao campo [Produit.categorie] de um produto do tipo PROXY, é lançada uma exceção [org.hibernate.LazyInitializationException] em ambos os casos (linhas 7 e 17).



































