Versão ALPHA! Este artigo está em versão 'Alpha' e, portanto, não foi ainda revisado corretamente

Implementando Buscador: Buscadores

Introdução

Seguimos agora com a especificação e a implementação do Buscador.

Antes de iniciar

Este artigo assume conhecimento prévio em:

  • EspecificaçãoTODO
  • TDD
  • scripts SQL flywayTODO

Passo-a-passo

Especificação

Siga o checklist abaixo:

O TesteDeBuscarFuncionario foi devidamente implementado? help!
Quem ele implementa? help!
Quais são as propriedades envolvidas? help!

O TesteDeBuscarFuncionario foi devidamente implementado?

É importante se certificar de que o teste foi devidamente revisado, para que ele possa apontar corretamente os erros nas implementações envolvidas.

Quem ele deve implementar?

É importante ficar claro que ao implementar a Interface do Buscador (no nosso caso BuscarFuncionario), o Guice (ou BuscarFuncionarioGuice) terá de implementar os métodos de busca, que foram definidos no artigo que fala sobre TesteDeBuscarFuncionario.

Portanto, ele terá a implementação dos métodos:

  Funcionario porId(int id);
  
  Funcionario porMatricula(String matricula);
  
  List<Funcionario> porSuperior(Superior superior);
  
  Iterator<Funcionario> iterarPorFuncionario(Superior superior);
  
  Funcionario porDiretor(Diretor diretor);

Quais são as propriedades envolvidas?

Conhecimento mandatório. Dentre elas, temos:

  • Nome do banco de dados (DATABASE);

  • Nome da tabela Funcionário (FUNCIONARIO), e das demais relacionadas (SUPERIOR);

  • Nome das colunas e suas propriedades

Nossa tabela FUNCIONARIO ficará conforme abaixo:

       ID                  integer         not null     auto_increment, 
       NOME                varchar(50)     not null,
       MATRICULA           char(8)         not null,
       DATA_NASCIMENTO     date()          not null, 
       ADMISSAO            datetime        not null,
       DEMISSAO            datetime,
       SUPERIOR_ID         integer         not null,
       DIRETOR_ID          integer         not null,
       
       primary key         (ID),
       unique              (MATRICULA)

Implementação

Siga o checklist abaixo:

Adicionando as notações na Interface `BuscarFuncionario`
Criar a classe _BuscarFuncionarioGuice_
Declarar o SqlProvider e defini-lo no Construtor
Implementar o método newSelect()
Implementar os Filtros e os métodos de busca
Verificar se a Interface está correta
Clique aqui e vamos implementar o Loader!

Passo a passo

Adicionando as notações na Interface BuscarFuncionario

Primeiramente abra a interface do Buscador, que em nosso caso é BuscarFuncionario, nela já deverão existir os métodos que criamos através dos atalhos do eclipse no TesteDeBuscarFuncionario:

  public interface BuscarFuncionario {
  
    Funcionario porId(int id);
  
  }

E nela adicione a anotação @ImplementedBy para que o Buscador seja implementado por BuscarFuncionarioGuice.

  @ImplementedBy(BuscarFuncionarioGuice.class)
  public interface BuscarFuncionario {
  
    Funcionario porId(int id);
  
  }

Você verá um alerta de erro de compilação. Isso ocorre porque a classe Guice não existe. Para resolver, siga o procedimento abaixo.

Criar a classe BuscarFuncionarioGuice

Utilizando as teclas de atalho Ctrl + ponto e em seguida Ctrl + 1, Enter vamos criar esta classe e na sequência começar a implementar seu código, que por enquanto estará assim:

  class BuscarFuncionarioGuice {
  
  }
A classe `Guice` será acessada pelo `Buscador`. Portanto, não se esqueça desse detalhe: a classe não será pública, ela será declarada como `default`:
  class BuscarFuncionarioGuice {
  
  }

