Spring Boot — Como criar uma aplicação do zero — Parte 2 (Persistência)

Marcelo Carvalho
13 min readApr 23, 2019

--

Continuando com nossa ideia de criar uma aplicação completa do zero, com Spring Boot, hoje veremos como persistimos facilmente em um banco de dados.

Em nossa primeira parte eu mostrei como criar um projeto utilizando o Spring Initializr. Peço que se por acaso não viram o artigo anterior, por favor deem uma olhada agora. Neste, iremos utilizá-lo para criar novamente outro projeto, mas agora, com as bibliotecas responsáveis por criar, atualizar e recuperar os dados que serão salvos em um banco de dados.

Criando o projeto

Como no artigo anterior, nosso projeto será um projeto Gradle, com Java e Spring Boot 2.1.4. Nosso “Group” será com.mvalho.medium.example e nosso “Artifact” se chamará persistence e não mais hello.

Agora, diferentemente do nosso primeiro projeto, vamos adicionar duas dependências. Para isso é muito simples, na caixa de texto chamada “Search for dependencies” digite “JPA ”e uma caixa de seleção se abrirá logo abaixo da caixa de texto.

Vamos fazer o mesmo procedimento para nosso banco de dados. Digitaremos “HSQLDB ” na caixa de texto e adicionaremos como uma de nossas dependências. O resultado ficará como visto abaixo.

Resultado

Após isso, só clicar no botão para gerar o projeto. 😃

O resultado será uma estrutura como na figura abaixo:

Criando uma Entidade

No nosso sistema iremos cadastrar pessoas, coisa simples e bem trivial. O nosso objetivo aqui é mostrar como o Spring Boot trabalha com JPA e não vamos focar no JPA em si. Por isso, espero que você já tenha alguma familiaridade com JPA/Hibernate para uma melhor compreensão.

Pessoa.java

package com.mvalho.medium.example.persistence.entity;import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.time.LocalDate;
@Entity
public class Pessoa {
private long id;
private String nome;
private LocalDate dataDeNascimento;
private Pessoa() {
}
public Pessoa(String name, LocalDate dataDeNascimento) {
this.name = name;
this.dataDeNascimento = dataDeNascimento;
}
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "pessoa_id")
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
@Column
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
@Column
public LocalDate getDataDeNascimento() {
return dataDeNascimento;
}
public void setDataDeNascimento(LocalDate dataDeNascimento) {
this.dataDeNascimento = dataDeNascimento;
}
@Override
public String toString() {
return "Pessoa{" +
"id=" + id +
", nome='" + nome + '\'' +
", dataDeNascimento=" + dataDeNascimento +
'}';
}
}

A classe “Pessoa” é bem simples e utilizamos as anotações do pacote javax.persistence. Eu criei um novo pacote abaixo do pacote padrão chamado entity nesse pacote criei a nossa classe “Pessoa”.

Spring Data

Quando lá no inicio, utilizando o Spring Initializr, nós adicionamos a dependência JPA ao nosso projeto, no arquivo build.gradle foi adicionado o
compile(‘org.springframework.boot:spring-boot-starter-data-jpa’) que diferentemente do nosso primeiro projeto possui o “data-jpa” no final, isso indica ao Spring Boot que ele deve não somente carregar as bibliotecas do Spring mas também que vamos utilizar o Spring Data e que utilizaremos algum banco de dados junto à nossa aplicação. Por isso, é importante adicionar a biblioteca do banco de dados que vamos utilizar junto ao JPA. No nosso caso ficou assim:

dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
runtime('org.hsqldb:hsqldb')
testCompile('org.springframework.boot:spring-boot-starter-test')
}

Quando rodarmos a nossa aplicação, o Spring Boot irá escanear o classpath em busca dos drives dos bancos de dados que ele suporta, assim que ele encontrar, ele tentará encontrar no application.properties as configurações para o banco se ele não achar, tentará usar os valores defaults de acordo com a documentação de cada banco de dados.

