Projeto Java — Parte 3 — Spring Data JPA

Diogenes Silveira
Dev Cave
Published in
7 min readFeb 6, 2017

Vamos continuar com a terceira parte da série Projeto Java e vamos apresentar algumas ferramentas interessantes.

Para poder ter alguma coisa específica, continuaremos o desenvolvimento de nosso projeto. Fizemos nosso planejamento, priorização e estimativa (mentiiiiraaaa) e a seguinte tarefa para puxar é o cadastro de candidatos. Como explicamos no post sobre processo, as vezes podemos identificar que uma história é na realidade um épico, pois é fácil identificar varias histórias menores nela. Nesse caso podemos dividir o épico nas seguintes histórias:

  • Persistir Candidato
  • Pesquisar Candidato
  • Alterar Candidato
  • Detalhes do Candidato

Se preferir chamar isso de tarefas, tudo bem, é questão de nomenclatura. Mas tenha claro que esses pontos ainda vão ser divididos em várias mini tarefas, e vai precisar um nome para eles. Nós preferimos chamar a primeira divisão de história e a segunda de tarefa.

O que é Spring Data JPA?

Antes de começar, precisamos estabelecer umas bases. Você precisa conhecer JPA antes de continuar. Ou pelo menos Hibernate. Se não, o resto do post não fará sentido pra você.

Você já cansou de criar os mesmos métodos em todos os repositórios de acesso a dados? Repetir o getById, o findAll, o delete, o persist, o update? Bom, se cansou, talvez tentou criar Interfaces e Implementações de acesso a dados com Generics? O Spring Data é um daqueles projetos maravilhosos que vem para encapsular isso pra nós. Para melhorar o entendimento, vamos mostrar o código da entidade Candidato:

@Entity
@Table(name = "candidate")
@Getter
@EqualsAndHashCode(callSuper = false)
@ToString(callSuper = true)
public class Candidate extends BaseEntity {
@Id
@GeneratedValue
@Column(name = "id_candidate", nullable = false)
private Long id;
@Column(name = "nam_candidate", nullable = false, length = 255)
@NotBlank
@Size(max = 255)
private String name;
@Column(name = "des_email", nullable = false, unique = true, length = 255)
@NotBlank
@Size(max = 255)
private String email;
@Column(name = "num_phone", length = 20)
@Size(max = 20)
private String phoneNumber;
}

E a classe base:

/**
* Entidade abstrata base do resto. Mesmo que não tenha método abstrato,
* é bom para garantir que ninguem vai instanciar um objeto
*/
@MappedSuperclass
@Getter
@ToString
public abstract class BaseEntity {
@Column(name="dat_creation", nullable = false)
@Temporal(TemporalType.TIMESTAMP)
private Calendar creationDate;
@Column(name="dat_update")
@Temporal(TemporalType.TIMESTAMP)
private Calendar updateDate;
@Version
@Column(name="num_version", nullable = false)
private Long version;
public abstract Long getId();@PrePersist
public void prePersist(){
this.creationDate = Calendar.getInstance();
}
@PreUpdate
public void preUpdate(){
this.updateDate = Calendar.getInstance();
}
}

Vamos explicar as anotações menos conhecidas, calma, mas foquemos na entidade por enquanto. Ela tem poucos campos, nome, email, telefone, e os que vamos ter em todas as entidades, data de criação, data de atualização e versão do registro, já voltamos nisso.
Nessa classe base, podemos colocar todos aqueles campos que fazem sentido em todas as tabelas da nossa aplicação. As vezes podemos precisar adicionar também o usuário que fez a alteração, por exemplo. Ou talvez, pode usar outras opções relacionadas a auditoria. Isso vai depender da sua necessidade.

Antes de entrar no Spring Data, note que não tem getters nem setters. Realmente não tem? Veja o que faz o lombok. Mas de todas formas, note que mesmo com o Lombok, estamos só gerando os getters. Da uma olhada nesse link sobre como não aprender Java OO da Caelum. Espero não ter que explicar o que é o @Column, mas talvez se pergunte o por que do nível de detalhe, nullable, length, unique, etc. Isso é porque se precisamos gerar a tabela no banco a través da entidade (e vamos precisar nos testes), vamos deixar que o Hibernate cuide disso. Por último, o @Version cuida pra nós da concorrência. Isso evita que um registro seja modificado por duas threads ao mesmo tempo. Da uma pesquisa sobre lock optimista e lock pessimista se não tem muito claro o conceito.

