Skip to content

Latest commit

 

History

History
370 lines (292 loc) · 14.5 KB

08_armazenamento.md

File metadata and controls

370 lines (292 loc) · 14.5 KB

Substituindo o mecanismo de armazenamento

Até agora temos usado persistência dos dados em memória. Isso é útil para prototipações rápidas mas nem tanto em ambientes de produção, afinal queremos que nossos dados persistam a reinicializações de servidor.

Para resolver este problema precisamos substituir a implementação de FilmesRepository atualmente usada pela aplicação, ou seja, trocar a classe FilmesRepositoryRAM por uma classe que conecta a ideia de negócio FilmesRepository com um provedor de armazenamento diferente. Utilizaremos uma base relacional postgres para este fim, em conjunto com a biblioteca Spring Data.

Vamos começar adicionando o starter-pom Spring Data JPA no projeto. Isso é feito no arquivo build.gradle, na seção de dependências:

dependencies {
    [...]
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '1.5.7.RELEASE'
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: '1.5.7.RELEASE'
    [...]
}

Depois temos que atualizar os descritores do projeto na IDE:

$ gradle idea

Agora vamos criar uma outra implementação para a interface FilmesRepository, chamada FilmesRepositoryJPA:

import com.opensanca.trilharest.movies.comum.Pagina;
import com.opensanca.trilharest.movies.comum.ParametrosDePaginacao;
import java.time.LocalDate;
import java.util.UUID;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class FilmesRepositoryJPA implements FilmesRepository {

  @Autowired
  private EntityManager entityManager;

  @Override
  public Pagina<Filme> buscarPaginaEmExibicao(ParametrosDePaginacao parametrosDePaginacao,
      LocalDate referencia) {

    CriteriaBuilder cb = this.entityManager.getCriteriaBuilder();

    CriteriaQuery<Filme> criteriaSeletora = cb.createQuery(Filme.class);
    Root<Filme> raizSeletora = criteriaSeletora.from(Filme.class);
    criteriaSeletora.where(construirPredicadoEmExibicao(referencia, cb, raizSeletora));
    Query querySeletora = this.entityManager.createQuery(criteriaSeletora);
    querySeletora.setFirstResult((parametrosDePaginacao.getPagina() - 1) * parametrosDePaginacao.getTamanhoDaPagina());
    querySeletora.setMaxResults(parametrosDePaginacao.getTamanhoDaPagina());

    CriteriaQuery<Long> criteriaContadora = cb.createQuery(Long.class);
    Root<Filme> raizContadora = criteriaContadora.from(Filme.class);
    criteriaContadora.select(cb.count(cb.literal(1)));
    criteriaContadora.where(construirPredicadoEmExibicao(referencia, cb, raizContadora));
    Query queryContadora = this.entityManager.createQuery(criteriaContadora);

    Pagina<Filme> pagina = new Pagina<>();
    pagina.setTotalDeRegistros(((Number) queryContadora.getSingleResult()).intValue());
    pagina.setRegistros(querySeletora.getResultList());
    return pagina;
  }

  @Override
  public Filme buscarPorId(UUID id) {
    return this.entityManager.find(Filme.class, id);
  }

  private Predicate construirPredicadoEmExibicao(LocalDate referencia, CriteriaBuilder cb,
      Root<Filme> raiz) {
    return cb.between(cb.literal(referencia), raiz.get("inicioExibicao"), raiz.get("fimExibicao"));
  }
}

Se tentarmos rodar a aplicação neste momento teremos o seguinte erro:

Cannot determine embedded database driver class for database type NONE

Isso ocorre pois não temos nenhum driver JDBC no classpath, tampouco dissemos para o Spring Data qual é o data source padrão dele. Vamos resolver a questão do data source colocando os seguintes parâmetros no nosso application.properties:

spring.datasource.url=jdbc:postgresql://127.0.0.1/filmes
spring.datasource.username=postgres
spring.datasource.password=123456

Agora o Spring Data vai tentar carregar o driver do postgres, mas não terá sucesso pois este não está no classpath. Vamos adicioná-lo no arquivo build.gradle na seção de dependências:

compile group: 'org.postgresql', name: 'postgresql', version: '42.1.4'

Nota: lembre-se de rodar o comando gradle idea sempre que alterar o build.gradle.

Por fim temos que criar a base de dados filmes, podemos fazer isso via psql ou via pgAdmin.

Note que depois disso o Spring não vai deixar a aplicação subir, alegando que existem duas implementações para a interface FilmesRepository:

Field filmesRepository in com.opensanca.trilharest.movies.filmes.FilmesAPI required a single bean, but 2 were found:
	- filmesRepositoryJPA: defined in file [/home/samuel/wksp/trilharest/hello1/out/production/hello1/com/opensanca/trilharest/movies/filmes/FilmesRepositoryJPA.class]
	- filmesRepositoryRAM: defined in file [/home/samuel/wksp/trilharest/hello1/out/production/hello1/com/opensanca/trilharest/movies/filmes/FilmesRepositoryRAM.class]