Primeiramente iremos fazer com que o Spring Boot utilize as configurações padrões do HSQLDB, depois disso, veremos como mudá-las.

Então vamos criar nosso repositório da entidade “Pessoa”. Para isso, vamos estender a interface JpaRepository.

Mas o que é exatamente essa interface? O JpaRepository faz parte do Spring Data, um projeto criado pela Spring para intermediar operações comuns com bancos de dados como CRUDs e busca por campos das entidades do sistema. Para isso, o projeto oferece algumas interfaces que ao serem estendidas, disponibilizam a implementação das mesmas para serem escaneadas, identificadas e utilizadas como beans pelo Spring.

Não é somente isso, ao extender uma das interfaces, dois tipo de parâmetros (Type Parameters) são obrigatórios. O primeiro, a classe de domínio. O segundo o tipo de ID que a entidade utiliza, por exemplo um tipo Long. Se você está confuso neste momento, não se preocupe, veremos isso no código mais a frente.

Esses dois tipos de parâmetros servem para que o Spring defina métodos com assinaturas baseadas na entidade utilizada, ou seja, um save da entidade “Pessoa” terá uma assinatura esperando que o parâmetro seja um objeto do tipo “Pessoa” e o retorno também seja.

Parece legal, mas ainda pode ser um pouco melhor.

Às vezes queremos buscar somente pelo nome ou ainda queremos uma lista de todos os nomes que começam com a letra M, também gostaríamos de buscar todas as pessoas em um intervalo de data de nascimento. Se você já é um programador com uma certa experiência, pode estar pensando em algumas linhas de código para conseguir esses resultados, mas veremos que a solução utilizando o Spring Data é muito mais simples.

Então vamos logo à isso, certo? Certo!

JpaRepository

Por que vamos utilizar essa interface ao invés de outras? O motivo é simples, pois ela estende várias outras interfaces e trás algumas características específicas do JPA para seu repositório como por exemplo, o Query By Methods, veremos isso mais adiante.

E o que devemos fazer para utilizar o básico do básico dessa interface? Outra coisa bem simples. Só precisamos criar uma interface que estenda a JpaRepository.

Então vamos lá, criaremos um novo pacote com o nome de “repository” dentro dele criaremos uma interface chamada “PessoaRepository”, essa interface irá extender (como dito anteriormente) a interface JpaRepository. Os “type parameters” serão Pessoa e Long. Veja o resultado abaixo:

PessoaRepository.java

package com.mvalho.medium.example.persistence.repository;import com.mvalho.medium.example.persistence.entity.Pessoa;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PessoaRepository extends JpaRepository<Pessoa, Long> {}

Bem simples né? Se você está se perguntando se precisará implementar essa interface criada, a resposta é não. O Spring Data se encarregará de fazer isso em tempo de execução. Se a sua próxima pergunta for “e se eu precisasse criar um query específica, como faria?”. Também há formas de conseguir isso sem precisar implementar a interface criada. Mas se mesmo depois disso tudo você ainda quer implementar essa interface, vá em frente e faça.

Enfim… Depois de criado precisamos testar e ver se está funcionado.

@EnableJpaRepositories

Na Parte 1 dessa série de artigos, vimos que quando criamos um novo projeto com o Spring Boot precisamos de uma main class, que será nossa classe de configuração da aplicação. Essa classe é anotada com @SpringBootApplication e graças a ela o Spring Boot saberá que todas as configurações estarão ali.

Nesse nosso novo projeto, nossa classe principal se chama “PersistenceApplication” e para que possamos usufruir de nosso repositório, vamos ter que adicionar mais uma anotação, @EnableJpaRepositories.

A anotação @EnableJpaRepositories irá habilitar a busca à nossas interfaces. Por padrão ele escaneia todos os pacotes abaixo do pacote onde a anotação se encontra, ou seja, no nosso caso a anotação está na nossa classe principal, que se encontra em “com.mvalho.medium.example.persistence”. Nossa interface PessoaRepository, encontra-se no pacote repository, sob o pacote persistence. Sendo assim, nossa interface será automaticamente identificada. Se não fosse esse o caso, teríamos que especificar o local para a anotação utilizando os parâmetros da anotação basePackages ou basePackageClasses.