Uma última coisa que talvez esteja te incomodando seja o uso do Calendar para trabalhar com datas. Se estamos babando com o Java 8, a programação funcional e outras novidades, porque não usar a nova API de datas (LocalDate, LocalDateTime)? O motivo é porque o JPA ainda não oferece suporte para trabalhar com essa API. Claro, poderíamos ter criado um converter para trabalhar com ela, mas preferimos não entrar nisso ainda.

Criando o repositório

Vamos criar então nosso repositório:

import br.com.devcave.recruiters.domain.Candidate;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CandidateRepository extends JpaRepository<Candidate,Long> {
}

E vamos usá-lo:

@Service
@Transactional
public class CandidateServiceImpl implements CandidateService {
@Autowired
private CandidateRepository candidateRespository;

public void test(){
candidateRepository.findAll();
candidateRepository.save(new Candidate());
candidateRepository.save(Arrays.asList(new Candidate(),new Candidate()));
candidateRepository.count();
candidateRepository.delete(new Candidate());
candidateRepository.delete(1L);
candidateRepository.exists(1L);
candidateRepository.findAll(Example.of(new Candidate()));
candidateRepository.findOne(1L);
}

Como? O que foi isso? Criamos uma Interface, estendemos de JpaRepository, especificamos os genéricos a entidade e o tipo do identificador (Candidate e Long), e injetamos ele no Service. E não, não é um erro, não anotamos a interface para indicar que o Spring deve injetá-lo. Ele já sabe. E olha quantos métodos temos já disponíveis!
Tá, passou a empolgação inicial, talvez até explicou para algum colega o que acabou de ler, mas parou, pensou, refletiu… e chegou na conclusão que não é tão foda assim. Beleza. Desafio aceito. Veja agora a nossa interface:

public interface CandidateRepository extends JpaRepository<Candidate,Long> {List<Candidate> findByName(String name);
Candidate findByEmail(String email);
List<Candidate> findTop5ByCreationDate(Calendar creationDate);
Long countByNameLike(String partOfName);
List<Candidate> findByNameLikeOrderByCreationDateAsc(String partOfName);
}

Melhorou? Pelo nome do método, o Spring decide que tipo de consulta fazer sem precisar implementar nada. E tem muitas mais opções. Concatenar condições com And, fazer paginações passando o objeto Pageable como último parâmetro, e até acessando a atributos de os objetos relacionados @ManyToOne, @OneToMany, etc. E o melhor, é que o nome do método é bem intuitivo. Por exemplo, se temos um método com o seguinte nome:

List<Candidate> findByEmailLikeAndCreationDateAfterOrderByName(String partOfEmail, Calendar datReference);

É facil deduzir o significado da funcionalidade: procuramos candidatos com email parecido ao primeiro parâmetro, criados depois da data de especificada no segundo parâmetro, e ordenados por nome. Pode dar uma olhada na documentação para mais informações, é bem completa.

Usando o @Query

Vamos melhorar mais um pouco. Imagina que precisamos transformar o resultado num VO, ou que a consulta fica tão grande que a legibilidade do nome do método atrapalha mais do que ajuda. Vamos tentar com a consulta anterior, mas retornando um VO:

@Query("SELECT new br.com.devcave.recruiters.dto.CandidateVO(c.name, c.email) FORM Candidate c " +
" WHERE c.email like :email and c.creationDate > :datReference " +
" ORDER BY c.name ")
List<CandidateVO> findByEmailAndDatCreation(@Param("email") String partOfEmail,
@Param("datReference") Calendar datReference);

De novo, não precisamos implementar nada. Só declarar o método, anotar o @Query e ser feliz. O porque usar um VO e não a entidade é outro papo. Vamos abordar esse ponto mais embaixo. De novo, se o último parâmetro for um Pageable, o Spring vai fazer a mágica para nós.

Customizando as implementações

Beleza, isso resolveu bastante coisa. Mas, pensando naquela tela com 20 filtros e nenhum obrigatório, talvez as soluções apresentadas anteriormente não se ajustam ao cenário. Como resolver isso? Claro, sempre podemos criar um repositório clássico e implementar nosso método, mas o problema é que teremos duas injeções para acessar na mesma entidade. Para evitar isso, o Spring novamente nos fornece uma solução. Vamos criar uma interface específica para esses métodos que vamos ter que implementar:

public interface CandidateRepositoryCustom {List<Candidate> findByFilter(CandidateFilter filter);}

Na interface anterior CandidateRepository estendemos ela:

public interface CandidateRepository extends JpaRepository<Candidate, Long>, CandidateRepositoryCustom {Candidate findByEmail(String email);}

E criamos a classe CandidateRepositoryImpl implementando só o CandidateRepositoryCustom:

public class CandidateRepositoryImpl implements CandidateRepositoryCustom {@Autowired
private EntityManager entityManager;
@Override
public List<Candidate> findByFilter (final CandidateFilter filter) {
// Implementação
return null;
}
}

Agora, injetando o CandidateRepository, vamos ter a implementação pronta:

@Service
@Transactional(readOnly = true)
public class CandidateServiceImpl implements CandidateService {
@Autowired
private CandidateRepository candidateRepository;
public List<Candidate> search(CandidateFilter filter){
return candidateRepository.findByFilter(filter);
}
}

Claro que isso tem um preço. Os nomes das interfaces e as implementações devem coincidir exatamente nesse padrão. Mas, não é nada demais né? Ou estava pensando em outro nome melhor?

Nomenclatura, camadas e outras decisões

Uma dor de cabeça grande e que da pé a muita discussão, é a estrutura do projeto. Isso abrange desde os nomes dos pacotes até o número de camadas. Não existe uma um jeito certo. Depende muito do contexto, do projeto e até do cliente. O que realmente importa é seguir um padrão no projeto tudo para não virar bagunça, combinado de inicio com o time se for possível. E se alguém questiona depois, só falar que foi uma decisão de desenho. Pronto. Certeza que nem todos vão concordar e vão achar um milhão de artigos falando o por que o jeito que eles gostam é o certo. Você consegue justificar que o seu está certo também.
No projeto apresentado tomamos as seguintes decisões:

  • Camadas: Controlador, negócio e acesso à dados.
  • Entidades: Pacote domain, sem sufixo no nome da classe.
  • Camada de negócio: Pacote service, com interface com sufixo Service e implementação com o mesmo nome com Impl no final.
  • Camada de acesso à dados: Pacote repository, com sufixo Repository. Se tiver alguma implementação, seguimos o padrão apresentado acima.
  • Objetos em transferência: Pacote dto. Objetos usados para pesquisas, sufixo Filter. Objetos para criar objetos, sufixo Form. Objetos para mostrar na tela, sufixo VO.
  • Exceptions da aplicação: Pacote exception, herdando da exception base RecruitersGenericException tipo Runtime.

É o melhor jeito? Não, provavelmente não. Mas por enquanto chegamos nesse consenso. E como dizia Groucho Marx:

Como sempre, pode fazer um checkout do projeto para ver como está ficando:

> git clone https://github.com/iundarigun/recruiters.git
> cd recruiters
> git checkout feature/DCR-002

Proximamente, vamos continuar o tópico Cadastro de candidatos. Pela frente estamos pensando trazer cache, segurança, testes e muito mais. E se tiver uma proposta, não esqueça de comentar!
PD: Tem outra discussão sobre nomenclatura que não teve coragem de levantar. É referente ao idioma usado para escrever o código. Pessoalmente, escreveria em português, pois acho desnecessário ficar traduzindo os nomes. Mas sei que é uma luta perdida, pois sou dos únicos que pensam assim …
PD (2): No readme, tem umas breves instruções sobre como levantar um mysql no docker. Para quem não quer ensujar sua máquina.
PD(3): Você quer colaborar no blog? Tem alguma coisa legal para compartilhar? Fala com a gente, comenta aqui!

--

--