Como resolver? Uma opção é remover de vez a classe FilmesRepositoryRAM, visto que ela não nos servirá no futuro (de fato é isso que faremos). Uma outra seria anotar uma das classes com a anotação @Primary.

Suba a aplicação, e tente recuperar uma lista de filmes em exibição acessando a URL http://localhost:8080/filmes?pagina=1&tamanhoDaPagina=3. Não vai dar certo, pois não anotamos a entidade Filme com os metadados que o JPA precisa:

import javax.persistence.Id;

@Entity
public class Filme {

    @Id
    private UUID id;

O último erro antes de conseguirmos de fato executar essa consulta é a criação da tabela no banco de dados! Quem diria! Temos diversas formas de resolver aqui, a primeira e mais ingênua seria escrever o DDL create table na mão e executar também manualmente dentro do pgAdmin. Essa estratégia funciona, mas começa a dar trabalho quando temos vários ambientes. Enviar scripts SQL por e-mail e pedir para um DBA executar são tarefas comuns mas que hoje em dia não fazemos mais.

A segunda forma é deixar o Hibernate criar tudo sozinho. Veja como podemos fazer isso com uma simples propriedade no application.properties:

spring.jpa.hibernate.ddl-auto=update

A priori isso parece muito bom, mas essa técnica é indicada apenas durante a fase de prototipação e validação de ideias, sendo ruim quando sua aplicação passa por evoluções e o banco precisa ser ajustado. Por exemplo, como adicionar um campo obrigatório dessa forma? Ou refatorar uma estrutura polimórfica que usava coluna de categoria para uma estrutura que usa várias tabelas do tipo 1 para 1? O ddl-auto não está preparado e não tem a intenção de resolver esse tipo de problema.

Observando a estrutura criada pelo Hibernate podemos identificar um outro problema, ele não entendeu o significado das classes de data do Java 8 (repare como temos que ajustar isso aqui também, assim como tivemos que ajustar com o Spring MVC, esse é o custo de usar APIs novas, mas é um custo tranquilo considerando os benefícios), mapeando todas para o tipo bytea (o que significa que ele vai simplesmente serializar em um array de bytes as instâncias, o que é bem longe do que realmente queremos).

Para resolver isso temos que configurar um pacote de conversores na classe FilmesApplication:

import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;

@ComponentScan
@EntityScan(basePackageClasses = {FilmesApplication.class, Jsr310JpaConverters.class})
public class MoviesApplication {

Repare que se tentar executar sem dropar a tabela existente, o ddl-auto não vai dar erro nem alterar o tipo da coluna, o que é um problema. Drope a tabela via psql ou pgAdmin e suba a aplicação novamente.

Note que resolvemos parcialmente o problema para as colunas de data (não queremos um timestamp mas sim um date) e não resolvemos nada com relação ao tipo Duration. Infelizmente não parece existir uma solução fácil para a questão do Duration que não seja escrever um conversor manualmente, a não ser que estejamos afim de criar uma dependência direta com o Hibernate, neste caso basta adicionar a seguinte dependência no build.gradle:

compile group: 'org.hibernate', name: 'hibernate-java8', version: '5.0.12.Final'

Lembre-se do gradle idea. Remove as configurações que fizemos no passo anterior referentes aos conversores Jsr310, drope a tabela existente e suba a aplicação novamente.

Agora sim! Finalmente temos um mapeamento coeso.