Adicionando essa anotação já garantimos que nossa aplicação funcione conectada ao banco de dados. Não acredita? vamos fazer nossos testes então.

No artigo anterior, também utilizamos o CommandLineRunner e agora para podermos interagir com nossa interface e brincar com o banco de dados utilizamos de novo.

PersistenceApplication.java

package com.mvalho.medium.example.persistence;import com.mvalho.medium.example.persistence.entity.Pessoa;
import com.mvalho.medium.example.persistence.repository.PessoaRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import java.time.LocalDate;
import java.time.Month;
import java.util.List;
@SpringBootApplication
@EnableJpaRepositories
public class PersistenceApplication {
@Autowired
private PessoaRepository pessoaRepository;
public static void main(String[] args) {
SpringApplication.run(PersistenceApplication.class, args);
}
@Bean
public CommandLineRunner commandLineRunner() {
return args -> {
Pessoa marcelo = new Pessoa("Marcelo Carvalho", LocalDate.of(1984, Month.SEPTEMBER, 18));
Pessoa libia = new Pessoa("Líbia Bráz", LocalDate.of(1990, Month.MAY, 30));
Pessoa joao = new Pessoa("João Carvalho", LocalDate.of(1954, Month.MARCH, 5));
Pessoa cleunice = new Pessoa("Cleunice Carvalho", LocalDate.of(1958, Month.MARCH, 3));
System.out.println("-- Salvando pessoas");
this.pessoaRepository.save(marcelo);
this.pessoaRepository.save(libia);
this.pessoaRepository.save(joao);
this.pessoaRepository.save(cleunice);
List<Pessoa> pessoas = this.pessoaRepository.findAll(); pessoas.forEach(pessoa -> System.out.println(pessoa));
System.out.println("Pessoas encontradas " + pessoas.size());
System.out.println();
System.out.println("-- Atualizando pessoas");
pessoas.forEach(pessoa -> {
pessoa.setName(pessoa.getNome() + " atualizado");
this.pessoaRepository.save(pessoa);
});
pessoas = this.pessoaRepository.findAll();
pessoas.forEach(pessoa -> System.out.println(pessoa));
System.out.println("Pessoas encontradas " + pessoas.size());
System.out.println();
System.out.println("-- Excluindo pessoas");
this.pessoaRepository.deleteAll(pessoas);
pessoas = this.pessoaRepository.findAll(); System.out.println("Pessoas encontradas " + pessoas.size()); };
}
}

Com isso, teremos o resultado a seguir no console:

Resultado do nosso código.

Como você pode perceber foi bem simples utilizarmos nossa interface e o CRUD básico foi utilizado com sucesso, Criamos, Recuperamos, Atualizamos e Deletamos as pessoas com poucas linha de código e, sem escrevermos uma linha de SQL.

SQL Nativo

Há certos momentos em que criar um query com SQL nativo é necessário, então, temos duas opções para chegar a esse resultado.

Como disse lá no inicio, teremos algumas queries personalizadas. São elas

  1. Busca por pessoas por nome exato.
  2. Lista de nomes que iniciam com um determinada letra.
  3. Pessoas com data de nascimento dentro de um intervalo.

Nossa primeira tentativa será com SQL Nativo. Para isso podemos usar a anotação @Query. Ela deve ser utilizada em um método e para uma query nativa devemos passar 2 parâmetros, o value que será a query e o native query que como o nome já diz, indica que a query passada no value deverá ser lida como uma query nativa.

Então vamos alterar nossa interface de repositório para deixamos como a seguir:

package com.mvalho.medium.example.persistence.repository;import com.mvalho.medium.example.persistence.entity.Pessoa;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.time.LocalDate;
import java.util.List;
public interface PessoaRepository extends JpaRepository<Pessoa, Long> { @Query(value = "select p.* from pessoa p where p.nome = ?1", nativeQuery = true)
Pessoa buscarPessoaPorNomeExato(String nomeCompleto);
@Query(value = "select p.* from pessoa p where p.nome like ?1%", nativeQuery = true)
List<Pessoa> buscarPessoasPorLetraDoPrimeiroNome(String letra);
@Query(value = "select p.* from pessoa p where p.data_de_nascimento between ?1 and ?2", nativeQuery = true)
List<Pessoa> buscarPessoasComDataDeNascimentoNoIntervalo(LocalDate de, LocalDate ate);
}

Como podemos ver, criamos métodos abstratos na interface, demos um nome, os parâmetros necessário e o retorno esperado. Após isso, anotamos cada um dos métodos com a anotação @Query como value colocamos as queries para cada um dos casos. Note que a query recebe os parâmetros dos métodos com “?1” ou seja, ponto de interrogação mais o número que identifica a índice do parâmetro.

Nosso próximo passo é testar o que fizemos, e isso pode ser visto abaixo. Assim como no artigo anterior, iremos utilizar a própria main class para fazermos nossos testes. Utilizaremos novamente o CommandLineRunner para criamos algumas pessoas e depois trabalharmos com elas.

package com.mvalho.medium.example.persistence;import com.mvalho.medium.example.persistence.entity.Pessoa;
import com.mvalho.medium.example.persistence.repository.PessoaRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import java.time.LocalDate;
import java.time.Month;
import java.util.List;
@SpringBootApplication
@EnableJpaRepositories
public class PersistenceApplication {
@Autowired
private PessoaRepository pessoaRepository;
public static void main(String[] args) {
SpringApplication.run(PersistenceApplication.class, args);
}
@Bean
public CommandLineRunner commandLineRunner() {
return args -> {
Pessoa marcelo = new Pessoa("Marcelo Carvalho", LocalDate.of(1984, Month.SEPTEMBER, 18));
Pessoa libia = new Pessoa("Líbia Bráz", LocalDate.of(1990, Month.MAY, 30));
Pessoa joao = new Pessoa("João Carvalho", LocalDate.of(1954, Month.MARCH, 5));
Pessoa cleunice = new Pessoa("Cleunice Carvalho", LocalDate.of(1958, Month.MARCH, 3));
Pessoa jose = new Pessoa("José Carvalho", LocalDate.of(1930, Month.DECEMBER, 30));
System.out.println("-- Salvando pessoas");
this.pessoaRepository.save(marcelo);
this.pessoaRepository.save(libia);
this.pessoaRepository.save(joao);
this.pessoaRepository.save(cleunice);
this.pessoaRepository.save(jose);
this.pessoaRepository.flush(); System.out.println("-- Buscando por nome exato: Marcelo Carvalho");
Pessoa pessoaComNomeExato = this.pessoaRepository.buscarPessoaPorNomeExato("Marcelo Carvalho");
System.out.println("Encontrado: " + pessoaComNomeExato);
System.out.println();
System.out.println("-- Buscando por nome com letra J de inicio");
List<Pessoa> pessoasComLentraJ = this.pessoaRepository.buscarPessoasPorLetraDoPrimeiroNome("J");
System.out.println("Encontrado: " + pessoasComLentraJ);
System.out.println();
System.out.println("-- Buscando por intervalo de data de nascimento");
List<Pessoa> pessoasNoIntervalo = this.pessoaRepository.buscarPessoasComDataDeNascimentoNoIntervalo(LocalDate.of(1950, Month.JANUARY, 01), LocalDate.of(1960, Month.DECEMBER, 31));

System.out.println("Encontrado: " + pessoasNoIntervalo);
};
}
}

Aqui podemos ver que todos as chamadas que fazemos para os métodos sempre espera algum retorno, como estudo de caso você poderá fazer os testes passando dados não existentes ou dados diferente para verificar o funcionamento.

Ao rodar nosso projeto com o comando abaixo, os resultados serão impressos no console.

gradle bootRun

