Pesquisar este blog

segunda-feira, 31 de agosto de 2015

Banco de dados Java com JPA, Hibernate e Spring Data.

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>
            <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>
E estas mesmas dependências no SBT, no arquivo build.sbt:
 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
@Configuration
public class JPAConfig{
//Definições
}
Agora iremos criar um arquivo de propriedades, que deverá ser armazenado na pasta resources, conforme descrito aqui

#Configuração de acesso ao banco
db.driver=org.h2.Driver
db.url=jdbc:h2:mem:dsd
db.username=sa
db.password=

#Configuração do Hibernate
hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.hbm2ddl.auto=create-drop
hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy
hibernate.show_sql=false
hibernate.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")
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);
}
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.
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:
  @Bean
    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;
    }
Com isto configuramos o acesso ao banco, pelo hibernate.

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:
@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
}
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.
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 {
@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;
}
}

Repare que:
  1. 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.
  2. Todas os parâmetros, possuem um método de get e set (padrão Bean)
  3. 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.
  4. A anotação @Entidade define esta classe como uma tabela
  5. A anotação @Id define uma coluna como chave primária daquela tabela.
Com isso mapeamos uma tabela para uma classe Java, agora para conseguir armazenar dados dela em um banco de dados, será necessário criar um Repositório.
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)
No exemplo do sistema desenvolvido neste blog, temos o usuário e seus dados, digamos que foi definido que um usuário pode ter vários endereços, portanto o relacionamento entre usuários e endereços será OneToMany, um usuário terá muitos endereços. Esta configuração pode ser feita assim:

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;
}
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.
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.
Tipificar a lista usando o generics, List<Endereco> é uma boa prática de programação, com isso garantimos que só serão aceitos objetos do tipo Endereço, na lista de endereços.
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:

public class Endereco {
@OneToOne(cascade = CascadeType.ALL, optional = false, fetch = FetchType.EAGER, orphanRemoval = true)
@PrimaryKeyJoinColumn
private Rua rua;
}
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.

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

Nenhum comentário:

Postar um comentário