E ela irá implementar a classe `BuscarFuncionario`

  class BuscarFuncionarioGuice implements BuscarFuncionario {
  
  }
Ao terminar a implementação, haverá erros de compilação sugerindo que sejam implementados os métodos da Interface. É necessário seguir essa sugestão, portanto use {CTRL + '1' e ENTER} sobre o nome da classe para escolher essa opção.

Declarar SqlProvider e defini-lo no Construtor

Devemos declarar uma variável do tipo Provider que será responsável por instanciar um objeto do tipo NativeSql, nosso código ficará da seguinte maneira:

  private final Provider<NativeSql> sqlProvider;

Agora utilizando as teclas de atalho Alt + s, a, criaremos o Construtor da classe. Acima do construtor, adicionaremos uma anotação @Inject para que a classe seja instanciada:

  @Inject
  public BuscarFuncionarioGuice(Provider<NativeSql> sqlProvider) {
    super();
    this.sqlProvider = sqlProvider;
  }

É necessário remover o super(). Fazendo assim, a classe ficará conforme abaixo:

class BuscarFuncionarioGuice implements BuscarFuncionario {

  private final Provider<NativeSql> sqlProvider;

  @Inject
  public BuscarFuncionarioGuice(Provider<NativeSql> sqlProvider) {
    this.sqlProvider = sqlProvider;
  }

}

Implementar o método newSelect()

Esse método é responsável por promover uma consulta completa, ou seja, com todas as colunas da entidade no Banco de Dados, e com todos os relacionamentos dela.

Começamos implementando a assinatura do método e seu retorno.

Atenção: Esse método será privado!

  private NativeSql newSelect() {
    return sqlProvider.get()

      .andLoadWith(new FuncionarioLoader());
  }

Este método irá retornar a instância de NativeSql que chamamos de ` sqlProvider, ela foi criada logo no início da classe. Resumidamente, a classe FuncionarioLoader` será responsável por trazer as informações de cada coluna de nossas tabelas do banco de dados.

Para dar continuidade ao desenvolvimento, caso não haja implementação do Loader da classe, apenas crie uma e deixe-a com o mínimo possível de código, como o exemplo abaixo.
  class FuncionarioLoader implements ResultSetLoader<Funcionario> {

    @Override
    public Funcionario load(ResultSet resultSet) throws SQLException {
      return null;
    }

  }
O Loader deve implementar o ResultSetLoader<?> para ser parametrizado na chamada andLoadWith(). Por isso o mínimo de código possível capaz de não deixar erros de compilação é o código acima.

Depois do Loader, preencheremos a consulta inicial ao banco de dados:

  private NativeSql newSelect() {
    return sqlProvider.get()

      .add("select *")
      .add("from DATABASE.FUNCIONARIO as FUNCIONARIO")

      .andLoadWith(new FuncionarioLoader());
  }

Em nosso exemplo, vamos considerar que a tabela Funcionario está se relacionando com uma tabela chamada Superior. Desta forma, se quisermos por exemplo realizar uma pesquisa trazendo todos os funcionários cadastrados haverá uma coluna informando qual superior é responsável por cada funcionário.

Para que possamos trazer informações deste relacionamento em nossa consulta faremos um JOIN, ou seja, uma união entre as tabelas.

Em nosso buscar podemos utilizar JOIN , RIGHT JOIN, LEFT JOIN, no exemplo abaixo utilizaremos JOIN, para maiores informações sobre este tema consulte o artigo Cláusula Join.

  private NativeSql newSelect() {
    return sqlProvider.get()

      .add("select *")
      .add("from DATABASE.FUNCIONARIO")

      .add("join DATABASE.SUPERIOR")
      .add("on FUNCIONARIO.SUPERIOR_ID = SUPERIOR.ID")

      .andLoadWith(new FuncionarioLoader());
  }