Resultado do Query By Method

Aqui funcionou como deveria, e ai? :D

Queries Personalizadas — Outra forma

Bom, vimos no capitulo anterior como criar queries nativas para nosso repositório. Se você já trabalhou com persistência de dados em Java, com certeza percebeu a simplicidade de não só ter os métodos básico para trabalhar com o banco (CRUD) mas também de ter queries personalizadas sem muita dificuldades utilizando SQL nativo.

Há outra forma de obter o mesmo resultado e de forma mais simples (na minha opinião claro). Ela se chama Query Methods.

Query Methods é uma forma dinâmica de se criar queries com base na entidade do repositório. Bem vago isso, eu sei. Mas o framework toma com base sua entidade e disponibiliza todos os campos dela para serem usadas no nome do método.

O que o framework faz é oferecer ao desenvolvedor uma série de palavras chaves (em inglês) comuns quando fazemos consultas no banco e junto delas permitir o uso dos atributos da entidade do repositório.

Acredito que com um exemplo será mais prático e fácil para entender.

Vamos atualizar nossa interface PessoaRepository e adicionaremos mais 3 métodos, veja:

package com.mvalho.medium.example.persistence.repository;import com.mvalho.medium.example.persistence.entity.Pessoa;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.time.LocalDate;
import java.util.List;
public interface PessoaRepository extends JpaRepository<Pessoa, Long> {
@Query(value = "select p.* from pessoa p where p.nome = ?1", nativeQuery = true)
Pessoa buscarPessoaPorNomeExato(String nomeCompleto);
@Query(value = "select p.* from pessoa p where p.nome like ?1%", nativeQuery = true)
List<Pessoa> buscarPessoasPorLetraDoPrimeiroNome(String letra);
@Query(value = "select p.* from pessoa p where p.data_de_nascimento between ?1 and ?2", nativeQuery = true)
List<Pessoa> buscarPessoasComDataDeNascimentoNoIntervalo(LocalDate de, LocalDate ate);
Pessoa findPessoaByNomeEquals(String nomeCompleto); List<Pessoa> findPessoasByNomeStartsWith(String letra); List<Pessoa> findPessoasByDataDeNascimentoBetween(LocalDate de, LocalDate ate);
}

Os três métodos são basicamente uma “tradução” dos outros três métodos que já existiam na interface.

Perceba no primeiro novo método que foi usado as palavras find(buscar), by(por) e equals(igual). São algumas das palavras chaves usadas pelo Spring Data para montar a query necessária para o retorno.

No segundo método além de find e by, também utilizamos mais duas palavras chaves starts(começa/inicia) e with(com). Caso quiséssemos buscar por mais de um atributo, poderiamos usar operadores lógicos And(e) e Or(ou). Um exemplo, buscar por nome e data de nascimento ficaria:

Pessoa findPessoaByNomeAndDataDeNascimento(String nome, LocalDate dataDeNascimento)

No terceiro método, utilizamos between(entre). Ele útil principalmente quando queremos buscar por um periodo entre datas. Como foi feito neste caso.

Com isso, conseguimos criar os mesmos resultados de uma forma mais simples, algo como se escrevessemos o que queremos para o Spring Data. Legal não é?

Uma observação pessoal, em caso de queries longas, é melhor você construir a query de outra forma.

Em todos os nomes dos métodos, após o verbo adicionamos o nome da nossa entidade, no caso Pessoa. Isso funciona somente para uma melhor leitura do que nosso Query Method fará. Caso o nome do método fosse findByNomeStartsWith também funcionaria.

Trocar as Configurações Padrões

Como eu disse lá no início, há uma forma de você especificar as informações para que o Spring Data conecte-se com o banco de dados escolhido.

No nosso caso, para facilitar, nós utilizamos o banco de dados em memória chamado HSQLDB. As configurações para o funcionamento dele foram feitas com base nos valores padrões especificados na documentação do banco de dados.

Agora iremos conhecer como usar valores personalizados para conectar ao banco. No caso de você precisar conectar à um banco de dados externo será dessa forma que você deve configurar sua aplicação.