  duracao bigint,
  fim_exibicao date,
  inicio_exibicao date,

Vamos voltar agora à questão do ddl-auto. Não queremos continuar usando-o pois teremos problemas com as próximas iterações do nosso banco de dados. Por isso adotaremos uma ferramenta de migração de banco de dados, que ficará responsável por executar scripts de migração nos ambientes da nossa solução. As duas mais conhecidas no mundo Java são o Liquibase e o Flyway. Usaremos o Liquibase na trilha, mas os conceitos são muito similares entre as duas ferramentas.

O Spring Boot já está preparado para usar o Liquibase, basta adicionarmos ele no projeto colocando uma dependência no build.gradle:

compile group: 'org.liquibase', name: 'liquibase-core', version: '3.5.3'

Lembre-se do gradle idea.

Assim que a aplicação sobe, o Liquibase é disparado e verifica se existe um arquivo no classpath no caminho db/changelog/db.changelog-master.yaml. Vamos criar esse arquivo com uma migração inicial. Note que esse arquivo pode ser criado dentro do diretório src/main/java, mas é boa prática manter arquivos desse tipo no diretório src/main/resources. Vamos criá-lo então:

$ mkdir src/main/resources
$ gradle idea

E por fim criar o arquivo db.changelog-master.yaml no caminho src/main/resources/db/changelog/:

databaseChangeLog:
  - changeSet:
      id: 1
      author: samuelgrigolato
      changes:
      - sqlFile:
          dbms: postgresql
          encoding: utf8
          path: sqls/0001-migracao-inicial.sql
          relativeToChangelogFile: true

Repare que estamos delegando a migração para um arquivo SQL, vamos criá-lo no caminho src/main/resources/db/changelog/sqls/0001-migracao-inicial.sql:

create table filme
(
  id uuid not null,
  duracao bigint,
  fim_exibicao date,
  inicio_exibicao date,
  nome character varying(100) not null,
  sinopse text,

  constraint filme_pkey primary key (id)
);

Antes de executar a aplicação não podemos nos esquecer de retirar o ddl-auto do arquivo application.properties e dropar a tabela existente.

Vamos adicionar registros equivalentes à versão RAM para testar corretamente:

insert into filme(id, duracao, fim_exibicao, inicio_exibicao, nome, sinopse)
values ('252f29dc-b5ba-437d-b070-c5b7a7ad128b', 153::bigint * 60 * 1000 * 1000 * 1000, '2017-10-20', '2017-10-1', 'Filme 1', 'Sinopse do filme 1'),
       ('75fc717d-f893-4e7b-8cb7-461ee34243c4', null, '2017-11-10', '2017-09-02', 'Filme 2', null),
       ('06a25754-2f4c-4b9a-b1a0-71b14ce99faa', null, null, null, 'Filme 3', null),
       ('29313c21-087a-4a6c-b972-01f5fe445087', 120::bigint * 60 * 1000 * 1000 * 1000, '2017-10-19', '2017-10-02', 'Filme 4', 'Sinopse do filme 4');

Vamos agora dar uma olhada na classe FilmesRepositoryJPA. Ela está muito verbosa. Felizmente podemos melhorar isso consideravelmente usando a API de interfaces de repositório semânticas do Spring Data. Isso é, ao invés de implementar queries simples, vamos instruir o Spring Data a fazer isso por nós.

A primeira coisa que vamos fazer é remover a classe FilmesRepositoryJPA. Depois vamos ajustar nossa interface FilmesRepository para usar os tipos semânticos do Spring Data:

import java.time.LocalDate;
import java.util.UUID;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.CrudRepository;

public interface FilmesRepository extends CrudRepository<Filme, UUID> {

  @Query("select f from Filme f where ?1 between f.inicioExibicao and f.fimExibicao")
  Page<Filme> buscarPaginaEmExibicao(LocalDate referencia, Pageable pageable);

}

Note que temos que atualizar também a classe FilmesAPI pois mudamos nosso domínio:

@RequestMapping(path="", method=RequestMethod.GET)
@ApiOperation(value="Buscar página de filmes em exibição",
    notes="Permite buscar uma página de registros em exibição. " +
            "Um filme em exibição é um filme que possui ambas " +
            "datas de início/fim e cujo período contempla a data de hoje")
public Page<Filme> get(Pageable parametrosDePaginacao) {
    LocalDate hoje = LocalDate.now();
    return filmesRepository.buscarPaginaEmExibicao(hoje, parametrosDePaginacao);
}

@RequestMapping(path="/{id}", method=RequestMethod.GET)
@ApiOperation("Buscar filme por ID")
public Filme getPorId(@ApiParam("identificador do filme") @PathVariable UUID id) {
    return filmesRepository.findOne(id);
}

Uma coisa bem conveniente aconteceu, como incorporamos as classes de domínio Page e Pageable do Spring Data, não precisamos mais das classes Pagina e ParametrosDePaginacao no nosso próprio domínio!

Use a seguinte URL para testar: http://localhost:8080/filmes/em-exibicao?page=0&size=2.

Antes de passar para o próximo passo, vamos dar uma última melhorada no retorno da busca por ID. Note que se passar um UUID não existente na base, a API retorna 200 OK com conteúdo vazio. Não é bem isso que queremos (queremos um 404), então vamos tratar isso na classe FilmesAPI:

@RequestMapping(path="/{id}", method=RequestMethod.GET)
@ApiOperation("Buscar filme por ID")
public Filme getPorId(@ApiParam("identificador do filme") @PathVariable UUID id) {
    Filme entidade = filmesRepository.findOne(id);
    if (entidade == null) {
        throw new EntidadeNaoEncontradaException();
    }
    return entidade;
}

Ficou um pouco melhor, mas o status code retornado está sendo 500 ao invés de 404. Isso pode confundir a equipe de front-end, então vamos anotar a classe de exceção para resolver esse ponto também:

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class EntidadeNaoEncontradaException extends RuntimeException {
}

E assim acabamos este tópico. Clique aqui para ir para o próximo assunto.