A classe completa ficou da seguinte forma:

  class BuscarFuncionarioGuice implements BuscarFuncionario {

    private final Provider<NativeSql> sqlProvider;

    @Inject
    public BuscarFuncionarioGuice(Provider<NativeSql> sqlProvider) {
      this.sqlProvider = sqlProvider;
    }

    private NativeSql newSelect() {
      return sqlProvider.get()

          .add("select *")
          .add("from DATABASE.FUNCIONARIO")

          .add("join DATABASE.SUPERIOR")
          .add("on FUNCIONARIO.SUPERIOR_ID = SUPERIOR.ID")

          .andLoadWith(new FuncionarioLoader());
    }

  }

Implementar Filtros e os métodos de busca

O método buscarFuncionario.porId() previamente usado na classe TesteDeBuscarFuncionario, será agora implementado. O objetivo deste método é trazer o funcionário cadastrado no banco de dados que possua um determinado ID. Para que esta pesquisa seja possível teremos que realizar uma consulta SQL em nosso banco.

Devido a implementação da Interface do Buscador, já devemos ter o seguinte bloco:

  @Override
  public Funcionario porId(int id) {
    return null;
  }
Perceba que ele já virá com a anotação de sobrescrita @Override.

Agora mudaremos o retorno dele. A interface NativeSql que estamos utilizando permite diversos retornos que serão ajustados de acordo com nossa consulta, os mais comuns são single() e list(). Em nosso exemplo, por se tratar de um valor único, ao informar um ID nossa pesquisa deverá retornar apenas um funcionário. Por isso usaremos o retorno single()

  @Override
  public Funcionario porId(int id) {
    return newSelect()

        .add("where FUNCIONARIO.ID = ?").param(id)

        .single();
  }
Sabendo que ID é único para cada Registro e Objeto de Funcionário, podemos afirmar que esse filtro nunca retornará uma listagem.

Por fim, ajustaremos nossa consulta ao banco de dados adicionando a cláusula WHERE ao método newSelect() :

@Override
public Funcionario porId(int id) {
  return newSelect()

    .add("where FUNCIONARIO.ID = ?").param(id)

    .single();
  }

Desta forma podemos notar que ao executar o código, o método newSelect() que seleciona o banco de dados que iremos utilizar e faz as ligações (Join) necessárias com outras tabelas é chamado antes de nossos métodos de busca.

A classe BuscarFuncionarioGuice está agora com essa implementação:

<a id="guice"> </a>

class BuscarFuncionarioGuice implements BuscarFuncionario {

    private final Provider<NativeSql> sqlProvider;
      
@Inject
public BuscarFuncionarioGuice(Provider<NativeSql> sqlProvider) {
  this.sqlProvider = sqlProvider;
}
      
@Override
public Funcionario porId(int id) {
  return newSelect()
  
  .add("where FUNCIONARIO.ID = ?").param(id)
    
  .single();
}
      
private NativeSql newSelect() {
  return sqlProvider.get()
    
          .add("select *")
          .add("from DATABASE.FUNCIONARIO")

          .add("join DATABASE.SUPERIOR")
          .add("on FUNCIONARIO.SUPERIOR_ID = SUPERIOR.ID")

    .andLoadWith(new FuncionarioLoader());
}

}

Verificar se a Interface está correta

Verifique na Interface se os métodos dela correspondem com os métodos públicos implementados no Guice.

@ImplementedBy(BuscarFuncionarioGuice.class)
public interface BuscarFuncionario {

  Funcionario porId(int id);

}

Variação A: Múltiplos Filtros em um único método

Para maiores informações sobre Filtros recomendamos a leitura do artigo Implementação de Filtros.

Variação B: Extraindo parâmetro(s) utilizados na consulta

Para deixar nosso código mais organizado, no método porId() iremos extrair o ID do funcionário antes de realizarmos a pesquisa.

Para extrair esta informação de FUNCIONARIO, guardaremos o ID do funcionário dentro de uma variável:

int _id = funcionario.getId();

O método completo ficará da seguinte forma:

@Override
public Funcionario porId(Funcionario funcionario) {
  int _id = funcionario.getId();
    return newSelect()
    
      .add("where FUNCIONARIO.ID = ?").param(_id)

      .single();
}

Erros comuns

No uso do JOIN no newSelect()

Fazer o JOIN incorretamente ou deixar de fazer um JOIN fará com que propriedades de tabelas relacionadas a nossa consulta não sejam acessadas.

Desta forma, vários erros poderão ocorrer ao executarmos nosso teste.

private NativeSql newSelect() {
return sqlProvider.get()
    
    .add("select *")
    .add("from DATABASE.FUNCIONARIO")

    .add("join DATABASE.SUPERIOR")
    .add("on FUNCIONARIO.SUPERIOR_ID = SUPERIOR.ID")

    .andLoadWith(new FuncionarioLoader());
}

Por exemplo, se no método acima tivéssemos esquecido de fazer o JOIN, nossa consulta traria todos os dados de FUNCIONARIO exceto os referente a coluna SUPERIOR.ID pois essa coluna recebe informações da tabela SUPERIOR. Essa situação causaria um erro no TesteDeBuscarFuncionario.

Discernir o que é ou não UNIQUE na tabela

Verifique o flyway mais atual da tabela ou dê um show create table <tabela> nela via phpMyAdmin.

  set foreign_key_checks=0;
  create database if not exists DATABASE ;
  drop table if exists DATABASE.FUNCIONARIO;
  
  create table DATABASE.FUNCIONARIO (
       ID                 integer     not null     auto_increment, 
       NOME               varchar(50) not null,
       MATRICULA          char(8)     not null,
       DATA_NASCIMENTO    date()      not null, 
       ADMISSAO           datetime    not null,
       SUPERIOR_ID        integer     not null,
       DIRETOR_ID         integer     not null,
       
       primary key        (ID),
       unique             (MATRICULA)
  ) type=InnoDB;

Os métodos que realizam uma pesquisa no banco de dados a partir de um campo “único”, como é o caso de métodos que utilizam Id para realizar a busca, retornam apenas um registro do banco de dados. Desta forma, esses métodos possuem assinaturas parecidas com os exemplos abaixo:

Funcionario porId(int id);

Funcionario porMatricula(String matricula);

Já os métodos que usam parâmetros que não são “únicos” e por isso podem trazer vários registros em sua pesquisa ao banco de dados, necessitarão de uma estrutura que armazene uma coleção de dados. Sendo assim, as assinaturas deste métodos serão similares aos exemplos abaixo:

List<Funcionario> porSuperior(Superior superior);

Iterator<Funcionario> iterarPorFuncionario(Superior superior);

O tipo de retorno dos Filtros e métodos de busca

Ao implementar um método ou filtro devemos verificar o tipo de retorno que ele deverá ter, devemos observar se nossa consulta poderá retornar um registro ou se poderá trazer vários registros do banco de dados. No exemplo abaixo, um superior está relacionado com muitos funcionários. Por isso ao buscarmos por funcionários relacionados a um determinado superior, poderemos receber vários registros sendo necessário armazená-los em uma coleção.

@Override
public List<Funcionario> porSuperior(Superior superior) {
  int superiorId = superior.getId();
  return newSelect()
  
    .add("where SUPERIOR.ID = ?").param(superiorId)
  
    .list()
        
}

O exemplo abaixo demonstra o retorno no caso de Iteradores:

@Override
public Iterator<Funcionario> iterarPorDiretor(Diretor diretor) {
  Integer _diretor = diretor.getId();
  
    return newSelect()
    
    .add("where FUNCIONARIO.DIRETOR_ID = ?").param(_diretor)
  
    .iterate();
  }

Neste exemplo, nossa consulta deverá retornar apenas um registro:

@Override
public Funcionario porMatricula(String matricula) {
  return newSelect()
  
  .add("where FUNCIONARIO.MATRICULA = ?").param(matricula)

  .single();
}