Bom, dentro da nossa estrutura de projeto tempo um arquivo chamado “application.properties” ele estará vazio nesse momento. Pra quem na conhece esse tipo de arquivo funciona como chave/valor que são separados pelo sinal igual (=). Ali adicionaremos as chaves que o Spring Data espera encontrar para personalizar a inicialização do banco de dados.

spring.datasource.url=jdbc:hsqldb:file:spring-boot-jpa
spring.datasource.username=SA
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true

Agora adicione as linhas acima no seu application.properties. As três primeiras linhas são informações para conectar ao banco. Veja:

  1. Diz qual o endereço do nosso banco de dados. No caso do HSQLDB não há um endereço fisico, apenas informações de como o nosso banco vai se comportar, ou seja, a leitura seria mais ou menos assim: “jdbc use o hsqldb como banco, criado em arquivo físico (file), com o nome do banco de dados sendo spring-boot-jpa”. Assim, dentro do seu projeto você verá os arquivos do banco de dados criados quando rodar a aplicação pela primeira vez.
  2. Nesta linha estamos especificando o usuário do banco de dados que por padrão é “SA”.
  3. A senha padrão é vazia então no valor não precisamos passar nada.

Se você já tem experiência deve ter notado a falta da especificação do Driver. Isto não é necessário no Spring Data, pois ele já identifica a assinatura do driver pela dependência que utilizamos no projeto.

As duas últimas linhas são configurações úteis do JPA. Veja:

  1. Essa linha específica se você quer que o JPA/Hibernate crie/atualize as tabelas do seu banco de dados com base nas suas entidades. Se a opção for:
    - create: toda vez que você iniciar sua aplicação, seu banco será apagado e criado novamente;
    - update: toda vez que você iniciar sua aplicação, o ORM irá verificar se houve alguma modificação nas suas entidades e atualizará o schema caso necessário;
    - validate: apenas válida se entidades e schema estão iguais. Não faz alterações;
    - none: Não faz nada. Útil para quando o banco é criado de forma externa, como por exemplo usando scripts.
    Existem mais opções como pode ser visto aqui.
  2. Essa serve para você ver no console os SQLs das transações que estão sendo feitas entre sua aplicação e o banco de dados.

Ao rodar sua aplicação você verá no console isso:

Hibernate: drop table pessoa if exists
Hibernate: drop sequence hibernate_sequence if exists
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: create table pessoa (pessoa_id bigint not null, data_de_nascimento date, nome varchar(255), primary key (pessoa_id))

Isso é nossa configuração personalizada funcionando :) você também poderá ver:

Hibernate: call next value for hibernate_sequence
Hibernate: insert into pessoa (data_de_nascimento, nome, pessoa_id) values (?, ?, ?)
Hibernate: call next value for hibernate_sequence
Hibernate: insert into pessoa (data_de_nascimento, nome, pessoa_id) values (?, ?, ?)
Hibernate: call next value for hibernate_sequence
Hibernate: insert into pessoa (data_de_nascimento, nome, pessoa_id) values (?, ?, ?)
Hibernate: call next value for hibernate_sequence
Hibernate: insert into pessoa (data_de_nascimento, nome, pessoa_id) values (?, ?, ?)
Hibernate: call next value for hibernate_sequence
Hibernate: insert into pessoa (data_de_nascimento, nome, pessoa_id) values (?, ?, ?)

E os arquivos do nosso banco de dados aparecerão assim:

Essa foi a segunda parte sobre Como Criar uma Aplicação do Zero com Spring Boot, a próxima será como disponibilizar nossa aplicação para ser consumida externamente utilizando REST. O objetivo, como dito anteriormente, desses artigos é levantar aquela aplicação para testar algo que estamos aprendendo ou pra alguma Prova de Conceito (POC). Não será explicado à fundo como tudo funciona, aliás, nem sei se conseguiria haha.

Qualquer dúvida é só perguntar!

--

--