O JPA é uma api do java para persistência, Java Persistence API. Persistência é um termo utilizado para se referir ao armazenamento dos dados em um sistema de banco de dados. Esta API surgiu para facilitar a utilização de bancos de dados, com o Java.Com o JPA fica extremamente fácil migrar de um banco de dados para outro, uma vez que não precisamos escrever códigos em SQL, ele faz toda a comunicação com o banco pra nós, com isso só precisamos trocar a configuração de conexão e pronto, o sistema já estará usando um banco de dados diferente.
Hibernate é um ORM (Object Relational Mapping), ou ferramenta de mapeamento de um banco relacional para objetos, nele vamos mapear cada tabela do nosso banco de dados, para uma classe, e as suas relações também serão mapeadas.
Spring Data é um framework que facilita o trabalho de toda a camada de acesso e troca de informações com um banco de dados. Nele é possível gerarmos queries, CRUDs, sem ter que escrever muito código.
Lembrando que todo o código referente ao desenvolvimento deste sistema web, estarão nos repositórios do github1 e github2.
Dependências
Para iniciar altere o seu arquivo pom.xml adicionando as seguintes dependências, para quem usa o maven:<dependency>E estas mesmas dependências no SBT, no arquivo build.sbt:
<groupid>org.springframework.data</groupid>
<artifactid>spring-data-jpa</artifactid>
<version>1.8.2.RELEASE</version>
</dependency>
<dependency>
<groupid>org.hibernate</groupid>
<artifactid>hibernate-entitymanager</artifactid>
<version>5.0.0.Final</version>
</dependency>
<dependency>
<groupid>com.h2database</groupid>
<artifactid>h2</artifactid>
<version>1.4.185</version>
</dependency>
<dependency>
<groupid>com.zaxxer</groupid>
<artifactid>HikariCP</artifactid>
<version>2.2.5</version>
</dependency>
libraryDependencies ++= Seq("org.springframework.data" % "spring-data-jpa" % "1.8.2.RELEASE" , "org.hibernate" % "hibernate-entitymanager" % "5.0.0.Final" , "com.h2database" % "h2" % "1.4.185", "com.zaxxer" % "HikariCP" % "2.2.5")Como deu para notar a versão utilizando o SBT foi muito mais simples.
Com as configurações acima adicionamos três dependências ao nosso projeto. A primeira delas, o Spring-Data é um framework que nos ajuda a lidar com a manipulação dos dados no Java. O hivernate-entitymanager é quem realiza a iteração com o banco de dados, é ele que vai explicitamente, se comunicar com o banco de dados e executar as operações. Temos um banco de dados que é o h2, este banco não precisa de instalação, pois ele roda na memória e é muito bom para executarmos testes. A última dependência é o HikariCP que é um administrador de conexões com o banco de dados, também conhecido como connection pool, o hikari abre uma série de conexões com o banco de dados e administra estas conexões de forma e deixar o acesso ao banco mais rápido.
Configuração
Um dos primeiros passos para desenvolver um aplicativo que se conecta ao banco de dados é configurar o acesso ao banco, para realizar estas configurações, iremos utilizar as anotações do java.O primeiro passo da configuração é criar uma classe de configuração que definirá as propriedades da camada de persistência, para isso criam os uma classe com a anotação @Configuration
@ConfigurationAgora iremos criar um arquivo de propriedades, que deverá ser armazenado na pasta resources, conforme descrito aqui
public class JPAConfig{
//Definições
}
#Configuração de acesso ao bancodb.driver=org.h2.Driverdb.url=jdbc:h2:mem:dsddb.username=sadb.password=#Configuração do Hibernatehibernate.dialect=org.hibernate.dialect.H2Dialecthibernate.hbm2ddl.auto=create-drophibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategyhibernate.show_sql=falsehibernate.format_sql=true
Neste arquivo estamos configurando tanto um acesso ao banco H2 na memória, na primeira parte, quanto a configuração referente ao Hibernate.
Configurações dos Beans
Definição do bean de conexão ao banco de dados:@Bean(destroyMethod = "close")Aqui estamos assegurando que toda vez que o nosso software for desligado, ele chamará o método destroy da conexão, com isso evitamos que conexões ficam abertas com a base de dados.
DataSource dataSource(Environment env) {
HikariConfig dataSourceConfig = new HikariConfig();
dataSourceConfig.setDriverClassName(env.getRequiredProperty("db.driver"));
dataSourceConfig.setJdbcUrl(env.getRequiredProperty("db.url"));
dataSourceConfig.setUsername(env.getRequiredProperty("db.username"));
dataSourceConfig.setPassword(env.getRequiredProperty("db.password"));
return new HikariDataSource(dataSourceConfig);
}
Definição de uma conexão JDBC, setando as propriedades do driver, url, username e senha.
E a criação de um objeto HikariDataSoure, que será o objeto retornado, que contem a conexão com o banco de dados.
Bean EntityManagerFactory
EntityManagerFactory é a classe que cuida de Fabricar (Factory é um design pattern que administra a criação de objetos, conforme descrito aqui) os administradores de Entidades (Entity Managers), que são as classes responsáveis por prover métodos que realizam acesso aos dados do banco de dados:@BeanCom isto configuramos o acesso ao banco, pelo hibernate.
LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource,
Environment env) {
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource);
entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
entityManagerFactoryBean.setPackagesToScan("br.unip.dsd","br.unip.dsd.modelos");
Properties jpaProperties = new Properties();
//Configures the used database dialect. This allows Hibernate to create SQL
//that is optimized for the used database.
jpaProperties.put("hibernate.dialect", env.getRequiredProperty("hibernate.dialect"));
jpaProperties.put("hibernate.hbm2ddl.auto",
env.getRequiredProperty("hibernate.hbm2ddl.auto")
);
jpaProperties.put("hibernate.ejb.naming_strategy",
env.getRequiredProperty("hibernate.ejb.naming_strategy")
);
jpaProperties.put("hibernate.show_sql",
env.getRequiredProperty("hibernate.show_sql")
);
jpaProperties.put("hibernate.format_sql",
env.getRequiredProperty("hibernate.format_sql")
);
entityManagerFactoryBean.setJpaProperties(jpaProperties);
return entityManagerFactoryBean;
}
Transaction Manager
Transaction manager é a classe responsável por prover configurações de níveis de transação aos nossos métodos. Por exemplo, podemos ter várias entidades que devem ser cadastradas dentro de uma mesma transação, como no cadastro de um usuário, todos os dados referentes ao endereço, email, usuário e senha, devem ser todos registrados dentro de uma mesma transação,e é o Transaction Manager que nos ajuda a configurar este comportamento. O Spring Data possui um transaction manager desenvolvido para o JPA, o JPATransactionManager, e é ele que iremos utilizar:@Bean
JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}
Habilitando a configuração de Transações por Anotações
Existem algumas anotações que nos facilitam a vida na hora de habilitar as configurações descritas acima. Para habilitar a configuração das transações, iremos adicionar a anotação @EnableTransactionManager na nossa classe de configuração, que ficará assim:@Configuration
@EnableTransactionManager
public class JPAConfig{
//Definições
}
Configuração do Spring Data JPA
A configuração dos Repositórios do Spring Data JPA será realizada através da anotação @EnableJpaRepositories, que será adicionada a nossa classe de configuração:Nesta configuração define o pacote br.unip.dsd.repositorios, como o pacote onde ficarão as classes de repositório do Spring Data. Com isso todas as classes deste pacote serão analisadas, e todas as classes que extendem os repositórios serão adicionadas ao projeto, com as funcionalidades de um repositório.@Configuration @EnableJpaRepositories("br.unip.dsd.repositorios") @EnableTransactionManagement @ComponentScan(basePackages = {"br.unip.dsd.modelos", "br.unip.dsd.controller"}) @EnableWebMvc @PropertySource("classpath:application.properties") public class JPAConfig{ //Definições }
Repositório é a denominação da camada que faz acesso aos dados. Em um repositório existem os métodos de criação, alteração, inserção, remoção e queries das nossas entidades, e cada entidade tem o seu respectivo repositório.
Entidades
Entidades são as classes que possuem os dados das tabelas dos banco de dados, também conhecidas como POJO.Para criar uma entidade basta criar uma classe com os parâmetros da tabela, onde cada parâmetro é uma coluna da nossa tabela. A definição de uma classe como entidade, acontece através da anotação @Entity, conforme mostrado abaixo:
@Entity public class Cidade {Repare que:
@Id
private Long id;
private String nome;
public Cidade() {
}
public Cidade(Long id, String nome) {
this.id = id;
this.nome = nome;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Cidade)) return false;
Cidade cidade = (Cidade) o;
if (id != null ? !id.equals(cidade.id) : cidade.id != null) return false;
if (nome != null ? !nome.equals(cidade.nome) : cidade.nome != null) return false;
return true;
}
@Override
public int hashCode() {
int result = id != null ? id.hashCode() : 0;
result = 31 * result + (nome != null ? nome.hashCode() : 0);
return result;
}
}
- Temos um construtor com todos os argumentos e um vazio. O construtor vazio é uma exigência do JPA, o construtor com os argumentos é uma forma de facilitar a criação destas entidades no código.
- Todas os parâmetros, possuem um método de get e set (padrão Bean)
- Definição do método equals e hashCode, muito importantes para o Hibernate, já que é através deles que o Hibernate sabe se os dados daquela classe foram alterados e precisam ser persistidos no banco.
- A anotação @Entidade define esta classe como uma tabela
- A anotação @Id define uma coluna como chave primária daquela tabela.
Um fator muito importante dos bancos SQL, são os relacionamentos existentes entre as tabelas, para isso existem algumas anotações para facilitar a definição de relacionamento.
Relacionamentos
Dos possíveis relacionamentos existenstes, entre as tabelas, nos banco de dados temos:- um pra muitos (@OneToMany)
- muitos pra um (@ManyToOne)
- muitos pra muitos (@ManyToMany)
Aqui temos tanto o mapeamento um pra muitos, como o mapeamento muitos pra um. Como um endereço é de um usuário, ele terá um parâmetro do tipo Usuario definido na sua entidade. Já a entidade Usuario, possui muitos endereços, portanto ela terá um parâmetro com uma lista de Enderecos nela.
public class Usuario{
@OneToMany(fetch=FetchType.LAZY, cascade = CascadeType.ALL, mappedBy="usuario")
private List<Endereco>endereco = Collections.<Endereco>.emptyList();
}
public class Endereco {
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name="usuario_id")
private Usuario usuario;
}
As anotações OneToMany e ManyToOne mapeiam o relacionamento entre estas entidades.
O atributo mappedBy determina o nome do parâmetro, da outra ponta do relacionamento, no caso o atributo usuario da classe Endereco, que será responsável por mapear este relacionamento. Este mapeamento é feito pela anotação JoinColumn na entidade Endereco.
O atributo fech=Lazy define quando o atributo será requisitado ao banco de dados, neste caso Lazy, ou preguiçoso, este parâmetro por padrão vem como nulo e só será requisitado ao banco de dados quando ele for acessado. E isto tem algumas implicações:
- Toda vez que buscarmos um Endereço, ou um Usuario no banco de dados, não haverá nenhum select nas tabelas opostas de relacionamento, o que garante uma melhor performance. Repare que se for necessário ter os dados dos endereços, dentro da entidade usuário, ao fazer um select na tabela usuário, também deverá ser feito um select na tabela endereço, através do seu relacionamento.
- Para acessar os dados do relacionamento, deveremos ter aberta a conexão com o banco de dados. Um erro muito comum é tentar acessar estes dados na interface, onde a conexão com o banco já fo fechada e acontecer um erro. Com isso lembre-se de acessar todos os relacionamentos necessários para a sua lógica de negócio dentro da camada de banco de dados.
Finalmente, o atributo cascade, define o que será feito com os relacionamentos em caso de remoção.
Por exemplo, caso tenhamos um usuário e tentamos remover ele, mas este usuário possui alguns endereços. O banco de dados não permite que você deixe dados órfãos nele, então este tipo de remoção geraria uma mensagem de erro pelo banco.
O JPA pode cuidar disto pra você, removendo todos os filhos daquele dado, antes de removê-lo, e isto é feito definindo o tipo de cascade, neste caso o ALL define que todos os filhos serão apagados na mesma transação que o pai, automaticamente. Estas configurações são um pouco delicadas, já que ao apagar o usuário, você também estará apagando os endereços, e algumas vezes este não é o comportamento desejado, tome muito cuidado ao definir esta propriedade.
Uma propriedade de um relacionamento um pra um (OneToOne) é definido da seguinte maneira:
PrimaryKeyJoinColumn, indica que o join entre as duas entidades será realizado pela chave primária da entidade Rua, assim teremos uma coluna rua_id na entidade Endereço, que será utilizada para definir o relacionamento entre as duas entidades. Repare que aqui a propriedade de Fetch foi alterada para EAGER, ou seja, todo select realizado em endereço, trará os dados da Rua. Optional false quer dizer que esta propriedade não pode ser nula, e orphanRemoval diz que quando remover o dado de um Endereço, o dado da Rua também será removido.
public class Endereco {
@OneToOne(cascade = CascadeType.ALL, optional = false, fetch = FetchType.EAGER, orphanRemoval = true)
@PrimaryKeyJoinColumn
private Rua rua;
}
Repositórios
A definição de um repositório, é um dos pontos mais interessantes deste aparato de frameworks utilizados neste post.O Spring Data facilita bastante o desenvolvimento de transações com o banco de dados, principalmente por que quase zera a necessidade de código para realizar operações com o banco de dados, veja no exemplo abaixo:
package br.unip.dsd.repositorios;
import br.unip.dsd.modelos.Usuario;
import org.springframework.data.jpa.repository.JpaRepository;
public interface RepositorioUsuario extends JpaRepository{
public Usuario findById(Long id);
public String findNameById(Long id);
public Usuario findByNameAndId(String name, Long id);
}
Pronto, já temos uma classe que consegue inserir, apagar, atualizar, buscar, contar e fazer todas as operações básicas de uma entidade. Mas como isso?
Lembrem-se que configuramos o pacote br.unip.dsd.repositorios como sendo o pacote de repositórios do nosso projeto? Então com isso estas anotações criam os métodos básicos de CRUD para a nossa entidade Usuario, bastando somente extendermos a interface JpaRepository.
Um outro ponto interessante aqui, é que ao criarmos métodos findByParametro, ele automaticamente gera as queries de busca, pelo nome do parâmetro e você não precisa escrever nada de SQL, nada! Assim foi feito o método findById, você passa um id long, e o Spring gera a query pra você.
E se eu precisar de mais um atributo na minha query? Tudo bem, veja o método findByNameAndId, ele fará uma busca pelo nome e pelo id. Mas e se eu precisar buscar apenas um atributo? Veja o método findNameById, neste caso ele retorna apenas o atributo nome, referente ao id.
Isto facilita muito a nossa vida, pois não "sujamos" o nosso código com queries SQL, não precisamos escrever quase nada de código e tudo o que precisamos, praticamente já vem pronto de bonus.
Claro que se você precisar implementar algum código de query, isso é possível extendendo esta classe e implementando o seu método, mas na maioria das vezes isso não será necessário.
Apresentação