A Classe deve ter visibilidade default

Os métodos responsáveis por realizar buscas no banco de dados, como por exemplo os métodos da classe BuscarFuncionarioGuice são acessados em tempo de execução através da interface implementada BuscarFuncionario. Por esta razão, a visibilidade de BuscarFuncionarioGuice deverá ser default.

Portanto, é “errado” declararmos uma classe Guice como public, ela deve ser declarada conforme o exemplo abaixo:

  class BuscarFuncionarioGuice implements BuscarFuncionario

Missing @ImplementedBy(BuscarFuncionarioGuice.class) na Interface

Deixar de colocar essa anotação na classe BuscarFuncionario causará erros que podem ser verificados por nosso buscador, pois a classe BuscarFuncionario é implementada por BuscarFuncionarioGuice e esta anotação serve para indicar esta condição.

Loader errado carregado na Query

No método newSelect(), o Loader da entidade que estamos trabalhando deve ser “carregado” no trecho .andLoadWith(). Um outro possível erro da classe BuscarFuncionarioGuice seria “carregarmos” erroneamente o Loader de outra entidade, conforme o exemplo abaixo:

private NativeSql newSelect() {
  return sqlProvider.get()

    .add("select *")
    .add("from DATABASE.FUNCIONARIO")

    .add("join DATABASE.SUPERIOR as SUPERIOR")
    .add("from FUNCIONARIO.SUPERIOR_ID")

    .add("join DATABASE.DIRETOR as DIRETOR")
    .add("from FUNCIONARIO.DIRETOR_ID")

    .andLoadWith(new DiretorLoader());
} Neste exemplo, o correto seria passarmos como parâmetro uma nova instância do Loader de Funcionários e não de diretor:

    .andLoadWith(new FuncionarioLoader());

Erro de sintaxe SQL ou de nomes de propriedades no banco errados

Na classe BuscarFuncionarioGuice, ao implementarmos os métodos que realizam pesquisa no banco de dados, podemos errar o nome de alguma coluna de uma tabela.

Caso isso ocorra, um erro na consulta SQL será informado no stacktrace para que possamos verificar onde o problema está acontecendo.

Exemplos de erro de digitação, onde no lugar da coluna FUNCIONARIO é digitamos erroneamente FUCIONARIO sem a letra “N”:

@Override
public Funcionario porId(Funcionario funcionario) {
  Integer _funcionario = funcionário.getId();
    return newSelect()

    .add("where FUCIONARIO.ID = ?").param(id)

    .single();
}

Ou ainda podemos colocar um nome diferente do nome da coluna da tabela do banco de dados: No exemplo abaixo, o nome correto da coluna no banco de dados seria ID mas em nossa consulta SQL passamos o nome da coluna como ID_FUNCIONARIO.

@Override
public Funcionario porId(Funcionario funcionario) {
  Integer _funcionario = funcionário.getId();
    return newSelect()
    
    .add("where FUNCIONARIO.ID_FUNCIONARIO = ?").param()

    .single();
}

Utilizar o Provider para NativeSql

Utilizamos o Provider<NativeSql>. Marcamos ele como final para não permitir que sua referência possa ser mudada em tempo de execução.

Como consequência, obrigatoriamente devemos colocá-lo no Construtor da Classe.

  private final Provider<NativeSql> sqlprovider;

O construtor deve estar com @Inject

Como já foi explicado anteriormente. Veja no artigo de testes em caso de dúvida.

Vamos seguir em frente? Loaders!

...Ou voltar? Testes!

Leia mais uma vez! Revisar!

Ler códigos!

BuscarFuncionario.java BuscarFuncionarioGuice.java
FuncionarioLoader.java

 
 

objectos, Fábrica de Software LTDA

  • R. Demóstenes, 627. cj 123
  • 04614-013 - Campo Belo
  • São Paulo - SP - Brasil
  • +55 11 5093-8640
  • +55 11 2359-8699
  • contato@objectos.com.br